@salesforce/afv-skills 1.67.0 → 1.68.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.
Files changed (118) hide show
  1. package/README.md +55 -5
  2. package/package.json +1 -1
  3. package/skills/agentforce-offline-experimentation-run/SKILL.md +14 -22
  4. package/skills/agentforce-offline-experimentation-run/scripts/bt_compare.py +81 -0
  5. package/skills/agentforce-offline-experimentation-run/scripts/bt_fetch.py +358 -0
  6. package/skills/agentforce-offline-experimentation-run/scripts/bt_patch_agent.py +110 -0
  7. package/skills/agentforce-offline-experimentation-run/scripts/btlib/__init__.py +34 -0
  8. package/skills/agentforce-offline-experimentation-run/scripts/btlib/agentpatch.py +483 -0
  9. package/skills/agentforce-offline-experimentation-run/scripts/btlib/agentsrc.py +565 -0
  10. package/skills/agentforce-offline-experimentation-run/scripts/btlib/cache.py +95 -0
  11. package/skills/agentforce-offline-experimentation-run/scripts/btlib/compare.py +617 -0
  12. package/skills/agentforce-offline-experimentation-run/scripts/btlib/fetch.py +1012 -0
  13. package/skills/agentforce-offline-experimentation-run/scripts/btlib/judge.py +148 -0
  14. package/skills/agentforce-offline-experimentation-run/scripts/btlib/messages.py +92 -0
  15. package/skills/agentforce-offline-experimentation-run/scripts/btlib/patch.py +327 -0
  16. package/skills/agentforce-offline-experimentation-run/scripts/btlib/redact.py +40 -0
  17. package/skills/agentforce-offline-experimentation-run/scripts/btlib/results.py +288 -0
  18. package/skills/agentforce-offline-experimentation-run/scripts/btlib/scoring.py +340 -0
  19. package/skills/agentforce-offline-experimentation-run/scripts/btlib/sfap.py +238 -0
  20. package/skills/agentforce-offline-experimentation-run/scripts/btlib/splits.py +136 -0
  21. package/skills/agentforce-offline-experimentation-run/scripts/btlib/stdm.py +98 -0
  22. package/skills/agentforce-offline-experimentation-run/scripts/btlib/tokens.py +173 -0
  23. package/skills/agentforce-offline-experimentation-run/scripts/btlib/units.py +199 -0
  24. package/skills/agentforce-offline-experimentation-run/scripts/btlib/versions.py +194 -0
  25. package/skills/agentforce-offline-experimentation-run/scripts/build_report.py +366 -378
  26. package/skills/agentforce-offline-experimentation-run/scripts/resolve-token.sh +14 -4
  27. package/skills/agentforce-offline-experimentation-run/scripts/sfap_replay.py +472 -796
  28. package/skills/dx-lifecycle-center/SKILL.md +5 -2
  29. package/skills/dx-lifecycle-center/assets/widgets/connect-connected/connect-connected.json +65 -0
  30. package/skills/dx-lifecycle-center/assets/widgets/connect-connected/schema.json +32 -0
  31. package/skills/dx-lifecycle-center/assets/widgets/connect-sign-in/connect-sign-in.json +57 -0
  32. package/skills/dx-lifecycle-center/assets/widgets/connect-sign-in/schema.json +21 -0
  33. package/skills/dx-lifecycle-center/examples/workflows.md +20 -6
  34. package/skills/dx-lifecycle-center/references/connect-org.md +21 -2
  35. package/skills/dx-lifecycle-center/references/deploy.md +2 -2
  36. package/skills/dx-lifecycle-center/references/list-orgs.md +1 -1
  37. package/skills/dx-lifecycle-center/scripts/card-policy.jq +71 -0
  38. package/skills/dx-lifecycle-center/scripts/fill-widget.jq +29 -0
  39. package/skills/dx-lifecycle-center/scripts/widget.sh +28 -0
  40. package/skills/dx-org-devhub-configure/SKILL.md +85 -84
  41. package/skills/dx-org-devhub-configure/examples/status-output.md +25 -2
  42. package/skills/dx-org-devhub-configure/scripts/devhub.sh +265 -140
  43. package/skills/dx-org-devhub-configure/scripts/test-devhub.sh +100 -15
  44. package/skills/dx-packaging-move-to-2gp-migrate/SKILL.md +89 -0
  45. package/skills/dx-packaging-move-to-2gp-migrate/references/api-operations.md +164 -0
  46. package/skills/dx-scratch-def-configure/SKILL.md +163 -0
  47. package/skills/dx-scratch-def-configure/examples/confirm-card.md +53 -0
  48. package/skills/dx-scratch-def-configure/examples/report-recipes.md +100 -0
  49. package/skills/dx-scratch-def-configure/references/features.json +3270 -0
  50. package/skills/dx-scratch-def-configure/references/project-scratch-def.schema.json +1841 -0
  51. package/skills/dx-scratch-def-configure/references/settings-map.json +682 -0
  52. package/skills/dx-scratch-def-configure/scripts/lib_scratch_def.py +1306 -0
  53. package/skills/dx-scratch-def-configure/scripts/requirements.txt +3 -0
  54. package/skills/dx-scratch-def-configure/scripts/scrape_config_values.py +848 -0
  55. package/skills/education-cloud-domain-configure/SKILL.md +1 -1
  56. package/skills/education-cloud-multi-campus-configure/SKILL.md +1 -1
  57. package/skills/education-cloud-student-recruitment-agent-configure/SKILL.md +1 -1
  58. package/skills/experience-design-validate/SKILL.md +1 -1
  59. package/skills/experience-search-coordinate/SKILL.md +18 -12
  60. package/skills/experience-search-coordinate/references/content-route.md +79 -1
  61. package/skills/field-service-data-capture-migrate/SKILL.md +1 -1
  62. package/skills/platform-apex-generate/SKILL.md +14 -11
  63. package/skills/platform-apex-test-generate/SKILL.md +2 -1
  64. package/skills/platform-datamask-run/SKILL.md +52 -71
  65. package/skills/platform-datamask-run/references/api-surface.md +10 -4
  66. package/skills/platform-datamask-run/references/policy-authoring.md +56 -7
  67. package/skills/platform-datamask-run/references/run-and-abort.md +17 -10
  68. package/skills/platform-datamask-run/scripts/poll-job.sh +15 -25
  69. package/skills/platform-dataspace-access-configure/SKILL.md +19 -1
  70. package/skills/platform-metadata-discovery-get/SKILL.md +185 -0
  71. package/skills/platform-metadata-discovery-get/references/api-map.md +25 -0
  72. package/skills/platform-metadata-discovery-get/references/h360-routes.md +30 -0
  73. package/skills/platform-metadata-discovery-get/references/scoped-soql.md +52 -0
  74. package/skills/platform-metadata-grounding-get/SKILL.md +170 -0
  75. package/skills/platform-policy-rule-generate/SKILL.md +84 -232
  76. package/skills/platform-policy-rule-generate/references/condition-model.md +47 -0
  77. package/skills/platform-policy-rule-generate/references/condition-templates.md +89 -0
  78. package/skills/platform-policy-rule-generate/references/conjunction-compiler.md +25 -0
  79. package/skills/platform-policy-rule-generate/references/conjunction-expression.md +22 -0
  80. package/skills/platform-policy-rule-generate/references/consumer-all.md +9 -0
  81. package/skills/platform-policy-rule-generate/references/consumer-datacloud-ui.md +91 -0
  82. package/skills/platform-policy-rule-generate/references/deploy-errors.md +24 -36
  83. package/skills/platform-policy-rule-generate/references/examples/ExistsJoinRLS/package.xml +12 -0
  84. package/skills/platform-policy-rule-generate/references/examples/ExistsJoinRLS/policyRuleDefinitionSets/ExistsJoinRLS.policyRuleDefinitionSet +6 -0
  85. package/skills/platform-policy-rule-generate/references/examples/ExistsJoinRLS/policyRuleDefinitions/ExistsJoinRLS_Rule_1.policyRuleDefinition +91 -0
  86. package/skills/platform-policy-rule-generate/references/examples/FieldReadDeny/package.xml +12 -0
  87. package/skills/platform-policy-rule-generate/references/examples/FieldReadDeny/policyRuleDefinitionSets/FieldReadDeny.policyRuleDefinitionSet +6 -0
  88. package/skills/platform-policy-rule-generate/references/examples/FieldReadDeny/policyRuleDefinitions/FieldReadDeny_Rule_1.policyRuleDefinition +72 -0
  89. package/skills/platform-policy-rule-generate/references/examples/LastCharactersFieldMasking/package.xml +12 -0
  90. package/skills/platform-policy-rule-generate/references/examples/LastCharactersFieldMasking/policyRuleDefinitionSets/LastCharactersFieldMasking.policyRuleDefinitionSet +6 -0
  91. package/skills/platform-policy-rule-generate/references/examples/LastCharactersFieldMasking/policyRuleDefinitions/LastCharactersFieldMasking_Rule_1.policyRuleDefinition +77 -0
  92. package/skills/platform-policy-rule-generate/references/examples/PrincipalFieldMatchRLS/package.xml +12 -0
  93. package/skills/platform-policy-rule-generate/references/examples/PrincipalFieldMatchRLS/policyRuleDefinitionSets/PrincipalFieldMatchRLS.policyRuleDefinitionSet +6 -0
  94. package/skills/platform-policy-rule-generate/references/examples/PrincipalFieldMatchRLS/policyRuleDefinitions/PrincipalFieldMatchRLS_Rule_1.policyRuleDefinition +77 -0
  95. package/skills/platform-policy-rule-generate/references/examples/TaggedObjectAccess/package.xml +12 -0
  96. package/skills/platform-policy-rule-generate/references/examples/TaggedObjectAccess/policyRuleDefinitionSets/TaggedObjectAccess.policyRuleDefinitionSet +4 -0
  97. package/skills/platform-policy-rule-generate/references/examples/TaggedObjectAccess/policyRuleDefinitions/TaggedObjectAccess_Rule_1.policyRuleDefinition +65 -0
  98. package/skills/platform-policy-rule-generate/references/headless-transport.md +87 -0
  99. package/skills/platform-policy-rule-generate/references/policy-schema-full.md +17 -10
  100. package/skills/platform-policy-rule-generate/references/policy-shapes.md +74 -0
  101. package/skills/platform-policy-rule-generate/references/principal-expression.md +31 -0
  102. package/skills/platform-policy-rule-generate/references/reference-resolution.md +51 -0
  103. package/skills/platform-policy-rule-generate/references/resource-expression.md +48 -0
  104. package/skills/platform-policy-rule-generate/references/runtime-context.md +9 -0
  105. package/skills/platform-policy-rule-generate/references/templates-access.md +11 -9
  106. package/skills/platform-policy-rule-generate/references/templates-advanced.md +4 -2
  107. package/skills/platform-policy-rule-generate/references/templates-record.md +37 -22
  108. package/skills/platform-policy-rule-generate/references/templates-transform.md +10 -8
  109. package/skills/platform-policy-rule-generate/references/templates.md +2 -2
  110. package/skills/platform-policy-rule-generate/references/ui-compatibility.md +35 -17
  111. package/skills/platform-report-generate/SKILL.md +9 -4
  112. package/skills/platform-report-generate/references/chart-types.md +26 -2
  113. package/skills/platform-report-generate/references/column-names.md +43 -0
  114. package/skills/platform-report-generate/references/errors-and-troubleshooting.md +1 -1
  115. package/skills/platform-report-generate/references/verification-checklist.md +10 -2
  116. package/skills/service-digital-engagement-channel-configure/SKILL.md +7 -5
  117. package/skills/service-digital-engagement-channel-configure/assets/messaging_channel_template.xml +33 -0
  118. package/skills/service-digital-engagement-channel-configure/references/channel_settings.md +110 -14
@@ -17,7 +17,7 @@ metadata:
17
17
  - "platform-metadata-deploy"
18
18
  - "platform-permission-set-generate"
19
19
  - "platform-validation-rule-generate"
20
- accessCheck: "License.EducationCloud"
20
+ accessCheck: "OrgPermissions.EducationCloud"
21
21
  ---
22
22
 
23
23
  # Configuring Education Cloud Domains
@@ -8,7 +8,7 @@ metadata:
8
8
  - "Education"
9
9
  relatedSkills:
10
10
  - "education-cloud-domain-configure"
11
- accessCheck: "License.EducationCloud"
11
+ accessCheck: "OrgPermissions.EducationCloud"
12
12
  mcpTools:
13
13
  headless-360:
14
14
  tools: ["describe", "discover", "dispatch", "dispatch_readonly"]
@@ -18,7 +18,7 @@ metadata:
18
18
  cliTools:
19
19
  - tool: ["sf"]
20
20
  semver: ">=2.0.0"
21
- accessCheck: "(OrgPermissions.EinsteinForEducationCloud) && (License.Agentforce)"
21
+ accessCheck: "(OrgPermissions.EinsteinForEducationCloud) && (OrgPermissions.EinsteinAgentPlatform)"
22
22
  ---
23
23
 
24
24
  # Configuring the Education Cloud Student Recruitment Agent
@@ -3,7 +3,7 @@ name: experience-design-validate
3
3
  description: "Use this skill to run a visual-craft audit of a rendered UI and produce an evidence-grounded Craft Report across Useful, Usable, Reliable, Coherent, and Well-Crafted. Invoke it for craft audits, visual critiques, design comparisons, felt-quality reviews, or visual-craft readiness checks on screenshots (.png, .jpg, .jpeg, .webp, .gif), Figma frames, rendered prototypes, and live URLs. Do not use it for source-code review, SLDS compliance (use design-systems-slds-validate), or accessibility compliance (use experience-accessibility-validate)."
4
4
  metadata:
5
5
  version: "1.0"
6
- domains: ["Experience"]
6
+ domains: ["Experience", "Design Systems"]
7
7
  relatedSkills:
8
8
  - "design-systems-slds-validate"
9
9
  - "experience-accessibility-validate"
@@ -14,6 +14,7 @@ metadata:
14
14
  tools:
15
15
  - get_content_types_for_workspace
16
16
  - search_content_cms_channels
17
+ - search_digital_content
17
18
  - search_electronic_media
18
19
  - search_media_cms_channels
19
20
  semver: ">=1.0.0"
@@ -113,13 +114,18 @@ Routes search requests to the correct sub-workflow — structured CMS content or
113
114
 
114
115
  Follow this sequence for each intent Step 2 routed here. The content type FQN(s) are already known from Step 2.
115
116
 
116
- 1. **Build the search query:**
117
- - **Extract keywords** — concrete nouns and named subjects that would appear in the content's title or body. Use domain-specific synonyms, max 10 terms, joined with ` OR `. E.g. "blog posts about sustainable business practices" → `sustainable OR sustainability OR business OR environmental`.
118
- - **Extract taxonomies** — descriptive qualities, categories, or topical tags only (adjectives, categories, attributes — not concrete nouns). Stringify as `{"OR": ["term1", "term2"]}`, or `"{}"` if none apply. E.g. "sustainable business practices and environmental responsibility" → `{"OR": ["Business", "Corporate", "Environment", "Responsibility"]}`; "FAQs about password resets" → `"{}"` (no descriptive/categorical terms).
119
- - Load `assets/search-payload-template.json` for the request payload structure. Set `contentTypeFqn` to the comma-separated FQN list collected in Step 2. `contentAccessScope` and `channelIds` are **mutually exclusive** — never send both in the same call — so they're set per-call in point 3 below, not here.
120
- 2. **Confirm with the user** — required two-message pattern (labeled-parameters chat message, then `ask_followup_question` with `Yes - search now` / `Edit search` / `Cancel`). Never put the labeled list inside `ask_followup_question`'s text — it strips formatting. → Read `references/content-route.md` § "Confirming the query before searching" for templates.
121
- 3. **Execute the search call(s)** per the scope from Step 1 — one call for public-only scope, two (public + channel-scoped) when a `channelId` is in scope. → Read `references/content-route.md` § "Executing the search call(s)" before parsing results — array/merge rules.
122
- 4. **Present up to 5 unique results total** (de-duplicated by `managedContentKey` for display only — an item found under more than one channel still appears once here), grouped by content type, with a `Load More` option if more remain. Each item shows its **Channel** as a per-item field — never the grouping key:
117
+ 1. **Present search sources via `ask_followup_question` before any tool call** — only sources whose tools you have, plus "Other"; wait for the choice. → Read `references/content-route.md` § "Presenting available search sources".
118
+ 2. **Build the query, then confirm before searching:**
119
+ - **Search using keywords** (`search_content_cms_channels`):
120
+ - **Extract keywords** — concrete nouns and named subjects likely in the title or body, plus domain synonyms; max 10, joined with ` OR `. E.g. "blog posts about sustainable business practices" → `sustainable OR sustainability OR business OR environmental`.
121
+ - **Extract taxonomies** — descriptive qualities, categories, or topical tags only (not concrete nouns), stringified as `{"OR": ["term1", "term2"]}`, or `"{}"` if none.
122
+ - Payload: `assets/search-payload-template.json`, `contentTypeFqn` = the comma-separated FQN list from Step 2. `contentAccessScope` and `channelIds` are **mutually exclusive** — never both in one call — so they're set per call in point 3.
123
+ - **Search using Data 360 hybrid search** (`search_digital_content`): verbatim `searchQuery`; bare ISO `searchLanguage` only if the schema lists it. On zero results or a language error, ask the user before one retry without it → `references/content-route.md`.
124
+ - **Other** — ask for a direct URL, asset library path, or system to check (no tool call, no confirmation).
125
+
126
+ Confirm via the two-message pattern (`Yes - search now` / `Edit search` / `Cancel`). → Read `references/content-route.md` § "Confirming the query before searching".
127
+ 3. **Execute the search call(s)** — keyword search: one call for public-only scope, two (public + channel-scoped) when a `channelId` is in scope. Hybrid: one call, no channel scope. → Read `references/content-route.md` § "Executing the search call(s)" (keyword) or the hybrid section (field mapping) before parsing results.
128
+ 4. **Present up to 5 unique results total** (de-duplicated by `managedContentKey` for display only — an item found under more than one channel still appears once here), grouped by content type, with a `Load More` option if more remain (keyword search only). Each item shows its **Channel** as a per-item field — never the grouping key:
123
129
 
124
130
  ```text
125
131
  I found 3 content items:
@@ -162,7 +168,7 @@ Follow this sequence for each intent Step 2 routed here. The media content type
162
168
  When a request mixes media and content intents, run the two routes **sequentially, each to full completion, not interleaved and not merged into one combined presentation**:
163
169
 
164
170
  1. **Media Route first** — for every media intent, run Step 4 in full: present sources, build/confirm the query, search, present results, let the user select, and (point 6) offer the `experience-cms-content-render` hand-off (Yes/No). Finish this entire route — including the offer — before starting the Content Route.
165
- 2. **Content Route second** — for every content intent, run Step 3 in full: build/confirm the query, search, present results grouped by content type (channel shown per-item), let the user select, and (point 6) offer the `experience-cms-content-render` hand-off if a uiBundle channel is in scope.
171
+ 2. **Content Route second** — for every content intent, run Step 3 in full: present sources, build/confirm the query, search, present results grouped by content type (channel shown per-item), let the user select, and (point 6) offer the `experience-cms-content-render` hand-off if a uiBundle channel is in scope.
166
172
 
167
173
  Each route's results are labeled and presented on their own (e.g. "Images:" for the Media Route's output, then "Articles:" for the Content Route's) — never merge the two into one undifferentiated list, and never hold Media Route's results back to present alongside Content Route's.
168
174
 
@@ -170,12 +176,12 @@ Each route's results are labeled and presented on their own (e.g. "Images:" for
170
176
 
171
177
  | Error | Response |
172
178
  |---|---|
173
- | No `search_content_cms_channels`/`search_media_cms_channels` tools found via introspection | "No content search connection is configured. Please connect a content-readonly MCP server." |
179
+ | No content or media search tools (keyword or hybrid) found via introspection | "No content search connection is configured. Please connect a content-readonly MCP server." |
174
180
  | Neither media tool available | "No automated media search sources are currently configured. Please provide a direct URL or asset library path." |
175
181
  | `metadata-grounding` unavailable or no content types match | Apply the fallback chain in `references/content-type-discovery.md` — do not block or ask for an FQN |
176
182
  | No search results (either route) | "No [content/media] found. Try broader keywords, removing descriptive terms, or a different [content type/source]." |
177
183
  | Invalid user selection | Re-display options and ask again |
178
- | Tool returns error (invalid input, missing argument, or otherwise) | Show error message, offer retry with corrected parameters **on that same tool — never fall through to the other route's search tool as a workaround** (see Key Principle 3); hybrid language errors: offer one no-language retry, with the user's OK (see media route) |
184
+ | Tool returns error (invalid input, missing argument, or otherwise) | Show error message, offer retry with corrected parameters **on that same tool — never fall through to the other route's search tool as a workaround** (see Key Principle 3); hybrid language errors: offer one no-language retry, with the user's OK (see each route's reference) |
179
185
  | Search tool on the connected server returns a server error (5xx, timeout, connection failure — not "no results") | **Fail the call — do NOT silently retry against a different server.** Tell the user to check that server's connection/session and wait before retrying. |
180
186
 
181
187
  **Never silently fail on a route the user asked for.** If one route in a mixed request comes up empty, still present the other route's results and say so explicitly. This does not mean substituting a different server when the connected one errors — see the server-error row above.
@@ -186,7 +192,7 @@ Each route's results are labeled and presented on their own (e.g. "Images:" for
186
192
  2. **Search first, route second** — `search_metadata` + `query_metadata` decide the route; fall back to `get_content_types_for_workspace`, then intent wording, only when that comes up empty (Step 2)
187
193
  3. **Never cross-route, including on failure** — content requests never hit media search tools and vice versa; if a route's search tool errors for any reason, fix and retry that same tool or stop and report the error — never fall through to the other route's tool as a workaround
188
194
  4. **Metadata grounding is mandatory for content** — discover content type FQNs dynamically, never hardcode them
189
- 5. **Media route starts text-only** — present sources and wait before any tool call
195
+ 5. **Both routes start text-only** — present sources and wait before any tool call
190
196
  6. **Confirm before searching (both routes)** — user approves query parameters before execution
191
197
  7. **Never auto-select** — always wait for user choice, across channel scope and both routes
192
198
  8. **Run mixed requests sequentially, Media Route first** — complete it fully (including the render offer) before starting the Content Route; present each route's results as its own labeled set, never merged
@@ -195,4 +201,4 @@ Each route's results are labeled and presented on their own (e.g. "Images:" for
195
201
  11. **Never fail over to a different server on a server error** — fail the call and tell the user to check that server's session; don't silently retry elsewhere
196
202
  12. **Group by content type, never by channel** — in both routes, channel is always a per-item field, never the group heading
197
203
  13. **Disambiguate channel at selection, not before** — an item is shown once at presentation regardless of its delivery-details array length; only after selection do you ask which channel's copy to use, one option per entry by `managedContentChannelDetails.name`, in both routes
198
- 14. **Unauthenticated URL is public-channel-only** — `PUBLIC_UNAUTHENTICATED`/`COMMUNITY` entries only; never a `WEB_APP` (uiBundle) entry's `contentUrl`, in either route
204
+ 14. **Unauthenticated URL is public-channel-only** — `PUBLIC_UNAUTHENTICATED`/`COMMUNITY` entries only; never a `WEB_APP` (uiBundle) entry's `contentUrl`, in either route. A hybrid entry on a Step 1 uiBundle `channelId` counts as `WEB_APP`
@@ -1,5 +1,22 @@
1
1
  # Content Route — Full Reference
2
2
 
3
+ ## Presenting available search sources
4
+
5
+ Before any tool call, dispatch `ask_followup_question` offering only the sources whose tools you actually have (via introspection), plus "Other". Each is its own option — never numbered lines inside the question text:
6
+
7
+ ```text
8
+ question: "How would you like to search for content?"
9
+ options:
10
+ - "Search using keywords — Search Salesforce CMS by keywords and taxonomies"
11
+ - "Search using Data 360 hybrid search — Semantic (vector + keyword) search across Salesforce CMS and connected DAMs"
12
+ - "Other — Provide your own URL, asset library path, or system to check"
13
+ ```
14
+
15
+ Wait for the user's selection. Do not call any tool before this point. Then follow the matching branch:
16
+ - **Search using keywords** → `search_content_cms_channels` (build keywords + taxonomies, confirm, execute per scope — see below).
17
+ - **Search using Data 360 hybrid search** → `search_digital_content` (verbatim query + bare ISO `searchLanguage` when the schema lists it — see "Search using Data 360 hybrid search" below).
18
+ - **Other** → ask for a direct URL, asset library path, or specific system to check. No confirmation step, since no tool is called.
19
+
3
20
  ## Confirming the query before searching
4
21
 
5
22
  Required two-message pattern, same turn:
@@ -69,6 +86,67 @@ Even though `contentUrl` is present, this item's only entry is `WEB_APP` — the
69
86
 
70
87
  The multi-channel case is not "one entry from the public call, one from the channel-scoped call" — it's whatever the array actually contains, however many entries that is.
71
88
 
89
+ ## Search using Data 360 hybrid search (`search_digital_content`)
90
+
91
+ Chosen when the user picked "Search using Data 360 hybrid search" at the source step. Data 360 hybrid search (vector + keyword) runs across Salesforce CMS and any connected third-party CMS/DAM.
92
+
93
+ 1. Pass `searchQuery` **verbatim** — same language, same script as the user wrote it. No translation, no keyword extraction, no ` OR `-joining, no `taxonomyExpression`, no `contentTypeFqn`/`contentAccessScope`/`channelIds`, no pagination. Translating first only makes step 2 harder, since you'd be detecting the language of your own translation instead of the user's actual words.
94
+ 2. **Determine language** — a short ISO code read directly off the user's literal wording, before any translation happens in your reasoning. Bare 2-letter codes (`en`, `fr`, `de`, `es`, `ja`, ...) cover most languages and are the validated, simpler form; a few regional splits need a country suffix instead (`pt_BR`, `zh_CN`, `zh_TW`, ...). Default to `en` only when the message is genuinely ambiguous (e.g. a bare product name) — never because translating felt natural. Use the bare 2-letter form whenever one exists — the backend accepts only the platform's own language codes, so `fr` works but `fr_FR` is rejected as invalid (`en` and `en_US` both work because `en_US` is itself a platform code).
95
+ 3. **Data 360 hybrid search ignores channel scope entirely** — there is no public vs. channel-scoped split. Make a **single call** regardless of whether a `channelId` is in scope from Step 1: `search_digital_content(searchQuery="<verbatim query>", searchLanguage="<language code>")`. **Only send `searchLanguage` if the tool's input schema actually lists it** — orgs without language-variant search enabled don't declare the input at all. When it's absent, call with `searchQuery` alone, skip the language line in the confirmation message, and skip steps 4–5 (the backend searches primary-language content only).
96
+ 4. **Offer one retry with `searchLanguage` omitted entirely — ask the user first, never retry silently — but only when the first call either:**
97
+ - returns **zero results**, **or**
98
+ - fails with an **input-validation error about the language value itself** — an unrecognized language code errors the whole call (for example, `INVALID_API_INPUT` / "Specify a valid language code, such as en_US, fr, or de.") rather than returning an empty list.
99
+
100
+ **Do NOT offer this retry on a server or transport error (5xx, timeout, connection failure).** Those must fail per `SKILL.md`'s Error Handling table — omitting the language cannot fix them and would mask the real failure. The retry exists only for the two language-specific cases above.
101
+
102
+ Offer it before concluding nothing exists and before suggesting broader keywords: tell the user what happened in plain chat text (e.g. "No results found in French."), then `ask_followup_question` with options `Yes - search without language filter` / `No - stop here`. The retry is a second backend call the user didn't confirm, so they decide whether it runs. Skip the question only if the user already said, in this conversation, to fall back to any language when nothing matches theirs — then retry directly. This is the same policy as the Media Route's hybrid search (`media-route.md`). Omitting the field makes the backend fall back to each item's own primary/default language variant instead of the requested one — so an item that only has, say, a French variant still surfaces on the retry even when the user asked in English. Only fall through to "no results" / broadening suggestions if the user declines, or the retry also comes back empty.
103
+ 5. **Read each result item's `language` field to know which language variant you actually got** (a locale string such as `en_US` or `fr`). If it differs from the language the user asked for — typically after the retry — say so explicitly when presenting that item, e.g. "No French version exists for this item — here's the original in English." Never present a fallback-language result as if it were the requested-language match; the user needs to know a substitution happened.
104
+
105
+ If `language` is **absent** on the results of a call that sent `searchLanguage`, the backend couldn't apply the language filter (for example, the org's search index predates language-variant support) and silently returned primary-language content instead — even on the first call, with no error and no empty list. Don't claim the results are in the requested language; tell the user the search couldn't be restricted to that language.
106
+
107
+ This is a **strict** filter, not soft ranking like the keyword route: the backend only returns items carrying that exact language variant, and it does **not** fall back to English (or any other language) per item on its own — that's why step 4's retry exists as an explicit, user-approved second call, not something the backend does silently.
108
+
109
+ Example — French input "trouve-moi un article sur les pinceaux depuis le CMS" → `search_digital_content(searchQuery="trouve-moi un article sur les pinceaux depuis le CMS", searchLanguage="fr")`.
110
+
111
+ Example of the retry — English input "give me a blog post about a snowboard from the CMS" → first call `search_digital_content(searchQuery="give me a blog post about a snowboard from the CMS", searchLanguage="en")` returns zero results (the only snowboard article in the org has a French variant, not English) → tell the user "No results found in English." and ask whether to search without the language filter → user picks `Yes - search without language filter` → retry `search_digital_content(searchQuery="give me a blog post about a snowboard from the CMS")` with `searchLanguage` omitted → the item surfaces with `language: "fr"` → tell the user: "No English version exists for this item — here's the original in French."
112
+
113
+ Still confirm first via the two-message pattern — Message 1 surfaces the detected language so the user can catch a mis-detection before the search runs (language detection is the most error-prone step of this route):
114
+
115
+ ```text
116
+ I'll search Data 360 hybrid search with:
117
+
118
+ Query: trouve-moi un article sur les pinceaux depuis le CMS
119
+ Language: fr (auto-detected — tell me if this is wrong)
120
+ ```
121
+
122
+ Show `Query` exactly as the user wrote it (verbatim) and `Language` as the bare code you'll send. This route has no `Content Type` / `Keywords` / `Taxonomies` / `Channels` lines — it ignores channel scope and does its own semantic matching. Then `ask_followup_question` with `Yes - search now` / `Edit search` / `Cancel`, exactly as for keyword search.
123
+
124
+ ### Parsing the `search_digital_content` response
125
+
126
+ The response shape differs from `search_content_cms_channels`. Map each result to the same content-route fields the rest of this reference uses, so "Disambiguating channel at selection", display, and the render hand-off all work unchanged:
127
+
128
+ | `search_digital_content` field | Content-route equivalent |
129
+ |---|---|
130
+ | `contentName` | `title` |
131
+ | `contentId` | `managedContentId` |
132
+ | `contentKey` | `managedContentKey` |
133
+ | `contentTypeFqn` | `contentType` |
134
+ | `contentUrls[]` → each `{ channelName, channelId, url }` | `managedContentChannelDeliveryDetails[]` → each `{ contentUrl: url, managedContentChannelDetails: { id: channelId, name: channelName } }` |
135
+ | `language` | `language` — present when language-variant search applied; use it per step 5 above |
136
+ | `searchScore` | _(extra — hybrid-only relevance score; optional to surface)_ |
137
+
138
+ **Omissions to account for:** if `language` is absent, omit the Language line for that item (and, if the call sent `searchLanguage`, tell the user the search couldn't be restricted to the requested language — see step 5).
139
+
140
+ **Classifying channel type.** The hybrid response carries **no channel `type`** on its delivery entries, and because hybrid search ignores channel scope it can return an item delivered to a uiBundle channel. Fill in `managedContentChannelDetails.type` yourself so the public-channel-only URL rule in "Executing the search call(s)" above still holds:
141
+ - An entry whose `channelId` matches any uiBundle `channelId` found in Step 1 (any `uiBundles/*/public/content-metadata.json` channel, or the named uiBundle's resolved channel) → `WEB_APP`. Its `url` is **never** surfaced as the Unauthenticated URL, and the render hand-off passes `contentKey` instead of a URL for it. Compare on the first 15 characters — Salesforce IDs appear in both 15- and 18-character forms.
142
+ - Every other entry → treat as a public channel (`PUBLIC_UNAUTHENTICATED`), so its `url` is the Unauthenticated URL.
143
+
144
+ A `WEB_APP`-classified entry is still a selectable channel option at disambiguation, exactly as in the keyword case — it just never produces a displayed URL.
145
+
146
+ **Filter to the Step 2 content types.** The hybrid call takes no `contentTypeFqn`, so results can include types outside what Step 2 discovered. Keep only results whose `contentTypeFqn` is one of Step 2's FQN(s), so both sources answer the same question. If that filter removes every result from a non-empty response, don't report "no results" — tell the user which other content types came back and ask whether to show them.
147
+
148
+ After mapping, the `managedContentChannelDeliveryDetails` array behaves identically to the keyword case (zero / one / many entries), so everything downstream — channel disambiguation, display, and the render hand-off — is reused as-is.
149
+
72
150
  ## Disambiguating channel at selection
73
151
 
74
152
  Reached at Step 3 point 5, only for a selected item whose (merged, per "Executing the search call(s)" above) `managedContentChannelDeliveryDetails` array has more than one entry. Build one option per entry, using that entry's `managedContentChannelDetails.name`, in the order the array returns them — not a fixed two-option choice. **Call `ask_followup_question` with these as its actual `question` and `options` parameters — never concatenate the channel list into the question string itself**, the same rule every other question in this skill follows:
@@ -111,7 +189,7 @@ I can render the following in the <uiBundle> app:
111
189
 
112
190
  ## Building the search query
113
191
 
114
- Keyword/taxonomy extraction rule: see `SKILL.md` Step 3 point 1 (the Media Route uses the same split — see `media-route.md` for media-specific examples). Additional worked examples for content:
192
+ Keyword/taxonomy extraction rule: see `SKILL.md` Step 3 point 2 (the Media Route uses the same split — see `media-route.md` for media-specific examples). Additional worked examples for content:
115
193
  - "press release about our product launch" → keywords: launch, announcement, release, product launch; taxonomies: Launch, Announcement, News
116
194
  - "FAQs about password resets" → keywords: password, reset, login, account; taxonomies: _(empty — no descriptive/categorical terms)_
117
195
 
@@ -18,7 +18,7 @@ metadata:
18
18
  slack:
19
19
  tools: ["slack_create_canvas", "slack_send_message"]
20
20
  semver: ">=1.0.0"
21
- accessCheck: "License.FieldService"
21
+ accessCheck: "OrgPermissions.FieldService"
22
22
  ---
23
23
 
24
24
  # Migrate Field Service Mobile Flows to Data Capture
@@ -7,6 +7,8 @@ metadata:
7
7
  minApiVersion: "66.0"
8
8
  relatedSkills:
9
9
  - "platform-apex-test-generate"
10
+ - "dx-code-analyzer-run"
11
+ - "integration-apex-symbol-table-query"
10
12
  cliTools:
11
13
  - tool: ["sf"]
12
14
  semver: ">=2.0.0"
@@ -47,6 +49,7 @@ All steps are sequential. Do not skip, merge, or reorder. If blocked, stop and a
47
49
  - Service-Selector-Domain layering, logging utilities
48
50
  - Existing classes/triggers and current trigger framework or handler pattern
49
51
  - Whether Trigger Actions Framework (TAF) is already in use
52
+ - Signatures of classes and built-in types you will call or extend: use `integration-apex-symbol-table-query` (API v67.0+); if unavailable, read the source
50
53
 
51
54
  2. **Choose the smallest correct pattern** (see Type-Specific Guidance below)
52
55
 
@@ -57,22 +60,22 @@ All steps are sequential. Do not skip, merge, or reorder. If blocked, stop and a
57
60
 
58
61
  4. **Author with guardrails** -- apply every rule in the Rules section below
59
62
  - Generate `{ClassName}.cls` with ApexDoc
60
- - Generate `{ClassName}.cls-meta.xml`
63
+ - Generate `{ClassName}.cls-meta.xml`
61
64
 
62
- 5. **Generate test classes** -- Load the skill `platform-apex-test-generate` to create `{ClassName}Test.cls` and `{ClassName}Test.cls-meta.xml`. Apex tests are always required to be generated to deploy. No test file creation or edits can occur without loading the `platform-apex-test-generate` skill to generate tests.
65
+ 5. **Generate test classes** -- Load the skill `platform-apex-test-generate` to create `{ClassName}Test.cls` and `{ClassName}Test.cls-meta.xml`. Apex tests are required to deploy; never create or edit test files without loading it.
63
66
 
64
67
  ### Phase 2 — Validate (required before reporting)
65
68
 
66
- Writing files is the midpoint, not the finish line. Steps 6 and 7 each require a tool invocation and produce output that must appear in the Step 8 report. Do not summarize or present the report until both steps have run and their output is captured.
69
+ Steps 6 and 7 each require a tool invocation and produce output that must appear in the Step 8 report. Do not present the report until both have run and their output is captured.
67
70
 
68
71
  6. **Run code analyzer**
69
- - Invoke MCP `run_code_analyzer` on all generated/updated `.cls` files.
72
+ - Use the `dx-code-analyzer-run` skill on all generated/updated `.cls` files.
70
73
  - Remediate all `sev0`, `sev1`, and `sev2` violations; re-run until clean.
71
- - Capture the final tool output verbatim for the report.
72
- - Fallback: `sf code-analyzer run --target <target>`. If both are unavailable, record `run_code_analyzer=unavailable: <error>` in the report.
74
+ - Keep the final tool output verbatim for the report.
75
+ - Fallback: `sf code-analyzer run --target <target>`. If both are unavailable, record `code_analyzer=unavailable: <error>` in the report.
73
76
 
74
77
  7. **Execute Apex tests**
75
- - Run org tests including `{ClassName}Test` via `sf apex run test` or MCP.
78
+ - Run org tests including `{ClassName}Test` via `sf apex run test`.
76
79
  - Delegate all test generation/fixes/coverage work to `platform-apex-test-generate`; iterate until the tests pass.
77
80
  - Capture pass/fail counts and coverage percentage for the report.
78
81
  - If unavailable, record `test_execution=unavailable: <error>` in the report.
@@ -80,9 +83,9 @@ Writing files is the midpoint, not the finish line. Steps 6 and 7 each require a
80
83
  ### Phase 3 — Report
81
84
 
82
85
  8. **Report** -- use the output format at the bottom of this file.
83
- - The `Analyzer` line must contain the actual Step 6 tool output (or `run_code_analyzer=unavailable: <reason>` after attempting invocation).
84
- - The `Testing` line must contain the actual Step 7 results (or `test_execution=unavailable: <reason>` after attempting invocation).
85
- - A report missing either line is incomplete. Always attempt the tool invocation before recording unavailable.
86
+ - The `Analyzer` line must contain the actual Step 6 tool output (or `code_analyzer=unavailable: <reason>` after attempting it).
87
+ - The `Testing` line must contain the actual Step 7 results (or `test_execution=unavailable: <reason>` after attempting it).
88
+ - A report missing either line is incomplete; always attempt the invocation before recording unavailable.
86
89
 
87
90
  ---
88
91
 
@@ -392,7 +395,7 @@ Files: <paths>
392
395
  Design: <pattern / framework choices>
393
396
  Workflow: all steps completed (1-8); any N/A justified
394
397
  Risks: <security, bulkification, async, dependency notes>
395
- Analyzer: <REQUIRED -- paste actual run_code_analyzer output or state "run_code_analyzer=unavailable: <reason>">
398
+ Analyzer: <REQUIRED -- paste actual code analyzer output or state "code_analyzer=unavailable: <reason>">
396
399
  Testing: <REQUIRED -- paste actual test execution results (pass/fail, coverage) or state "test_execution=unavailable: <reason>">
397
400
  Deploy: <dry-run or next step>
398
401
  ```
@@ -7,6 +7,7 @@ metadata:
7
7
  minApiVersion: "66.0"
8
8
  relatedSkills:
9
9
  - "platform-apex-generate"
10
+ - "integration-apex-symbol-table-query"
10
11
  cliTools:
11
12
  - tool: ["sf"]
12
13
  semver: ">=2.0.0"
@@ -51,7 +52,7 @@ Always wrap the code under test in `Test.startTest()` / `Test.stopTest()`:
51
52
 
52
53
  Before generating or fixing tests, identify:
53
54
 
54
- - the target production class(es) under test
55
+ - the target production class(es) under test; use the `integration-apex-symbol-table-query` skill to get their exact method signatures, fields, and inner types rather than inferring them. If the org does not support it (API v67.0+ required) or the call fails, read the source instead
55
56
  - existing test classes, test data factories, and setup helpers
56
57
  - desired test scope (single class, specific methods, suite, or local tests)
57
58
  - coverage threshold (75% minimum for deploy, 90%+ recommended)
@@ -99,15 +99,25 @@ Prefer reusing an existing policy (fastest, no deploy):
99
99
  sf data query --use-tooling-api --target-org <alias> \
100
100
  --query "SELECT Id, DeveloperName, MasterLabel FROM DataMaskPolicy"
101
101
  ```
102
- If none targets the Contact PII you need, author one with the **two-step** recipe (the
102
+ If none targets the object and fields you need, choose the fields first (below), then author one.
103
+
104
+ **Choose the fields from Core's classification, not from field names alone.** Before inserting any
105
+ `DataMaskPolicyField` row, call the field-classification operation once per target object and map each
106
+ `recommendedPiiType` to a treatment. The steps are in `references/policy-authoring.md` ("Choosing the
107
+ fields from Core's classification"). Any recommendation with no verified `MaskValue` token is reported
108
+ as unmapped; never guess a `MaskValue`. Use the static table only if the call fails, and say so in the
109
+ report.
110
+
111
+ Then author one with the **two-step** recipe (the
103
112
  `DataMaskPolicy` Metadata shape is a thin shell; membership is Tooling-inserted):
104
113
  1. **Metadata-deploy the thin shell** in **mdapi format** (`--metadata-dir` + `package.xml`; a
105
114
  source-format `--source-dir` deploy fails "Could not infer a metadata type"). The shell carries
106
115
  only `<label>`, `<description>`, `<runOnRefresh>`. This creates the policy *with an active
107
- revision*, which A2 requires.
116
+ revision*, which the Tooling inserts in the next step require.
108
117
  2. **Tooling-insert** the `DataMaskPolicyObject` (one per object) then its `DataMaskPolicyField`
109
- rows. Each field row's treatment is `MaskingCategory` (`library`) + `MaskValue` (a snake_case
110
- token like `first_name`, `email`, `phone`). There is **no `MaskingRuleType` column**.
118
+ rows. Each field row's treatment is `MaskingCategory` + `MaskValue`. For `library`, `MaskValue` is a
119
+ snake_case token like `first_name`, `email`, `phone`. For `characters` and `replaceRandom`, `MaskValue`
120
+ is the field's Salesforce type (`Text`, `Date`). There is **no `MaskingRuleType` column**.
111
121
 
112
122
  > Insert order matters: a Tooling-created parent (no active revision) makes the child insert fail
113
123
  > `INSUFFICIENT_ACCESS_ON_CROSS_REFERENCE_ENTITY`. Metadata-deploy the shell first.
@@ -150,36 +160,36 @@ mid-run status as final.
150
160
 
151
161
  **Jobs are slow — expect several minutes, and poll with the bundled script.** Data Mask runs on a
152
162
  backend pool/scheduler with a **~5–10 minute floor**: even a tiny (20-row) job usually does **not**
153
- reach a terminal state or emit detail rows for several minutes after the run starts. This is fixed
154
- overhead, **not** proportional to row count. Plan the run around it — the single biggest failure mode
155
- is treating the job as instant, polling on a tight interval, and either timing out or writing a
156
- "still pending" report.
163
+ reach a terminal state or emit detail rows for several minutes. This is fixed overhead, **not**
164
+ proportional to row count. The biggest failure mode is treating the job as instant, polling tightly,
165
+ and either timing out or writing a "still pending" report.
157
166
 
158
167
  Run `scripts/poll-job.sh` as a **single command** — do **not** hand-roll a SOQL poll loop:
159
168
  ```bash
160
169
  bash scripts/poll-job.sh <alias> <jobRunId> # defaults: cap 600s (10 min), 20s interval
161
170
  ```
162
- It sleeps on a low-frequency interval, short-circuits the instant a ground-truth detail row appears,
163
- prints the terminal signal (`completed`/`failed`/`canceled`) on stdout, and exits `0` (or `1` on
164
- timeout). **Call it once and read its result — do not wrap it in your own retry loop**, and do not
165
- poll on a sub-10s interval (it just burns tool calls against a job that cannot finish sooner).
166
-
167
- **Ground truth is the detail rows, not the parent status.** The parent `DataMaskPolicyJobRun.Status`
168
- can **lag** — it may read `pending`/`running` for a while after masking actually finished. Once a
169
- `total_records_masked` (or `completed`) `DataMaskPolicyJobRunDtl` row exists, the masking is done.
170
- `poll-job.sh` already encodes all of this — the bounded interval and timeout, the short-circuit on
171
- the ground-truth detail row, and the terminal-signal exit code — so you do **not** re-implement any
172
- of it inline. Run the poller once, read its exit signal, then update `report.md` (the stub you wrote
173
- before polling) with the terminal status and the masked count from A5.
171
+ It sleeps on a low-frequency interval, prints the terminal status (`completed` /
172
+ `completed_with_errors` / `failed` / `canceled`) on stdout, and exits `0` (or `1` on timeout).
173
+ **Call it once and read its result — do not wrap it in your own retry loop**, and do not poll on a
174
+ sub-10s interval (it just burns tool calls against a job that cannot finish sooner).
175
+
176
+ **The job is done only when the parent `DataMaskPolicyJobRun.Status` is terminal.** A
177
+ `DataMaskPolicyJobRunDtl` row is not a completion signal: a `total_records_masked` row can already
178
+ exist, reading `0`, while the parent is still `running`, so a count read mid-run is wrong. Wait for
179
+ the poller to exit `0`, then read the detail rows (A5) and update the `report.md` stub with the
180
+ terminal status and the masked count.
174
181
 
175
182
  ### A5. Report results from the job DETAIL object
176
183
  The parent job carries an overall status; **per-object masked counts live on the child**
177
- `DataMaskPolicyJobRunDtl` (linked by `DataMaskPolicyJobRunId`). Report a concrete count, not a
178
- fabricated one:
184
+ `DataMaskPolicyJobRunDtl` (linked by `DataMaskPolicyJobRunId`). `DataMaskPolicyJobRunDtl` has **no
185
+ `Status` column** — the status lives in `Type`/`Subtype`, with the count itself in `Value` (a
186
+ string). Report a concrete count, not a fabricated one:
179
187
  ```bash
180
188
  sf data query --target-org <alias> \
181
- --query "SELECT Id, DataMaskPolicyJobRunId, Status FROM DataMaskPolicyJobRunDtl WHERE DataMaskPolicyJobRunId = '<jobRunId>'"
189
+ --query "SELECT ObjectReference, Type, Subtype, Value FROM DataMaskPolicyJobRunDtl WHERE DataMaskPolicyJobRunId = '<jobRunId>' ORDER BY Timestamp"
182
190
  ```
191
+ Expect one `status_update` row per `Subtype` (`loaded`, `completed`, `total_records_masked`) for
192
+ each target object; `total_records_masked`'s `Value` is the masked count.
183
193
 
184
194
  **Report only what the rows literally show — do not overstate granularity.** The detail rows are
185
195
  **object-level** status_update entries (`loaded`, `completed`, `total_records_masked` for the object,
@@ -199,45 +209,28 @@ just to cancel it. Steps B1–B4 are all required.
199
209
 
200
210
  ### B1. Confirm sandbox + identify the run to cancel
201
211
  Confirm the org is a sandbox (`sf org display`) and get the `jobRunId` of the run to abort — the one
202
- the user is asking to cancel. **Capture its `DataMaskPolicyId` too** — you need it to start a
203
- replacement run if the abort window is missed (B2 exit 3 / exit 1). If they just started it, use that
204
- id; otherwise query for the active run:
212
+ the user is asking to cancel. **Capture its policy Id too** — you need it to start a replacement run
213
+ if the abort window is missed (B2 exit 3 / exit 1). If they just started it, use that policy id
214
+ directly; otherwise query for the active run:
205
215
  ```bash
206
216
  sf data query --target-org <alias> \
207
- --query "SELECT Id, Status, DataMaskPolicyId FROM DataMaskPolicyJobRun ORDER BY CreatedDate DESC LIMIT 5"
217
+ --query "SELECT Id, Status, PolicyName FROM DataMaskPolicyJobRun ORDER BY CreatedDate DESC LIMIT 5"
208
218
  ```
209
- Note the `DataMaskPolicyId` (`8dm` prefix) of the run you pick — that is the `<policyId>` A3 needs.
210
-
211
- ### B2. Wait for the job to be `running` (the only abortable state)
212
- You can only abort while `DataMaskPolicyJobRun.Status` is `running`. A `pending`/`scheduled` job
213
- `409`s; a terminal one is already done. Poll for the `running` window with the bundled poller in its
214
- **`running` mode** — it exits the instant the status reads `running` (unlike the default mode, which
215
- waits for a terminal state), so it will not block past the abortable window:
219
+ `DataMaskPolicyJobRun` has **no `DataMaskPolicyId` lookup field** — the only link back to the policy
220
+ is the human-readable `PolicyName` (`MasterLabel`). Resolve the Id from it via the Tooling API:
216
221
  ```bash
217
- POLL_MODE=running bash scripts/poll-job.sh <alias> <jobRunId> 900 15
222
+ sf data query --use-tooling-api --target-org <alias> \
223
+ --query "SELECT Id, DeveloperName FROM DataMaskPolicy WHERE MasterLabel = '<PolicyName>'"
218
224
  ```
219
- The cap is **900s (15 min)**, above the ~5–10 min scheduling floor so a slow-to-start job still gets
220
- caught. Handle every exit:
221
- - **Exit `0`** (prints `running`) → go straight to B3.
222
- - **Exit `3`** → the job raced to a terminal state before `running` was caught; the abort window is
223
- gone. Start a fresh run against the policy you captured in B1 (A3 with that `<policyId>`), then
224
- return here and poll the **new** `jobRunId`.
225
- - **Exit `1`** (timeout — the cap expired) → re-query the job's status:
226
- ```bash
227
- sf data query --target-org <alias> \
228
- --query "SELECT Id, Status FROM DataMaskPolicyJobRun WHERE Id = '<jobRunId>'"
229
- ```
230
- If it is still non-terminal (`pending`/`scheduled`/`running`), re-run the poller **once** more (same
231
- command) to continue waiting. If it is `running`, go to B3. If it is terminal, treat it like exit 3
232
- — start a fresh run (A3 with the B1 `<policyId>`) and poll the new job.
233
-
234
- Because of the ~5–10 min pool floor the `running` window is usually minutes wide, so there is time to
235
- catch it; do not poll with no delay.
236
-
237
- > **If no run is currently in progress** (the job already completed, or you must reproduce a
238
- > run→cancel flow end to end), start one first with A3, then return here — poll it to `running` and
239
- > abort **that** live job. Never substitute an older, already-terminal job to "show" a cancel; the
240
- > abort must target the run that is actually live.
225
+ That `Id` (`8dm` prefix) is the `<policyId>` A3 needs.
226
+
227
+ ### B2. Wait for the job to be `running` (the only abortable state)
228
+ Poll with `POLL_MODE=running` (cap 900s, 15s interval). Full command, exit-code handling (0 / 3 / 1),
229
+ and the re-query-on-timeout steps are in `references/run-and-abort.md` section 5. A `pending` or
230
+ `scheduled` job `409`s on abort, so do not abort until the poller exits `0`.
231
+
232
+ If no run is in progress, start one with A3 and abort **that** live job. Never substitute an older,
233
+ already-terminal job to "show" a cancel.
241
234
 
242
235
  ### B3. Abort via the run API
243
236
  Abort via the run API — **not** by DML/delete on the job record:
@@ -263,14 +256,8 @@ Cancellation is asynchronous. **Re-query** `DataMaskPolicyJobRun` and confirm `S
263
256
  | Rule | Rationale |
264
257
  |------|-----------|
265
258
  | Run each `sf` command **bare** — never add a pipe or redirect of any kind (`\|`, `\| python3`, `\| grep`, `2>&1`, `2>/dev/null`, `> file`) | `sf ... --json` already prints clean JSON on stdout; read it directly. A redirect/pipe trips an unbypassable shell-safety guard that silently stalls the whole run to timeout. Never post-process with `python3`/`grep`/`jq`, and never suppress stderr — even if a command prints a warning, the `--json` payload on stdout is still valid; just parse it as-is |
266
- | Never use standard SOQL / `sobject describe` on `DataMaskPolicy*` config objects | They return `INVALID_TYPE` / `NOT_FOUND` — use Tooling API or MDAPI |
267
- | Read masked counts from `DataMaskPolicyJobRunDtl`, never invent them | The child detail is the source of truth for per-object results |
268
- | Only `completed` / `completed_with_errors` / `failed` are terminal | Reporting `running`/`scheduled` as final is wrong |
269
259
  | Abort only via the run-API abort endpoint | DML/delete on the job record is not a real abort and corrupts state |
270
- | Always re-query status after abort and confirm `canceled` | An abort call returning 200 is not proof the job stopped |
271
- | Data Mask runs on sandboxes only | Run/abort endpoints `403` on production |
272
260
  | Use API version `v67.0` or later, and no `/connect/` segment | The run/abort endpoints are `/services/data/v67.0/platform/data-resilience/data-mask/...` — a `connect` segment or a pre-v67 version returns `NOT_FOUND` |
273
- | Poll via `scripts/poll-job.sh` (one call), never a hand-rolled SOQL loop | The script caps attempts and short-circuits on the ground-truth detail row; a manual loop against the lagging parent status is the #1 cause of a run timing out with no report |
274
261
 
275
262
  ---
276
263
 
@@ -278,15 +265,9 @@ Cancellation is asynchronous. **Re-query** `DataMaskPolicyJobRun` and confirm `S
278
265
 
279
266
  | Issue | Resolution |
280
267
  |-------|------------|
281
- | `sf sobject describe DataMaskPolicy` → `NOT_FOUND` | It's a Tooling/MDAPI entity — query with `--use-tooling-api`, don't retry standard API |
282
- | `SELECT ... FROM DataMaskPolicy` → `INVALID_TYPE` | Same cause — use Tooling API for the policy; standard API only for `DataMaskPolicyJobRun`/`Dtl` |
283
- | Run start returns `409` | A run is already in progress for that policy — poll the existing one or wait for it to finish |
284
- | Abort returns `409` "status=PENDING" | The job is still `pending`/`scheduled`, not yet `running` — keep polling and abort only once it reads `running`; don't give up on the abort |
285
268
  | Small job finishes before you can abort it | The `running` window is seconds on a small sandbox — start a fresh run and poll tightly; never substitute a previously-aborted job to fake the flow |
286
- | Abort returns `200` but SOQL status still `running` | Cancellation is async — keep polling the SOQL status until `canceled`; don't report success early |
287
- | Run API says `CANCELED` but SOQL says `running` | Case + surface differ: the run API is UPPERCASE, SOQL picklist is lowercase. Trust the SOQL value for terminal state |
288
- | Job "finished" instantly | Re-check: `scheduled` is not terminal. Poll until a terminal value actually appears |
289
- | Run/abort endpoint `NOT_FOUND` | The path must be `/services/data/v67.0/platform/data-resilience/data-mask/...` — no `/connect/` segment, and version `v67.0`+ (Core 262). See `references/api-surface.md` |
269
+ | Run API says `CANCELED` but SOQL says `running` | Case + surface differ (run API UPPERCASE, SOQL lowercase). Trust the SOQL value for terminal state |
270
+ | Run start returns `409` or abort `409` | Run start: a run is already active for that policy. Abort: the job is not `running` yet. Details in `references/run-and-abort.md` failure table |
290
271
 
291
272
  ---
292
273
 
@@ -30,8 +30,9 @@ authoritative map.
30
30
  > then (2) Tooling-insert the object + field rows against it. The Metadata deploy must come first:
31
31
  > it creates the policy with an active revision, without which the Tooling child insert fails
32
32
  > `INSUFFICIENT_ACCESS_ON_CROSS_REFERENCE_ENTITY`. See `policy-authoring.md` for the full recipe.
33
- > `DataMaskPolicyField` treatment columns are `MaskingCategory` (`library` / `replaceRandom`) +
34
- > `MaskValue` (a snake_case library token) — there is **no `MaskingRuleType` column**.
33
+ > `DataMaskPolicyField` treatment columns are `MaskingCategory` (`library` / `characters` /
34
+ > `replaceRandom`) + `MaskValue` (a snake_case library token for `library`, or the field's Salesforce
35
+ > type such as `Text` / `Date` for `characters` / `replaceRandom`) — there is **no `MaskingRuleType` column**.
35
36
 
36
37
  > **Schedule fields live ON the policy** — there is no separate schedule entity. `RunFrequency`
37
38
  > (`once` / `daily` / `weekly` / `monthly`), `ScheduledStart`, and `RunOnRefresh` are fields on
@@ -64,11 +65,16 @@ sf data query --use-tooling-api --target-org <alias> \
64
65
 
65
66
  # RIGHT — job + job-detail via standard API:
66
67
  sf data query --target-org <alias> \
67
- --query "SELECT Id, Status, Type, TotalRecords FROM DataMaskPolicyJobRun ORDER BY CreatedDate DESC LIMIT 5"
68
+ --query "SELECT Id, Status, Type, PolicyName FROM DataMaskPolicyJobRun ORDER BY CreatedDate DESC LIMIT 5"
68
69
  sf data query --target-org <alias> \
69
- --query "SELECT Id, DataMaskPolicyJobRunId, Status FROM DataMaskPolicyJobRunDtl WHERE DataMaskPolicyJobRunId = '<jobRunId>'"
70
+ --query "SELECT ObjectReference, Type, Subtype, Value FROM DataMaskPolicyJobRunDtl WHERE DataMaskPolicyJobRunId = '<jobRunId>' ORDER BY Timestamp"
70
71
  ```
71
72
 
73
+ `DataMaskPolicyJobRun` has **no `TotalRecords`** and **no `DataMaskPolicyId`** lookup field —
74
+ resolve the policy from `PolicyName` (`MasterLabel`) via the Tooling API instead.
75
+ `DataMaskPolicyJobRunDtl` has **no `Status`** column — status lives in `Type`/`Subtype`, and the
76
+ masked count is `Value` on the row where `Subtype = 'total_records_masked'`.
77
+
72
78
  ## Status picklist values (`DataMaskPolicyJobRun.Status`)
73
79
 
74
80
  ```text