@salesforce/afv-skills 1.63.0 → 1.64.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/skills/agentforce-offline-experimentation-run/SKILL.md +253 -0
- package/skills/agentforce-offline-experimentation-run/scripts/build_report.py +796 -0
- package/skills/agentforce-offline-experimentation-run/scripts/resolve-token.sh +111 -0
- package/skills/agentforce-offline-experimentation-run/scripts/sfap_replay.py +1053 -0
- package/skills/automation-sandbox-post-copy-config-generate/SKILL.md +21 -11
- package/skills/automation-sandbox-post-copy-config-generate/assets/config_template.json +10 -0
- package/skills/automation-sandbox-post-copy-config-generate/assets/json_schema.json +33 -1
- package/skills/automation-sandbox-post-copy-config-generate/references/configuration_catalog.md +44 -1
- package/skills/automation-sandbox-post-copy-configure/SKILL.md +132 -124
- package/skills/automation-sandbox-post-copy-configure/references/flow_activation_path.md +151 -0
- package/skills/automation-sandbox-post-copy-configure/references/scheduled_apex_path.md +55 -9
- package/skills/consumer-goods-promotion-bo-api-deploy/SKILL.md +54 -107
- package/skills/consumer-goods-promotion-bo-api-deploy/assets/set-comment-value/payloads/copy.json +1 -1
- package/skills/consumer-goods-promotion-bo-api-deploy/references/conventions-and-payload-rules.md +82 -5
- package/skills/consumer-goods-promotion-bo-api-deploy/references/generate-and-wire.md +35 -2
- package/skills/consumer-goods-promotion-bo-api-deploy/references/preflight-and-seed-queries.md +149 -0
- package/skills/consumer-goods-promotion-bo-api-deploy/references/smoke-and-verify.md +17 -1
- package/skills/dx-devops-conflict-resolve/SKILL.md +1 -5
- package/skills/dx-devops-pipeline-manage/SKILL.md +1 -5
- package/skills/dx-devops-project-manage/SKILL.md +1 -5
- package/skills/dx-devops-promote/SKILL.md +1 -5
- package/skills/dx-devops-request-status/SKILL.md +1 -5
- package/skills/dx-lifecycle-center/SKILL.md +120 -0
- package/skills/dx-lifecycle-center/examples/workflows.md +175 -0
- package/skills/dx-lifecycle-center/references/connect-api.md +123 -0
- package/skills/dx-lifecycle-center/references/connect-org.md +184 -0
- package/skills/dx-lifecycle-center/references/deploy.md +153 -0
- package/skills/dx-lifecycle-center/references/error-handling.md +21 -0
- package/skills/dx-lifecycle-center/references/hub-org.md +37 -0
- package/skills/dx-lifecycle-center/references/list-orgs.md +99 -0
- package/skills/dx-lifecycle-center/references/request-body.md +75 -0
- package/skills/dx-lifecycle-center/references/troubleshooting.md +31 -0
- package/skills/dx-lifecycle-center/scripts/poll-deploy-status.sh +200 -0
- package/skills/dx-lifecycle-center/scripts/resolve-environment.sh +120 -0
- package/skills/dx-org-devhub-configure/SKILL.md +1 -3
- package/skills/dx-org-shape-manage/SKILL.md +1 -5
- package/skills/dx-org-snapshot-manage/SKILL.md +1 -3
- package/skills/education-cloud-academic-calendar-generate/SKILL.md +1 -3
- package/skills/education-cloud-course-catalog-migrate/SKILL.md +1 -3
- package/skills/education-cloud-domain-configure/SKILL.md +1 -3
- package/skills/education-cloud-multi-campus-configure/SKILL.md +1 -3
- package/skills/education-cloud-student-recruitment-agent-configure/SKILL.md +1 -5
- package/skills/experience-ui-bundle-2gp-deploy/SKILL.md +1 -3
- package/skills/experience-ui-bundle-features-generate/SKILL.md +1 -5
- package/skills/experience-ui-bundle-mfa-configure/SKILL.md +1 -3
- package/skills/field-service-data-capture-migrate/SKILL.md +1 -3
- package/skills/insurance-brokerage-agency-billing-configure/SKILL.md +1 -3
- package/skills/marketing-message-test/SKILL.md +1 -3
- package/skills/nonprofit-fundraising-configure/SKILL.md +240 -0
- package/skills/nonprofit-fundraising-configure/references/gotchas.md +26 -0
- package/skills/nonprofit-fundraising-configure/references/mcp-invocation.md +678 -0
- package/skills/nonprofit-fundraising-configure/references/permission-targeting.md +35 -0
- package/skills/nonprofit-volunteer-management-model-get/SKILL.md +166 -0
- package/skills/nonprofit-volunteer-management-model-get/references/entities.md +315 -0
- package/skills/nonprofit-volunteer-management-model-get/references/relationships.md +175 -0
- package/skills/nonprofit-volunteer-management-model-get/references/themes.md +143 -0
- package/skills/nonprofit-volunteer-management-records-create/SKILL.md +121 -0
- package/skills/nonprofit-volunteer-management-records-create/references/field-guide.md +284 -0
- package/skills/nonprofit-volunteer-management-records-create/references/mcp-invocation.md +248 -0
- package/skills/nonprofit-volunteer-management-records-create/references/review-format.md +140 -0
- package/skills/nonprofit-volunteer-management-records-create/scripts/expand-series.py +632 -0
- package/skills/nonprofit-volunteer-management-records-create/scripts/render-review-table.py +419 -0
- package/skills/nonprofit-volunteer-management-records-create/scripts/tests/test_expand_series.py +690 -0
- package/skills/nonprofit-volunteer-management-records-create/scripts/tests/test_render_review_table.py +421 -0
- package/skills/nonprofit-volunteer-management-structure-generate/SKILL.md +119 -0
- package/skills/nonprofit-volunteer-management-structure-generate/references/configuration-decisions.md +159 -0
- package/skills/nonprofit-volunteer-management-structure-generate/references/discovery-interview.md +137 -0
- package/skills/nonprofit-volunteer-management-structure-generate/references/edge-cases.md +48 -0
- package/skills/nonprofit-volunteer-management-structure-generate/references/mcp-invocation.md +75 -0
- package/skills/nonprofit-volunteer-management-structure-generate/references/plan-output.md +141 -0
- package/skills/platform-custom-field-generate/SKILL.md +1 -0
- package/skills/platform-custom-field-generate/references/advanced-picklists.md +5 -5
- package/skills/platform-custom-metadata-type-generate/SKILL.md +3 -0
- package/skills/platform-custom-object-generate/SKILL.md +4 -0
- package/skills/platform-custom-setting-generate/SKILL.md +3 -0
- package/skills/platform-datamask-run/SKILL.md +1 -5
- package/skills/platform-permission-set-generate/SKILL.md +3 -0
- package/skills/platform-salesforce-connect-adapter-generate/SKILL.md +1 -3
- package/skills/platform-sandbox-configure/SKILL.md +1 -3
- package/skills/platform-value-set-generate/SKILL.md +3 -0
- package/skills/sales-call-scoring-configure/SKILL.md +1 -5
- package/skills/service-agentforce-channel-configure/SKILL.md +1 -3
- package/skills/service-agentforce-human-escalation-configure/SKILL.md +1 -3
- package/skills/service-catalog-template-coordinate/SKILL.md +1 -3
- package/skills/service-concierge-portal-generate/SKILL.md +1 -3
- package/skills/service-email-to-case-configure/SKILL.md +1 -5
- package/skills/service-itsm-agentic-setup-agent-runtime-access-assign/SKILL.md +1 -3
- package/skills/service-itsm-agentic-setup-agentforce-coordinate/SKILL.md +1 -3
- package/skills/service-itsm-agentic-setup-agentforce-studio-configure/SKILL.md +1 -3
- package/skills/service-itsm-agentic-setup-agentforce-studio-validate/SKILL.md +1 -3
- package/skills/service-itsm-agentic-setup-cmdb-access-assign/SKILL.md +1 -3
- package/skills/service-itsm-agentic-setup-cmdb-bundle-deploy/SKILL.md +1 -5
- package/skills/service-itsm-agentic-setup-cmdb-configure/SKILL.md +1 -3
- package/skills/service-itsm-agentic-setup-cmdb-coordinate/SKILL.md +1 -3
- package/skills/service-itsm-agentic-setup-cmdb-discovery-configure/SKILL.md +1 -3
- package/skills/service-itsm-agentic-setup-employee-agent-configure/SKILL.md +1 -3
- package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/SKILL.md +1 -3
- package/skills/service-itsm-agentic-setup-incident-sla-configure/SKILL.md +1 -0
- package/skills/service-itsm-agentic-setup-itsm-agentforce-permset-assign/SKILL.md +1 -3
- package/skills/service-itsm-agentic-setup-uel-user-create/SKILL.md +1 -9
- package/skills/service-itsm-incident-mgmt-configure/SKILL.md +1 -5
- package/skills/service-itsm-incident-priority-configure/SKILL.md +1 -3
- package/skills/service-itsm-slack-itservice-configure/SKILL.md +1 -3
- package/skills/service-itsm-swarming-configure/SKILL.md +1 -3
- package/skills/service-itsm-teams-configure/SKILL.md +1 -3
- package/skills/service-itsm-teams-debug/SKILL.md +1 -3
- package/skills/service-itsm-teams-employee-agent-configure/SKILL.md +1 -3
- package/skills/service-itsm-teams-itdesk-configure/SKILL.md +1 -3
- package/skills/service-itsm-teams-itservice-configure/SKILL.md +1 -3
- package/skills/service-native-voice-recording-transcription-configure/SKILL.md +1 -3
- package/skills/service-omni-agent-users-create/SKILL.md +1 -3
- package/skills/service-omni-agent-work-sharing-configure/SKILL.md +1 -3
- package/skills/service-omni-attribute-routing-configure/SKILL.md +1 -3
- package/skills/service-omni-base-settings-configure/SKILL.md +1 -3
- package/skills/service-omni-channel-inventory-analyze/SKILL.md +1 -3
- package/skills/service-omni-channel-limits-analyze/SKILL.md +1 -3
- package/skills/service-omni-channel-setup-coordinate/SKILL.md +1 -3
- package/skills/service-omni-command-center-analyze/SKILL.md +1 -3
- package/skills/service-omni-command-center-configure/SKILL.md +1 -3
- package/skills/service-omni-permission-set-assign/SKILL.md +1 -3
- package/skills/service-omni-presence-status-deploy/SKILL.md +1 -3
- package/skills/service-omni-presence-user-config-deploy/SKILL.md +1 -3
- package/skills/service-omni-queue-deploy/SKILL.md +1 -3
- package/skills/service-omni-queue-members-assign/SKILL.md +1 -3
- package/skills/service-omni-queue-routing-config-deploy/SKILL.md +1 -3
- package/skills/service-omni-routing-flow-deploy/SKILL.md +1 -3
- package/skills/service-omni-service-channel-configure/SKILL.md +1 -3
- package/skills/service-omni-sidebar-configure/SKILL.md +1 -3
- package/skills/service-omni-skills-based-routing-configure/SKILL.md +1 -3
- package/skills/service-omni-supervisor-config-deploy/SKILL.md +1 -3
- package/skills/service-omni-supervisor-permset-assign/SKILL.md +1 -3
- package/skills/service-omni-supervisor-surface-deploy/SKILL.md +1 -3
- package/skills/service-omni-supervisor-users-create/SKILL.md +1 -3
- package/skills/service-omni-work-skill-routing-configure/SKILL.md +1 -3
- package/skills/field-service-setup-orchestrator-get/SKILL.md +0 -47
- package/skills/tableau-next-asset-create/README.md +0 -65
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
"""Tests for render-review-table.py — the fixed-column review table.
|
|
2
|
+
|
|
3
|
+
Run from the skill's scripts/tests directory:
|
|
4
|
+
cd contributed-skills/npc-titan/nonprofit-volunteer-management-records-create/scripts/tests
|
|
5
|
+
python3 -m unittest test_render_review_table
|
|
6
|
+
|
|
7
|
+
Covers: one column order for every row regardless of the order fields arrive in, label
|
|
8
|
+
headers, lookups last, required fields shown as needs-input, optional fields with no value
|
|
9
|
+
omitted, only reuse and needs-input tags printed, the Name column dropped when the row label shows it, same-on-every-row columns moved
|
|
10
|
+
above the table, identical-row grouping (and --expand), and the spec errors the agent can hit.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import importlib.util
|
|
14
|
+
import json
|
|
15
|
+
import os
|
|
16
|
+
import subprocess
|
|
17
|
+
import sys
|
|
18
|
+
import unittest
|
|
19
|
+
|
|
20
|
+
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
21
|
+
SCRIPT = os.path.join(os.path.dirname(HERE), "render-review-table.py")
|
|
22
|
+
|
|
23
|
+
_spec = importlib.util.spec_from_file_location("render_review_table", SCRIPT)
|
|
24
|
+
rrt = importlib.util.module_from_spec(_spec)
|
|
25
|
+
_spec.loader.exec_module(rrt)
|
|
26
|
+
GUIDE = rrt.parse_field_guide(rrt.DEFAULT_FIELD_GUIDE)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def f(value, origin="given"):
|
|
30
|
+
return {"value": value, "origin": origin}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def render(spec, expand=False):
|
|
34
|
+
return rrt.build_table(spec, GUIDE, expand=expand)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def run_cli(spec, *args):
|
|
38
|
+
return subprocess.run([sys.executable, SCRIPT, *args], input=json.dumps(spec),
|
|
39
|
+
capture_output=True, text=True)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def header_row(table):
|
|
43
|
+
return [line for line in table.splitlines() if line.startswith("| Record")][0]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def body_rows(table):
|
|
47
|
+
lines = table.splitlines()
|
|
48
|
+
start = lines.index(header_row(table)) + 2
|
|
49
|
+
rows = []
|
|
50
|
+
for line in lines[start:]:
|
|
51
|
+
if not line.startswith("|"):
|
|
52
|
+
break
|
|
53
|
+
rows.append([c.strip() for c in line.strip().strip("|").split("|")])
|
|
54
|
+
return rows
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def headers(table):
|
|
58
|
+
return [c.strip() for c in header_row(table).strip().strip("|").split("|")]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class FieldGuideParsing(unittest.TestCase):
|
|
62
|
+
def test_every_creatable_entity_is_parsed(self):
|
|
63
|
+
for entity in ("VolunteerInitiative", "Location", "Address", "OperatingHours",
|
|
64
|
+
"TimeSlot", "Position", "PositionQualification", "JobPosition",
|
|
65
|
+
"JobPositionQualification", "JobPositionShift", "Competency",
|
|
66
|
+
"Examination"):
|
|
67
|
+
self.assertIn(entity, GUIDE, entity)
|
|
68
|
+
self.assertTrue(GUIDE[entity], entity)
|
|
69
|
+
|
|
70
|
+
def test_relationship_rows_map_to_api_names(self):
|
|
71
|
+
apis = {fld["api"]: fld for fld in GUIDE["JobPosition"]}
|
|
72
|
+
self.assertIn("VolunteerInitiativeId", apis)
|
|
73
|
+
self.assertTrue(apis["VolunteerInitiativeId"]["lookup"])
|
|
74
|
+
self.assertFalse(apis["Title"]["lookup"])
|
|
75
|
+
self.assertTrue(apis["Title"]["schema_required"])
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class MixedKeyOrder(unittest.TestCase):
|
|
79
|
+
"""The defect this script exists for: fields arriving in a different order per record."""
|
|
80
|
+
|
|
81
|
+
def spec(self):
|
|
82
|
+
return {"entity": "JobPositionShift", "records": [
|
|
83
|
+
{"label": "Sat 3 Oct", "fields": {
|
|
84
|
+
"JobPositionId": f("Greeter — Eastside", "reuse"),
|
|
85
|
+
"StartDate": f("2026-10-03"), "EndDate": f("2026-10-03"),
|
|
86
|
+
"StartTime": f("09:00"), "EndTime": f("12:00"),
|
|
87
|
+
"TimeZone": f("America/Chicago", "default"),
|
|
88
|
+
"Status": f("Upcoming", "default"), "MaximumAttendeesCount": f(8)}},
|
|
89
|
+
{"label": "Sun 4 Oct", "fields": {
|
|
90
|
+
"MaximumAttendeesCount": f(6), "Status": f("Draft"),
|
|
91
|
+
"EndTime": f("16:00"), "TimeZone": f("America/Denver"),
|
|
92
|
+
"StartTime": f("13:00"), "EndDate": f("2026-10-04"),
|
|
93
|
+
"StartDate": f("2026-10-04"),
|
|
94
|
+
"JobPositionId": f("Greeter — Westside", "reuse")}},
|
|
95
|
+
{"label": "Sat 10 Oct", "fields": {
|
|
96
|
+
"Status": f("Upcoming", "default"), "StartTime": f("09:00"),
|
|
97
|
+
"JobPositionId": f("Greeter — Eastside", "reuse"),
|
|
98
|
+
"TimeZone": f("America/Chicago", "default"), "EndDate": f("2026-10-10"),
|
|
99
|
+
"MaximumAttendeesCount": f(8), "EndTime": f("12:00"),
|
|
100
|
+
"StartDate": f("2026-10-10")}},
|
|
101
|
+
]}
|
|
102
|
+
|
|
103
|
+
def test_every_value_lands_under_its_own_header(self):
|
|
104
|
+
table = render(self.spec())
|
|
105
|
+
cols = headers(table)
|
|
106
|
+
rows = body_rows(table)
|
|
107
|
+
self.assertEqual(len(rows), 3)
|
|
108
|
+
expected = [
|
|
109
|
+
{"Start Date": "2026-10-03", "Start Time": "09:00", "End Time": "12:00",
|
|
110
|
+
"Maximum Attendees Count": "8", "Status": "Upcoming",
|
|
111
|
+
"Time Zone": "America/Chicago", "Job Position": "Greeter — Eastside `reuse`"},
|
|
112
|
+
{"Start Date": "2026-10-04", "Start Time": "13:00", "End Time": "16:00",
|
|
113
|
+
"Maximum Attendees Count": "6", "Status": "Draft",
|
|
114
|
+
"Time Zone": "America/Denver", "Job Position": "Greeter — Westside `reuse`"},
|
|
115
|
+
{"Start Date": "2026-10-10", "Start Time": "09:00", "End Time": "12:00",
|
|
116
|
+
"Maximum Attendees Count": "8", "Status": "Upcoming",
|
|
117
|
+
"Time Zone": "America/Chicago", "Job Position": "Greeter — Eastside `reuse`"},
|
|
118
|
+
]
|
|
119
|
+
for row, want in zip(rows, expected):
|
|
120
|
+
cells = dict(zip(cols, row))
|
|
121
|
+
for header, value in want.items():
|
|
122
|
+
self.assertEqual(cells[header], value, header)
|
|
123
|
+
|
|
124
|
+
def test_column_order_is_tier_order_then_lookups(self):
|
|
125
|
+
cols = headers(render(self.spec()))
|
|
126
|
+
self.assertEqual(cols, [
|
|
127
|
+
"Record", "Start Date", "End Date", "Start Time", "End Time", "Time Zone",
|
|
128
|
+
"Status", "Maximum Attendees Count", "Job Position"])
|
|
129
|
+
|
|
130
|
+
def test_order_does_not_depend_on_the_first_record(self):
|
|
131
|
+
spec = self.spec()
|
|
132
|
+
reversed_spec = {"entity": spec["entity"], "records": list(reversed(spec["records"]))}
|
|
133
|
+
self.assertEqual(headers(render(spec)), headers(render(reversed_spec)))
|
|
134
|
+
|
|
135
|
+
def test_relationship_name_and_api_name_are_one_column(self):
|
|
136
|
+
spec = {"entity": "JobPosition", "records": [
|
|
137
|
+
{"fields": {"Name": f("A"), "Title": f("A"),
|
|
138
|
+
"VolunteerInitiative": f("Food Pantry", "reuse")}},
|
|
139
|
+
{"fields": {"Name": f("B"), "Title": f("B"),
|
|
140
|
+
"VolunteerInitiativeId": f("Food Drive", "reuse")}},
|
|
141
|
+
]}
|
|
142
|
+
cols = headers(render(spec))
|
|
143
|
+
self.assertEqual(cols.count("Volunteer Initiative"), 1)
|
|
144
|
+
self.assertEqual(cols[-1], "Volunteer Initiative")
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class Headers(unittest.TestCase):
|
|
148
|
+
def test_labels_not_api_names(self):
|
|
149
|
+
spec = {"entity": "VolunteerInitiative", "records": [
|
|
150
|
+
{"fields": {"Name": f("Child"), "ParentVolunteerInitiative":
|
|
151
|
+
f("Parent Program", "reuse")}}]}
|
|
152
|
+
cols = headers(render(spec))
|
|
153
|
+
self.assertIn("Parent Volunteer Initiative", cols)
|
|
154
|
+
self.assertNotIn("ParentVolunteerInitiativeId", cols)
|
|
155
|
+
|
|
156
|
+
def test_describe_label_overrides(self):
|
|
157
|
+
spec = {"entity": "JobPosition",
|
|
158
|
+
"labels": {"VolunteerInitiativeId": "Related Volunteer Initiative"},
|
|
159
|
+
"records": [{"fields": {"Name": f("A"), "Title": f("A"),
|
|
160
|
+
"VolunteerInitiativeId": f("P", "new")}}]}
|
|
161
|
+
self.assertIn("Related Volunteer Initiative", headers(render(spec)))
|
|
162
|
+
|
|
163
|
+
def test_custom_fields_after_standard_before_lookups(self):
|
|
164
|
+
spec = {"entity": "JobPosition", "records": [{"fields": {
|
|
165
|
+
"Name": f("A"), "Title": f("A"), "Region__c": f("North"),
|
|
166
|
+
"LocationId": f("Eastside Hall", "reuse")}}]}
|
|
167
|
+
cols = headers(render(spec))
|
|
168
|
+
self.assertEqual(cols, ["Record", "Title", "Region", "Location"])
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
class MissingFields(unittest.TestCase):
|
|
172
|
+
def test_optional_field_with_no_value_is_omitted(self):
|
|
173
|
+
spec = {"entity": "VolunteerInitiative", "records": [
|
|
174
|
+
{"fields": {"Name": f("A"), "Status": f("Upcoming", "default")}},
|
|
175
|
+
{"fields": {"Name": f("B"), "Status": f("Upcoming", "default")}}]}
|
|
176
|
+
cols = headers(render(spec))
|
|
177
|
+
self.assertNotIn("Start Date", cols)
|
|
178
|
+
self.assertNotIn("Description", cols)
|
|
179
|
+
|
|
180
|
+
def test_optional_field_set_on_one_row_shows_dash_elsewhere(self):
|
|
181
|
+
spec = {"entity": "VolunteerInitiative", "records": [
|
|
182
|
+
{"fields": {"Name": f("A"), "Description": f("Weekly pantry", "generated")}},
|
|
183
|
+
{"fields": {"Name": f("B")}}]}
|
|
184
|
+
table = render(spec)
|
|
185
|
+
cells = dict(zip(headers(table), body_rows(table)[1]))
|
|
186
|
+
self.assertEqual(cells["Description"], "—")
|
|
187
|
+
|
|
188
|
+
def test_required_field_missing_shows_needs_input(self):
|
|
189
|
+
spec = {"entity": "JobPosition", "records": [{"fields": {"Name": f("Greeter")}}]}
|
|
190
|
+
table = render(spec)
|
|
191
|
+
cells = dict(zip(headers(table), body_rows(table)[0]))
|
|
192
|
+
self.assertEqual(cells["Title"], "`needs-input`")
|
|
193
|
+
self.assertIn("Still needed before this can be written: Title (row 1)", table)
|
|
194
|
+
|
|
195
|
+
def test_explicit_needs_input_origin(self):
|
|
196
|
+
spec = {"entity": "Location", "records": [
|
|
197
|
+
{"fields": {"Name": f("Eastside Hall"),
|
|
198
|
+
"LocationType": f(None, "needs-input")}},
|
|
199
|
+
{"fields": {"Name": f("Westside Hall"),
|
|
200
|
+
"LocationType": f(None, "needs-input")}}]}
|
|
201
|
+
table = render(spec)
|
|
202
|
+
self.assertIn("Location Type (rows 1, 2)", table)
|
|
203
|
+
|
|
204
|
+
def test_schema_required_blank_value_is_needs_input(self):
|
|
205
|
+
spec = {"entity": "JobPosition", "records": [
|
|
206
|
+
{"fields": {"Name": f("A"), "Title": f("", "given")}}]}
|
|
207
|
+
self.assertIn("Title (row 1)", render(spec))
|
|
208
|
+
|
|
209
|
+
def test_always_set_but_not_schema_required_can_be_left_blank(self):
|
|
210
|
+
spec = {"entity": "Address", "records": [{"fields": {
|
|
211
|
+
"ParentId": f("Eastside Hall", "new"), "LocationType": f("Business Site"),
|
|
212
|
+
"Street": f("12 Main St"), "City": f("Austin"), "State": f("TX"),
|
|
213
|
+
"PostalCode": f("78701"), "Country": f("", "given")}}]}
|
|
214
|
+
table = render(spec)
|
|
215
|
+
cells = dict(zip(headers(table), body_rows(table)[0]))
|
|
216
|
+
self.assertEqual(cells["Country"], "—")
|
|
217
|
+
self.assertNotIn("Still needed", table)
|
|
218
|
+
|
|
219
|
+
def test_required_custom_field(self):
|
|
220
|
+
spec = {"entity": "JobPosition", "requiredCustomFields": ["Region__c"],
|
|
221
|
+
"records": [{"fields": {"Name": f("A"), "Title": f("A")}}]}
|
|
222
|
+
table = render(spec)
|
|
223
|
+
self.assertIn("Region", headers(table))
|
|
224
|
+
self.assertIn("Region (row 1)", table)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
class Grouping(unittest.TestCase):
|
|
228
|
+
def spec(self):
|
|
229
|
+
same = {"JobPositionId": f("Sorter", "reuse"), "StartDate": f("2026-10-03"),
|
|
230
|
+
"EndDate": f("2026-10-03"), "StartTime": f("09:00"), "EndTime": f("12:00"),
|
|
231
|
+
"TimeZone": f("America/Chicago", "default")}
|
|
232
|
+
other = dict(same, StartTime=f("13:00"), EndTime=f("16:00"))
|
|
233
|
+
return {"entity": "JobPositionShift", "records": [
|
|
234
|
+
{"fields": dict(same)}, {"fields": dict(same)}, {"fields": dict(same)},
|
|
235
|
+
{"fields": other}]}
|
|
236
|
+
|
|
237
|
+
def test_identical_rows_collapse(self):
|
|
238
|
+
rows = body_rows(render(self.spec()))
|
|
239
|
+
self.assertEqual(len(rows), 2)
|
|
240
|
+
self.assertTrue(rows[0][0].startswith("Rows 1–3"))
|
|
241
|
+
self.assertIn('say "show all 3"', rows[0][0])
|
|
242
|
+
self.assertEqual(rows[1][0], "4")
|
|
243
|
+
|
|
244
|
+
def test_expand_lists_every_row(self):
|
|
245
|
+
rows = body_rows(render(self.spec(), expand=True))
|
|
246
|
+
self.assertEqual([r[0] for r in rows], ["1", "2", "3", "4"])
|
|
247
|
+
|
|
248
|
+
def test_only_consecutive_rows_group(self):
|
|
249
|
+
spec = self.spec()
|
|
250
|
+
spec["records"] = [spec["records"][0], spec["records"][3], spec["records"][1]]
|
|
251
|
+
self.assertEqual(len(body_rows(render(spec))), 3)
|
|
252
|
+
|
|
253
|
+
def test_different_names_do_not_group(self):
|
|
254
|
+
spec = {"entity": "Competency", "records": [
|
|
255
|
+
{"fields": {"Name": f("Forklift")}}, {"fields": {"Name": f("First Aid")}}]}
|
|
256
|
+
self.assertEqual(len(body_rows(render(spec))), 2)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
class NameColumn(unittest.TestCase):
|
|
260
|
+
def test_name_matching_label_is_not_repeated(self):
|
|
261
|
+
spec = {"entity": "Position", "records": [
|
|
262
|
+
{"fields": {"Name": f("Planter"), "Description": f("Plants trees", "generated")}},
|
|
263
|
+
{"fields": {"Name": f("Site Lead", "generated"),
|
|
264
|
+
"Description": f("Leads the crew", "generated")}}]}
|
|
265
|
+
table = render(spec)
|
|
266
|
+
self.assertNotIn("Name", headers(table))
|
|
267
|
+
self.assertEqual([r[0] for r in body_rows(table)],
|
|
268
|
+
["1 · Planter", "2 · Site Lead"])
|
|
269
|
+
|
|
270
|
+
def test_name_kept_when_a_label_differs(self):
|
|
271
|
+
spec = {"entity": "Position", "records": [
|
|
272
|
+
{"label": "Role A", "fields": {"Name": f("Planter")}},
|
|
273
|
+
{"fields": {"Name": f("Site Lead")}}]}
|
|
274
|
+
self.assertIn("Name", headers(render(spec)))
|
|
275
|
+
|
|
276
|
+
def test_missing_name_keeps_the_column(self):
|
|
277
|
+
spec = {"entity": "JobPosition", "records": [
|
|
278
|
+
{"fields": {"Name": f(None, "needs-input"), "Title": f("A")}}]}
|
|
279
|
+
table = render(spec)
|
|
280
|
+
self.assertIn("Name", headers(table))
|
|
281
|
+
self.assertIn("Name (row 1)", table)
|
|
282
|
+
|
|
283
|
+
def test_same_fields_different_names_still_two_rows(self):
|
|
284
|
+
spec = {"entity": "Position", "records": [
|
|
285
|
+
{"fields": {"Name": f("Planter"), "Status": f("Upcoming", "default")}},
|
|
286
|
+
{"fields": {"Name": f("Waterer"), "Status": f("Upcoming", "default")}}]}
|
|
287
|
+
self.assertEqual(len(body_rows(render(spec))), 2)
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
class SharedColumns(unittest.TestCase):
|
|
291
|
+
def spec(self):
|
|
292
|
+
return {"entity": "JobPosition", "records": [
|
|
293
|
+
{"fields": {"Name": f("Planter – LA"), "Title": f("Planter", "default"),
|
|
294
|
+
"Status": f("Upcoming", "default"),
|
|
295
|
+
"LocationId": f("Los Angeles", "new")}},
|
|
296
|
+
{"fields": {"Name": f("Planter – NY"), "Title": f("Planter", "default"),
|
|
297
|
+
"Status": f("Upcoming", "default"),
|
|
298
|
+
"LocationId": f("New York", "new")}}]}
|
|
299
|
+
|
|
300
|
+
def test_same_value_on_every_row_moves_above_the_table(self):
|
|
301
|
+
table = render(self.spec())
|
|
302
|
+
self.assertIn("Same on every row: Title Planter · Status Upcoming", table)
|
|
303
|
+
self.assertEqual(headers(table), ["Record", "Location"])
|
|
304
|
+
|
|
305
|
+
def test_a_single_row_keeps_every_column(self):
|
|
306
|
+
spec = self.spec()
|
|
307
|
+
spec["records"] = spec["records"][:1]
|
|
308
|
+
table = render(spec)
|
|
309
|
+
self.assertNotIn("Same on every row", table)
|
|
310
|
+
self.assertIn("Status", headers(table))
|
|
311
|
+
|
|
312
|
+
def test_needs_input_column_stays_in_the_table(self):
|
|
313
|
+
spec = {"entity": "Location", "records": [
|
|
314
|
+
{"fields": {"Name": f("A"), "LocationType": f(None, "needs-input")}},
|
|
315
|
+
{"fields": {"Name": f("B"), "LocationType": f(None, "needs-input")}}]}
|
|
316
|
+
table = render(spec)
|
|
317
|
+
self.assertIn("Location Type", headers(table))
|
|
318
|
+
self.assertNotIn("Same on every row", table)
|
|
319
|
+
|
|
320
|
+
def test_rows_that_differ_only_in_shared_columns_still_group(self):
|
|
321
|
+
spec = {"entity": "JobPositionShift", "records": [
|
|
322
|
+
{"fields": {"JobPositionId": f("Sorter", "reuse"), "StartDate": f("2026-10-03"),
|
|
323
|
+
"StartTime": f("09:00")}}] * 3}
|
|
324
|
+
rows = body_rows(render(spec))
|
|
325
|
+
self.assertEqual(len(rows), 1)
|
|
326
|
+
self.assertTrue(rows[0][0].startswith("Rows 1–3"))
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
class OriginTags(unittest.TestCase):
|
|
330
|
+
def spec(self):
|
|
331
|
+
return {"entity": "JobPosition", "records": [{"fields": {
|
|
332
|
+
"Name": f("Planter – LA"), "Title": f("Planter", "default"),
|
|
333
|
+
"Description": f("Plant trees", "generated"),
|
|
334
|
+
"LocationId": f("Los Angeles", "new"),
|
|
335
|
+
"VolunteerInitiativeId": f("Tree Planting – LA", "reuse")}}]}
|
|
336
|
+
|
|
337
|
+
def test_only_reuse_is_tagged(self):
|
|
338
|
+
table = render(self.spec())
|
|
339
|
+
cells = dict(zip(headers(table), body_rows(table)[0]))
|
|
340
|
+
self.assertEqual(cells["Title"], "Planter")
|
|
341
|
+
self.assertEqual(cells["Description"], "Plant trees")
|
|
342
|
+
self.assertEqual(cells["Location"], "Los Angeles")
|
|
343
|
+
self.assertEqual(cells["Volunteer Initiative"], "Tree Planting – LA `reuse`")
|
|
344
|
+
for tag in ("`given`", "`default`", "`generated`", "`new`"):
|
|
345
|
+
self.assertNotIn(tag, table)
|
|
346
|
+
|
|
347
|
+
def test_reuse_note_only_when_a_cell_is_reused(self):
|
|
348
|
+
self.assertIn("`reuse` = links to a record already in your org", render(self.spec()))
|
|
349
|
+
spec = self.spec()
|
|
350
|
+
spec["records"][0]["fields"]["VolunteerInitiativeId"] = f("Tree Planting – LA", "new")
|
|
351
|
+
self.assertNotIn("`reuse` =", render(spec))
|
|
352
|
+
|
|
353
|
+
def test_needs_input_note_when_something_is_missing(self):
|
|
354
|
+
spec = {"entity": "JobPosition", "records": [{"fields": {"Name": f("Greeter")}}]}
|
|
355
|
+
self.assertIn("`needs-input` = required, still missing", render(spec))
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
class Title(unittest.TestCase):
|
|
359
|
+
def test_default_title_counts_rows(self):
|
|
360
|
+
spec = {"entity": "Competency", "records": [
|
|
361
|
+
{"fields": {"Name": f("A")}}, {"fields": {"Name": f("B")}}]}
|
|
362
|
+
self.assertTrue(render(spec).startswith("Records to insert — 2 Competencies"))
|
|
363
|
+
|
|
364
|
+
def test_series_title_override(self):
|
|
365
|
+
spec = {"entity": "JobPositionShift",
|
|
366
|
+
"title": "Records to insert — 12 Job Position Shifts (1 series)",
|
|
367
|
+
"records": [{"label": "Saturdays 9:00–12:00", "fields": {
|
|
368
|
+
"JobPositionId": f("Driver", "new"),
|
|
369
|
+
"StartDate": f("every Saturday from 3 Oct 2026"),
|
|
370
|
+
"EndDate": f("same day"), "StartTime": f("09:00"), "EndTime": f("12:00"),
|
|
371
|
+
"TimeZone": f("America/Chicago", "default")}}]}
|
|
372
|
+
table = render(spec)
|
|
373
|
+
self.assertTrue(table.startswith("Records to insert — 12 Job Position Shifts (1 series)"))
|
|
374
|
+
self.assertNotIn("Still needed", table)
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
class Errors(unittest.TestCase):
|
|
378
|
+
def assert_fails(self, spec, fragment):
|
|
379
|
+
result = run_cli(spec)
|
|
380
|
+
self.assertNotEqual(result.returncode, 0)
|
|
381
|
+
self.assertIn(fragment, result.stderr)
|
|
382
|
+
|
|
383
|
+
def test_unknown_entity(self):
|
|
384
|
+
self.assert_fails({"entity": "JobPositionAssignment",
|
|
385
|
+
"records": [{"fields": {"Name": f("x")}}]}, "unknown entity")
|
|
386
|
+
|
|
387
|
+
def test_undocumented_field(self):
|
|
388
|
+
self.assert_fails({"entity": "Position", "records": [
|
|
389
|
+
{"fields": {"Name": f("x"), "Category": f("y")}}]}, "not in field-guide.md")
|
|
390
|
+
|
|
391
|
+
def test_system_managed_field(self):
|
|
392
|
+
self.assert_fails({"entity": "JobPositionShift", "records": [
|
|
393
|
+
{"fields": {"RemainingCapacity": f(3)}}]}, "system-managed")
|
|
394
|
+
|
|
395
|
+
def test_bad_origin(self):
|
|
396
|
+
self.assert_fails({"entity": "Competency", "records": [
|
|
397
|
+
{"fields": {"Name": f("x", "seeded")}}]}, "origin")
|
|
398
|
+
|
|
399
|
+
def test_lookup_needs_reuse_or_new(self):
|
|
400
|
+
self.assert_fails({"entity": "JobPosition", "records": [{"fields": {
|
|
401
|
+
"Name": f("x"), "Title": f("x"), "LocationId": f("Hall", "given")}}]},
|
|
402
|
+
"is a lookup")
|
|
403
|
+
|
|
404
|
+
def test_lookup_shows_name_not_id(self):
|
|
405
|
+
self.assert_fails({"entity": "JobPosition", "records": [{"fields": {
|
|
406
|
+
"Name": f("x"), "Title": f("x"),
|
|
407
|
+
"LocationId": f("131Hs000000Abc1IAC", "reuse")}}]}, "looks like a record Id")
|
|
408
|
+
|
|
409
|
+
def test_empty_records(self):
|
|
410
|
+
self.assert_fails({"entity": "Competency", "records": []}, "non-empty list")
|
|
411
|
+
|
|
412
|
+
def test_cli_happy_path(self):
|
|
413
|
+
result = run_cli({"entity": "Competency", "records": [
|
|
414
|
+
{"fields": {"Name": f("Forklift Operation")}}]})
|
|
415
|
+
self.assertEqual(result.returncode, 0, result.stderr)
|
|
416
|
+
self.assertIn("Records to insert — 1 Competency", result.stdout)
|
|
417
|
+
self.assertIn("| 1 · Forklift Operation |", result.stdout)
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
if __name__ == "__main__":
|
|
421
|
+
unittest.main()
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: nonprofit-volunteer-management-structure-generate
|
|
3
|
+
description: "Use this skill to plan a Salesforce Nonprofit Cloud Volunteer Management (VM) program before anything is created: it interviews the user about their programs, sites, volunteer roles, requirements, and shift schedules, checks the org read-only for records to reuse, and presents a plan of the records the program needs. It never writes; on the user's yes it hands the plan to nonprofit-volunteer-management-records-create. TRIGGER when a user wants to plan, design, or structure a VM program, or asks \"what records do we need\" or \"how should we model this program\". DO NOT TRIGGER to explain what a VM object or field is for (use nonprofit-volunteer-management-model-get), to write records (use nonprofit-volunteer-management-records-create), to enable Volunteer Management in an org, for per-person records (JobPositionAssignment, PersonLocationAvailability, PersonCompetency / PersonExamination, ConstituentRole), or for NPSP / Volunteers for Salesforce (V4S), which is unsupported."
|
|
4
|
+
metadata:
|
|
5
|
+
version: "1.2"
|
|
6
|
+
domains: ["Nonprofit"]
|
|
7
|
+
relatedSkills:
|
|
8
|
+
- "nonprofit-volunteer-management-model-get"
|
|
9
|
+
- "nonprofit-volunteer-management-records-create"
|
|
10
|
+
cliTools:
|
|
11
|
+
- tool: ["sf"]
|
|
12
|
+
semver: ">=2.0.0"
|
|
13
|
+
accessCheck: "(OrgPermissions.VolunteerManagement) && (OrgPreferences.VolunteerManagementEnabled)"
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
# Nonprofit Cloud Volunteer Management — Structure Planning
|
|
17
|
+
|
|
18
|
+
This skill turns a description of a volunteer program ("I run a weekly food pantry with team leads and general volunteers") into a plan of Volunteer Management records: the Volunteer Initiatives, Locations, Competencies, Examinations, Positions, Job Positions, their qualifications, and Job Position Shift series the program needs. It asks the discovery questions, walks through each configuration decision, checks the org for records it can reuse, and presents the plan. It adds only the structure the program needs.
|
|
19
|
+
|
|
20
|
+
## Hard rules
|
|
21
|
+
|
|
22
|
+
- **Never delete records.** No delete, rollback or cleanup of any kind. If a write fails part-way, report what was created and what wasn't; never undo it.
|
|
23
|
+
- **Never act on NPSP or Volunteers for Salesforce (V4S).** That legacy package is unsupported here. Say so and stop.
|
|
24
|
+
- **No acronyms or internal labels in what the user sees.** Write out Volunteer Initiative, Job Position, Job Position Shift, Job Position Assignment, Position Qualification, Job Position Qualification, Data Processing Engine, and "out of the box". Never show internal step or section labels, or a record's Id: name a reused record by its name; its Id goes only in the handoff.
|
|
25
|
+
- **Never create records.** This skill plans. It hands the plan to `nonprofit-volunteer-management-records-create` only after the user answers yes to "Do you want to create the records from the plan?".
|
|
26
|
+
|
|
27
|
+
## Mechanism
|
|
28
|
+
|
|
29
|
+
Org calls go through the connection the session has: the headless-360 tools (`dispatch_readonly` only; this skill never writes) or an authenticated Salesforce CLI. Both return the same results at API v67.0. Tell the user which one is in use. If neither is connected, stop and ask the user to connect one; never guess an org result. The exact call for each step, in both forms, is in `references/mcp-invocation.md`.
|
|
30
|
+
|
|
31
|
+
This skill only reads. With no org connection, planning still completes; see step 1.
|
|
32
|
+
|
|
33
|
+
## Scope
|
|
34
|
+
|
|
35
|
+
- **In scope:** the discovery interview, the configuration decisions, reuse checks against the org, the plan summary, and handing a confirmed plan to `nonprofit-volunteer-management-records-create`.
|
|
36
|
+
- **Out of scope:** writing records (records-create), setting field values (records-create's `field-guide.md`), explaining what an entity or field means (`nonprofit-volunteer-management-model-get`), turning Volunteer Management on, and day-to-day operations such as matching, assignments, or a volunteer's own availability and qualifications. Operating Hours and Time Slots are not planned here; records-create creates them only when the user asks for them.
|
|
37
|
+
|
|
38
|
+
## Required Inputs
|
|
39
|
+
|
|
40
|
+
- **The user's description of the program.** The interview fills in the rest.
|
|
41
|
+
- **An org connection (optional).** Used only for reads, and only after the user agrees.
|
|
42
|
+
|
|
43
|
+
**Defaults (Start Simple).** Recommend these unless the interview shows a clear need for more:
|
|
44
|
+
|
|
45
|
+
- One Volunteer Initiative, with no parent.
|
|
46
|
+
- No Position template; qualifications go on the Job Position unless the role is reused.
|
|
47
|
+
- One Job Position for the tasks the same volunteers do at one place ("greet guests and help set up" is one Job Position). Split it only when the user wants separate sign-up or tracking per task.
|
|
48
|
+
- No shifts unless volunteers sign up for set times.
|
|
49
|
+
|
|
50
|
+
A default is a recommendation. It is decided only when the user picks it.
|
|
51
|
+
|
|
52
|
+
When the user arrives with the decisions already made, still read `references/configuration-decisions.md` and `references/plan-output.md` before writing the plan; they hold the platform behaviors the plan has to state correctly.
|
|
53
|
+
|
|
54
|
+
## Workflow
|
|
55
|
+
|
|
56
|
+
1. **Ask for read consent.** Before the first org read, say what you'll read (the Volunteer Management check and the reuse checks) and why, and get a yes. Warn that declining means existing records can't be checked, so duplicates may be created. If the user declines, or no org is connected, skip steps 2 and 5 and keep planning; the plan is stamped `Reuse-check: DECLINED`. If the user agrees to some reads but not others, the plan is also stamped `DECLINED`. Without the read, a record the user says already exists stays New in the table; its Reason says the user named it and records-create finds and reuses it before writing.
|
|
57
|
+
2. **Check that Volunteer Management is on**, once, after the user agrees to the org read (calls in `references/mcp-invocation.md`):
|
|
58
|
+
1. Read the org preference `VolunteerManagementEnabled`. `true` → on. `false` or 404 → off: stop and tell the user an admin turns it on in Setup → Volunteer Management Settings. 403 → the user has no access to org settings: stop and say so.
|
|
59
|
+
2. Describe `VolunteerInitiative`. 200 → the user can see the Volunteer Management objects. Any error → stop and tell the user they need the Manage Volunteer Data permission set. Don't look up permission-set assignments beforehand.
|
|
60
|
+
3. **Run the discovery interview** (`references/discovery-interview.md`). Ask about these topics in this order, whatever the program's complexity: programs and how they relate, locations, roles, delivery format (onsite, virtual, hybrid), qualifications, scheduling, capacity. Stay on one topic at a time. Note anything the user says early about a later topic, but don't jump ahead. Finish every topic before deciding anything, then summarize what you heard and ask the user to correct it.
|
|
61
|
+
4. **Walk the configuration decisions** (`references/configuration-decisions.md`), one per turn: Locations, parent Volunteer Initiative, Position template, shifts or no shifts, recurrence, and qualifications (including whether a requirement belongs on the role or on the person). For each, say what you heard, give the options, recommend one with the reason, name the platform behavior that matters, and ask. Wait for an explicit answer before the next decision.
|
|
62
|
+
5. **Check for existing records** before planning anything as New. Run each check when its decision comes up, so the user decides with the org in view. The Volunteer Initiative check runs before the parent Volunteer Initiative decision. Check every planned record by its natural key:
|
|
63
|
+
|
|
64
|
+
| Record | Natural key |
|
|
65
|
+
|---|---|
|
|
66
|
+
| Volunteer Initiative, Location, Position, Competency, Examination | Name |
|
|
67
|
+
| Job Position | Name + Volunteer Initiative |
|
|
68
|
+
|
|
69
|
+
Filter each query to the names in the plan, never the whole object. Escape apostrophes in names with a backslash. Locations, Competencies, Examinations and Positions are meant to be shared across programs, so look for them by the distinctive word stems too, and run the query again with a shorter stem when a near-match could be worded differently. When a match turns up, show it and ask whether it's the record the user means. Job Positions always get the check: if the Volunteer Initiative is New, check by name only; a match belongs to another initiative, so the Job Position is still New and records-create checks again before writing.
|
|
70
|
+
6. **Present the plan (message 1).** Read `references/plan-output.md` and use its template: the summary table, then one or two lines each, only where they apply, for what was left out on purpose, the schedule, what these records don't do on their own, and outstanding inputs, then the `Reuse-check:` line. Check that the plan is complete first. Don't restate the table or add other sections. End the message with exactly one question: "Do you want to see the additional configuration and extensions you can set up beyond these records?"
|
|
71
|
+
7. **Offer the additional configuration (message 2).** Send this only after the user answers, and never in the same response as the plan. On yes, show the Additional Configuration Considerations checklist and the Extensions table from `references/plan-output.md`, both in full and both informational, then ask "Do you want to create the records from the plan?" as the last line. On no ("no", "skip that"), the answer covers only the additional configuration, not the records: reply with only "Do you want to create the records from the plan?" and wait. Never treat it as a no to creating the records. If the user asks for changes instead, update the plan and repeat step 6.
|
|
72
|
+
8. **Act on the create answer.** On yes, hand the plan to `nonprofit-volunteer-management-records-create`. On no, or if the user wanted only the plan, stop. If the user asks for changes instead, update the plan and repeat step 6.
|
|
73
|
+
|
|
74
|
+
**Reuse-check line.** Every plan carries exactly one of these:
|
|
75
|
+
|
|
76
|
+
- `Reuse-check: RESOLVED — org read; the Reuse/New mapping is authoritative`
|
|
77
|
+
- `Reuse-check: DECLINED — org not read (or read only in part); every unchecked record planned as New; records-create checks before writing`
|
|
78
|
+
|
|
79
|
+
If the line is missing or unrecognized, records-create runs its own reuse checks.
|
|
80
|
+
|
|
81
|
+
## Rules / Constraints
|
|
82
|
+
|
|
83
|
+
| Constraint | Rationale |
|
|
84
|
+
|---|---|
|
|
85
|
+
| One read consent before any org read; declining doesn't stop planning | The user controls access to their org. Without the read, duplicates are possible, so the plan says so |
|
|
86
|
+
| Ask the discovery topics in order and finish them all before deciding anything | Later answers often change earlier decisions |
|
|
87
|
+
| One configuration decision per turn: recommend, then ask | These are the user's decisions: how many volunteers is too many, which qualifications matter, whether a program needs its own initiative |
|
|
88
|
+
| Never plan a record as New without checking the org first (unless the read was declined) | Orgs collect Volunteer Management records over time; a duplicate splits the data |
|
|
89
|
+
| Filter every reuse query to the planned names; never read a whole object | A reuse check looks for matches. It is not an inventory of the org |
|
|
90
|
+
| Presenting the plan isn't consent; an earlier "go ahead" isn't consent | Only a yes to "Do you want to create the records from the plan?" lets records-create write |
|
|
91
|
+
| Offer the checklist and Extensions after the plan and before the create question; on yes, show them in full, as information only. Don't offer to build them or suggest they'll be done later | The user owns that work. Shown with the plan it buries the question; shown after the create answer it scrolls away under the records step |
|
|
92
|
+
| Every limit that applies to a planned record goes in the plan itself, under "What these records don't do on their own" | A user who skips the additional configuration still needs to know a maximum isn't enforced or an approval flag does nothing on its own |
|
|
93
|
+
| A shift's maximum is counted, never enforced: nothing blocks a sign-up to a full shift until the customer adds a validation rule. Never say the platform stops sign-ups on its own | Saying the cap stops sign-ups leaves the user with overbooked shifts |
|
|
94
|
+
| Volunteer matching uses only Competency qualifications with a required level. A requirement a volunteer must meet before being matched (a background check, a training) is a Competency; an Examination is for manual checks and compliance tracking only | An Examination never affects who is matched |
|
|
95
|
+
| Every Position Qualification or Job Position Qualification has a row for the Competency or Examination it points at, Reuse or New | A qualification can't be created without the record it references |
|
|
96
|
+
| The plan records decisions, not fields. Never write a field name or "TBD" in it | records-create sets field values from its field guide and asks for anything missing |
|
|
97
|
+
| Hand schedule details to records-create; don't plan Operating Hours, Time Slots or individual shifts for an existing Job Position | records-create builds the shift series and checks for shifts that already exist |
|
|
98
|
+
| Don't change existing records, org settings, users or permissions. Point out a misconfigured record, but leave it as it is | This skill only reads |
|
|
99
|
+
|
|
100
|
+
## Output Expectations
|
|
101
|
+
|
|
102
|
+
- **Message 1:** the plan summary table (Record · Name · Reuse or New · Belongs to · Decision); then, each in a line or two and only when it applies, what was left out on purpose, one line per shift series, what these records don't do on their own, and the outstanding inputs; the `Reuse-check:` line; and "Do you want to see the additional configuration and extensions you can set up beyond these records?" as the last line. Nothing in the message repeats the table.
|
|
103
|
+
- **Message 2:** on yes, the Additional Configuration Considerations checklist and the Extensions table in full; either way, "Do you want to create the records from the plan?" as the last line.
|
|
104
|
+
- **Handoff (only on yes):** the plan table, the Id of each reused record, the schedule details, the outstanding inputs and the `Reuse-check:` line, passed to `nonprofit-volunteer-management-records-create`.
|
|
105
|
+
|
|
106
|
+
## Cross-Skill Integration
|
|
107
|
+
|
|
108
|
+
- **`nonprofit-volunteer-management-model-get`**: what an entity, field or feature means. Use it when the user asks "what is…" during planning.
|
|
109
|
+
- **`nonprofit-volunteer-management-records-create`**: writes the confirmed plan. It owns field values, the write order, the existing-shift check, and Operating Hours and Time Slots.
|
|
110
|
+
|
|
111
|
+
## Reference File Index
|
|
112
|
+
|
|
113
|
+
| File | Used in |
|
|
114
|
+
|---|---|
|
|
115
|
+
| `references/mcp-invocation.md` | Steps 2 and 5 — the exact calls for the Volunteer Management check and each reuse check, for headless-360 and the Salesforce CLI |
|
|
116
|
+
| `references/discovery-interview.md` | Step 3 — the discovery questions, probing prompts and summary |
|
|
117
|
+
| `references/configuration-decisions.md` | Step 4 — each configuration decision, its options, default and platform behaviors |
|
|
118
|
+
| `references/plan-output.md` | Steps 6–8 — the plan template, schedule details, platform limits, completeness check, checklist, Extensions table and handoff |
|
|
119
|
+
| `references/edge-cases.md` | Any step — hybrid programs, existing records, contradictions, several programs |
|