@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.
- package/README.md +55 -5
- package/package.json +1 -1
- package/skills/agentforce-offline-experimentation-run/SKILL.md +14 -22
- package/skills/agentforce-offline-experimentation-run/scripts/bt_compare.py +81 -0
- package/skills/agentforce-offline-experimentation-run/scripts/bt_fetch.py +358 -0
- package/skills/agentforce-offline-experimentation-run/scripts/bt_patch_agent.py +110 -0
- package/skills/agentforce-offline-experimentation-run/scripts/btlib/__init__.py +34 -0
- package/skills/agentforce-offline-experimentation-run/scripts/btlib/agentpatch.py +483 -0
- package/skills/agentforce-offline-experimentation-run/scripts/btlib/agentsrc.py +565 -0
- package/skills/agentforce-offline-experimentation-run/scripts/btlib/cache.py +95 -0
- package/skills/agentforce-offline-experimentation-run/scripts/btlib/compare.py +617 -0
- package/skills/agentforce-offline-experimentation-run/scripts/btlib/fetch.py +1012 -0
- package/skills/agentforce-offline-experimentation-run/scripts/btlib/judge.py +148 -0
- package/skills/agentforce-offline-experimentation-run/scripts/btlib/messages.py +92 -0
- package/skills/agentforce-offline-experimentation-run/scripts/btlib/patch.py +327 -0
- package/skills/agentforce-offline-experimentation-run/scripts/btlib/redact.py +40 -0
- package/skills/agentforce-offline-experimentation-run/scripts/btlib/results.py +288 -0
- package/skills/agentforce-offline-experimentation-run/scripts/btlib/scoring.py +340 -0
- package/skills/agentforce-offline-experimentation-run/scripts/btlib/sfap.py +238 -0
- package/skills/agentforce-offline-experimentation-run/scripts/btlib/splits.py +136 -0
- package/skills/agentforce-offline-experimentation-run/scripts/btlib/stdm.py +98 -0
- package/skills/agentforce-offline-experimentation-run/scripts/btlib/tokens.py +173 -0
- package/skills/agentforce-offline-experimentation-run/scripts/btlib/units.py +199 -0
- package/skills/agentforce-offline-experimentation-run/scripts/btlib/versions.py +194 -0
- package/skills/agentforce-offline-experimentation-run/scripts/build_report.py +366 -378
- package/skills/agentforce-offline-experimentation-run/scripts/resolve-token.sh +14 -4
- package/skills/agentforce-offline-experimentation-run/scripts/sfap_replay.py +472 -796
- package/skills/dx-lifecycle-center/SKILL.md +5 -2
- package/skills/dx-lifecycle-center/assets/widgets/connect-connected/connect-connected.json +65 -0
- package/skills/dx-lifecycle-center/assets/widgets/connect-connected/schema.json +32 -0
- package/skills/dx-lifecycle-center/assets/widgets/connect-sign-in/connect-sign-in.json +57 -0
- package/skills/dx-lifecycle-center/assets/widgets/connect-sign-in/schema.json +21 -0
- package/skills/dx-lifecycle-center/examples/workflows.md +20 -6
- package/skills/dx-lifecycle-center/references/connect-org.md +21 -2
- package/skills/dx-lifecycle-center/references/deploy.md +2 -2
- package/skills/dx-lifecycle-center/references/list-orgs.md +1 -1
- package/skills/dx-lifecycle-center/scripts/card-policy.jq +71 -0
- package/skills/dx-lifecycle-center/scripts/fill-widget.jq +29 -0
- package/skills/dx-lifecycle-center/scripts/widget.sh +28 -0
- package/skills/dx-org-devhub-configure/SKILL.md +85 -84
- package/skills/dx-org-devhub-configure/examples/status-output.md +25 -2
- package/skills/dx-org-devhub-configure/scripts/devhub.sh +265 -140
- package/skills/dx-org-devhub-configure/scripts/test-devhub.sh +100 -15
- package/skills/dx-packaging-move-to-2gp-migrate/SKILL.md +89 -0
- package/skills/dx-packaging-move-to-2gp-migrate/references/api-operations.md +164 -0
- package/skills/dx-scratch-def-configure/SKILL.md +163 -0
- package/skills/dx-scratch-def-configure/examples/confirm-card.md +53 -0
- package/skills/dx-scratch-def-configure/examples/report-recipes.md +100 -0
- package/skills/dx-scratch-def-configure/references/features.json +3270 -0
- package/skills/dx-scratch-def-configure/references/project-scratch-def.schema.json +1841 -0
- package/skills/dx-scratch-def-configure/references/settings-map.json +682 -0
- package/skills/dx-scratch-def-configure/scripts/lib_scratch_def.py +1306 -0
- package/skills/dx-scratch-def-configure/scripts/requirements.txt +3 -0
- package/skills/dx-scratch-def-configure/scripts/scrape_config_values.py +848 -0
- package/skills/education-cloud-domain-configure/SKILL.md +1 -1
- package/skills/education-cloud-multi-campus-configure/SKILL.md +1 -1
- package/skills/education-cloud-student-recruitment-agent-configure/SKILL.md +1 -1
- package/skills/experience-design-validate/SKILL.md +1 -1
- package/skills/experience-search-coordinate/SKILL.md +18 -12
- package/skills/experience-search-coordinate/references/content-route.md +79 -1
- package/skills/field-service-data-capture-migrate/SKILL.md +1 -1
- package/skills/platform-apex-generate/SKILL.md +14 -11
- package/skills/platform-apex-test-generate/SKILL.md +2 -1
- package/skills/platform-datamask-run/SKILL.md +52 -71
- package/skills/platform-datamask-run/references/api-surface.md +10 -4
- package/skills/platform-datamask-run/references/policy-authoring.md +56 -7
- package/skills/platform-datamask-run/references/run-and-abort.md +17 -10
- package/skills/platform-datamask-run/scripts/poll-job.sh +15 -25
- package/skills/platform-dataspace-access-configure/SKILL.md +19 -1
- package/skills/platform-metadata-discovery-get/SKILL.md +185 -0
- package/skills/platform-metadata-discovery-get/references/api-map.md +25 -0
- package/skills/platform-metadata-discovery-get/references/h360-routes.md +30 -0
- package/skills/platform-metadata-discovery-get/references/scoped-soql.md +52 -0
- package/skills/platform-metadata-grounding-get/SKILL.md +170 -0
- package/skills/platform-policy-rule-generate/SKILL.md +84 -232
- package/skills/platform-policy-rule-generate/references/condition-model.md +47 -0
- package/skills/platform-policy-rule-generate/references/condition-templates.md +89 -0
- package/skills/platform-policy-rule-generate/references/conjunction-compiler.md +25 -0
- package/skills/platform-policy-rule-generate/references/conjunction-expression.md +22 -0
- package/skills/platform-policy-rule-generate/references/consumer-all.md +9 -0
- package/skills/platform-policy-rule-generate/references/consumer-datacloud-ui.md +91 -0
- package/skills/platform-policy-rule-generate/references/deploy-errors.md +24 -36
- package/skills/platform-policy-rule-generate/references/examples/ExistsJoinRLS/package.xml +12 -0
- package/skills/platform-policy-rule-generate/references/examples/ExistsJoinRLS/policyRuleDefinitionSets/ExistsJoinRLS.policyRuleDefinitionSet +6 -0
- package/skills/platform-policy-rule-generate/references/examples/ExistsJoinRLS/policyRuleDefinitions/ExistsJoinRLS_Rule_1.policyRuleDefinition +91 -0
- package/skills/platform-policy-rule-generate/references/examples/FieldReadDeny/package.xml +12 -0
- package/skills/platform-policy-rule-generate/references/examples/FieldReadDeny/policyRuleDefinitionSets/FieldReadDeny.policyRuleDefinitionSet +6 -0
- package/skills/platform-policy-rule-generate/references/examples/FieldReadDeny/policyRuleDefinitions/FieldReadDeny_Rule_1.policyRuleDefinition +72 -0
- package/skills/platform-policy-rule-generate/references/examples/LastCharactersFieldMasking/package.xml +12 -0
- package/skills/platform-policy-rule-generate/references/examples/LastCharactersFieldMasking/policyRuleDefinitionSets/LastCharactersFieldMasking.policyRuleDefinitionSet +6 -0
- package/skills/platform-policy-rule-generate/references/examples/LastCharactersFieldMasking/policyRuleDefinitions/LastCharactersFieldMasking_Rule_1.policyRuleDefinition +77 -0
- package/skills/platform-policy-rule-generate/references/examples/PrincipalFieldMatchRLS/package.xml +12 -0
- package/skills/platform-policy-rule-generate/references/examples/PrincipalFieldMatchRLS/policyRuleDefinitionSets/PrincipalFieldMatchRLS.policyRuleDefinitionSet +6 -0
- package/skills/platform-policy-rule-generate/references/examples/PrincipalFieldMatchRLS/policyRuleDefinitions/PrincipalFieldMatchRLS_Rule_1.policyRuleDefinition +77 -0
- package/skills/platform-policy-rule-generate/references/examples/TaggedObjectAccess/package.xml +12 -0
- package/skills/platform-policy-rule-generate/references/examples/TaggedObjectAccess/policyRuleDefinitionSets/TaggedObjectAccess.policyRuleDefinitionSet +4 -0
- package/skills/platform-policy-rule-generate/references/examples/TaggedObjectAccess/policyRuleDefinitions/TaggedObjectAccess_Rule_1.policyRuleDefinition +65 -0
- package/skills/platform-policy-rule-generate/references/headless-transport.md +87 -0
- package/skills/platform-policy-rule-generate/references/policy-schema-full.md +17 -10
- package/skills/platform-policy-rule-generate/references/policy-shapes.md +74 -0
- package/skills/platform-policy-rule-generate/references/principal-expression.md +31 -0
- package/skills/platform-policy-rule-generate/references/reference-resolution.md +51 -0
- package/skills/platform-policy-rule-generate/references/resource-expression.md +48 -0
- package/skills/platform-policy-rule-generate/references/runtime-context.md +9 -0
- package/skills/platform-policy-rule-generate/references/templates-access.md +11 -9
- package/skills/platform-policy-rule-generate/references/templates-advanced.md +4 -2
- package/skills/platform-policy-rule-generate/references/templates-record.md +37 -22
- package/skills/platform-policy-rule-generate/references/templates-transform.md +10 -8
- package/skills/platform-policy-rule-generate/references/templates.md +2 -2
- package/skills/platform-policy-rule-generate/references/ui-compatibility.md +35 -17
- package/skills/platform-report-generate/SKILL.md +9 -4
- package/skills/platform-report-generate/references/chart-types.md +26 -2
- package/skills/platform-report-generate/references/column-names.md +43 -0
- package/skills/platform-report-generate/references/errors-and-troubleshooting.md +1 -1
- package/skills/platform-report-generate/references/verification-checklist.md +10 -2
- package/skills/service-digital-engagement-channel-configure/SKILL.md +7 -5
- package/skills/service-digital-engagement-channel-configure/assets/messaging_channel_template.xml +33 -0
- 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: "
|
|
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: "
|
|
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) && (
|
|
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. **
|
|
117
|
-
|
|
118
|
-
- **
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
|
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
|
|
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. **
|
|
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
|
|
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
|
|
|
@@ -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`.
|
|
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
|
-
|
|
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
|
-
-
|
|
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
|
-
-
|
|
72
|
-
- Fallback: `sf code-analyzer run --target <target>`. If both are unavailable, record `
|
|
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
|
|
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 `
|
|
84
|
-
- The `Testing` line must contain the actual Step 7 results (or `test_execution=unavailable: <reason>` after attempting
|
|
85
|
-
- A report missing either line is incomplete
|
|
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
|
|
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
|
|
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
|
|
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`
|
|
110
|
-
token like `first_name`, `email`, `phone
|
|
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
|
|
154
|
-
|
|
155
|
-
|
|
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,
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
**
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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`).
|
|
178
|
-
|
|
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
|
|
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
|
|
203
|
-
|
|
204
|
-
|
|
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,
|
|
217
|
+
--query "SELECT Id, Status, PolicyName FROM DataMaskPolicyJobRun ORDER BY CreatedDate DESC LIMIT 5"
|
|
208
218
|
```
|
|
209
|
-
|
|
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
|
-
|
|
222
|
+
sf data query --use-tooling-api --target-org <alias> \
|
|
223
|
+
--query "SELECT Id, DeveloperName FROM DataMaskPolicy WHERE MasterLabel = '<PolicyName>'"
|
|
218
224
|
```
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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
|
-
|
|
|
287
|
-
| Run
|
|
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` / `
|
|
34
|
-
> `MaskValue` (a snake_case library token
|
|
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,
|
|
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
|
|
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
|