@salesforce/afv-skills 1.40.0 → 1.41.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 (136) hide show
  1. package/package.json +1 -1
  2. package/skills/automation-flow-generate/SKILL.md +32 -43
  3. package/skills/experience-portal-create/SKILL.md +497 -0
  4. package/skills/experience-portal-create/assets/report-template.md +30 -0
  5. package/skills/experience-portal-create/references/mcp-invocation.md +288 -0
  6. package/skills/experience-portal-create/references/post-creation-activate-publish.md +165 -0
  7. package/skills/experience-portal-create/references/templates.md +253 -0
  8. package/skills/experience-ui-bundle-features-generate/SKILL.md +5 -1
  9. package/skills/experience-ui-bundle-frontend-generate/SKILL.md +2 -0
  10. package/skills/experience-ui-bundle-frontend-generate/references/page.md +1 -0
  11. package/skills/platform-datamask-run/SKILL.md +345 -0
  12. package/skills/platform-datamask-run/references/api-surface.md +130 -0
  13. package/skills/platform-datamask-run/references/policy-authoring.md +185 -0
  14. package/skills/platform-datamask-run/references/run-and-abort.md +116 -0
  15. package/skills/platform-datamask-run/scripts/poll-job.sh +115 -0
  16. package/skills/platform-dataspace-access-configure/SKILL.md +51 -3
  17. package/skills/platform-dataspace-access-configure/scripts/inspect-dataspace-scopes.sh +56 -0
  18. package/skills/platform-lightning-type-widget-coordinate/references/build-plan-format.md +1 -0
  19. package/skills/platform-sandbox-configure/SKILL.md +17 -2
  20. package/skills/platform-trial-org-create/SKILL.md +175 -0
  21. package/skills/platform-trial-org-create/examples/create_request.json +9 -0
  22. package/skills/platform-trial-org-create/examples/error_response.json +41 -0
  23. package/skills/platform-trial-org-create/examples/success_response.json +27 -0
  24. package/skills/platform-trial-org-create/references/error_codes.md +42 -0
  25. package/skills/platform-trial-org-create/references/signup_request_fields.md +69 -0
  26. package/skills/platform-trial-org-create/scripts/create_signup_request.sh +175 -0
  27. package/skills/platform-trial-org-create/scripts/get_signup_request.sh +155 -0
  28. package/skills/platform-widget-generate/SKILL.md +47 -6
  29. package/skills/platform-widget-generate/examples/conditional.json +3 -3
  30. package/skills/platform-widget-generate/examples/list-with-foreach.json +2 -2
  31. package/skills/platform-widget-generate/examples/single-object.json +2 -2
  32. package/skills/platform-widget-generate/references/widget-bundle-layout.md +1 -1
  33. package/skills/service-agentforce-channel-configure/SKILL.md +271 -0
  34. package/skills/service-agentforce-channel-configure/references/agent-wiring.md +97 -0
  35. package/skills/service-agentforce-channel-configure/references/channel-branch-email.md +145 -0
  36. package/skills/service-agentforce-channel-configure/references/channel-branch-voice.md +69 -0
  37. package/skills/service-agentforce-channel-configure/references/channel-types.md +61 -0
  38. package/skills/service-agentforce-channel-configure/references/live-traffic-gate.md +86 -0
  39. package/skills/service-agentforce-channel-configure/references/queue-resolution.md +135 -0
  40. package/skills/service-agentforce-channel-configure/references/routing-flow.md +384 -0
  41. package/skills/service-catalog-template-deploy/SKILL.md +310 -0
  42. package/skills/service-catalog-template-deploy/references/cli-invocation.md +258 -0
  43. package/skills/service-catalog-template-deploy/scripts/activate-verify.mjs +164 -0
  44. package/skills/service-catalog-template-deploy/scripts/build-deploy-payload.mjs +94 -0
  45. package/skills/service-catalog-template-deploy/scripts/resolve-template.mjs +331 -0
  46. package/skills/service-catalog-template-search/SKILL.md +212 -0
  47. package/skills/service-catalog-template-search/references/cli-invocation.md +128 -0
  48. package/skills/service-catalog-template-search/scripts/classify-catalog.mjs +205 -0
  49. package/skills/service-concierge-portal-generate/SKILL.md +126 -0
  50. package/skills/service-concierge-portal-generate/references/portal-deploy-runbook.md +1428 -0
  51. package/skills/service-digital-engagement-channel-configure/SKILL.md +46 -6
  52. package/skills/service-digital-engagement-channel-configure/assets/messaging_channel_template.xml +2 -1
  53. package/skills/service-digital-engagement-channel-configure/examples/asa_agent_channel.xml +4 -1
  54. package/skills/service-helpagent-coordinate/README.md +8 -2
  55. package/skills/service-helpagent-coordinate/SKILL.md +126 -130
  56. package/skills/service-helpagent-coordinate/assets/help-agent-spec.md +70 -53
  57. package/skills/service-helpagent-coordinate/references/agent-script.md +571 -457
  58. package/skills/service-helpagent-coordinate/references/channel-voice.md +38 -9
  59. package/skills/service-helpagent-coordinate/references/channel-web-chat.md +173 -49
  60. package/skills/service-helpagent-coordinate/references/output-report-format.md +126 -0
  61. package/skills/service-itsm-agentic-setup-agentforce-coordinate/SKILL.md +153 -0
  62. package/skills/service-itsm-agentic-setup-agentforce-coordinate/examples/output-templates.md +79 -0
  63. package/skills/service-itsm-agentic-setup-agentforce-coordinate/scripts/verify-child-verdict.mjs +35 -0
  64. package/skills/service-itsm-agentic-setup-agentforce-studio-configure/SKILL.md +271 -0
  65. package/skills/service-itsm-agentic-setup-agentforce-studio-configure/references/cli-invocation.md +265 -0
  66. package/skills/service-itsm-agentic-setup-agentforce-studio-configure/scripts/classify-enable-plan.mjs +220 -0
  67. package/skills/service-itsm-agentic-setup-agentforce-studio-configure/scripts/classify-final-report.mjs +102 -0
  68. package/skills/service-itsm-agentic-setup-agentforce-studio-configure/scripts/record-enable-result.mjs +73 -0
  69. package/skills/service-itsm-agentic-setup-agentforce-studio-validate/SKILL.md +206 -0
  70. package/skills/service-itsm-agentic-setup-agentforce-studio-validate/references/cli-invocation.md +194 -0
  71. package/skills/service-itsm-agentic-setup-agentforce-studio-validate/scripts/classify-readiness.mjs +223 -0
  72. package/skills/service-itsm-agentic-setup-cmdb-configure/SKILL.md +45 -7
  73. package/skills/service-itsm-agentic-setup-cmdb-configure/references/mcp-invocation.md +45 -5
  74. package/skills/service-itsm-agentic-setup-configure/SKILL.md +116 -0
  75. package/skills/service-itsm-agentic-setup-configure/examples/output-templates.md +64 -0
  76. package/skills/service-itsm-agentic-setup-employee-agent-configure/SKILL.md +158 -0
  77. package/skills/service-itsm-agentic-setup-employee-agent-configure/references/cli-invocation.md +361 -0
  78. package/skills/service-itsm-agentic-setup-employee-agent-configure/references/error-taxonomy.md +44 -0
  79. package/skills/service-itsm-agentic-setup-employee-agent-configure/references/reactivation.md +66 -0
  80. package/skills/service-itsm-agentic-setup-employee-agent-configure/references/report-format.md +66 -0
  81. package/skills/service-itsm-agentic-setup-employee-agent-configure/references/specialized-templates.md +148 -0
  82. package/skills/service-itsm-agentic-setup-employee-agent-configure/references/workflow-detail.md +169 -0
  83. package/skills/service-itsm-agentic-setup-employee-agent-configure/scripts/build-create-body.mjs +116 -0
  84. package/skills/service-itsm-agentic-setup-employee-agent-configure/scripts/classify-agent-existence.mjs +185 -0
  85. package/skills/service-itsm-agentic-setup-employee-agent-configure/scripts/classify-preflight.mjs +168 -0
  86. package/skills/service-itsm-agentic-setup-employee-agent-configure/scripts/create-scratch-dir.mjs +58 -0
  87. package/skills/service-itsm-agentic-setup-employee-agent-configure/scripts/render-report.mjs +197 -0
  88. package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/SKILL.md +186 -0
  89. package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/references/action-availability.md +51 -0
  90. package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/references/cli-invocation.md +345 -0
  91. package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/references/error-taxonomy.md +44 -0
  92. package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/references/reactivation.md +63 -0
  93. package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/references/report-format.md +44 -0
  94. package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/references/workflow-detail.md +149 -0
  95. package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/scripts/build-create-body.mjs +110 -0
  96. package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/scripts/classify-action-availability.mjs +201 -0
  97. package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/scripts/classify-activate-result.mjs +135 -0
  98. package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/scripts/classify-agent-existence.mjs +194 -0
  99. package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/scripts/classify-preflight.mjs +158 -0
  100. package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/scripts/create-scratch-dir.mjs +58 -0
  101. package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/scripts/render-report.mjs +191 -0
  102. package/skills/service-itsm-agentic-setup-incident-management/SKILL.md +133 -0
  103. package/skills/service-itsm-agentic-setup-incident-management/examples/output-templates.md +71 -0
  104. package/skills/service-itsm-agentic-setup-incident-sla-configure/SKILL.md +308 -0
  105. package/skills/service-itsm-agentic-setup-incident-sla-configure/assets/attach-milestone.json +23 -0
  106. package/skills/service-itsm-agentic-setup-incident-sla-configure/examples/milestone-patterns.md +193 -0
  107. package/skills/service-itsm-agentic-setup-incident-sla-configure/examples/output-templates.md +57 -0
  108. package/skills/service-itsm-agentic-setup-incident-sla-configure/references/mcp-invocation.md +394 -0
  109. package/skills/service-itsm-agentic-setup-itsm-agentforce-permset-assign/SKILL.md +266 -0
  110. package/skills/service-itsm-agentic-setup-itsm-agentforce-permset-assign/references/cli-invocation.md +106 -0
  111. package/skills/service-itsm-agentic-setup-itsm-agentforce-permset-assign/references/helper-contracts.md +142 -0
  112. package/skills/service-itsm-agentic-setup-itsm-agentforce-permset-assign/references/permset-topology.md +82 -0
  113. package/skills/service-itsm-agentic-setup-itsm-agentforce-permset-assign/scripts/classify-action-surface.mjs +137 -0
  114. package/skills/service-itsm-agentic-setup-itsm-agentforce-permset-assign/scripts/classify-assignment-state.mjs +99 -0
  115. package/skills/service-itsm-agentic-setup-itsm-agentforce-permset-assign/scripts/classify-permset-availability.mjs +120 -0
  116. package/skills/service-itsm-agentic-setup-itsm-agentforce-permset-assign/scripts/resolve-target-user.mjs +86 -0
  117. package/skills/service-itsm-agentic-setup-uel-user-create/SKILL.md +284 -0
  118. package/skills/service-itsm-agentic-setup-uel-user-create/references/mcp-invocation.md +302 -0
  119. package/skills/service-itsm-channels-coordinate/SKILL.md +472 -0
  120. package/skills/service-itsm-incident-mgmt-configure/SKILL.md +212 -0
  121. package/skills/service-itsm-incident-mgmt-configure/references/mcp-invocation.md +225 -0
  122. package/skills/service-itsm-incident-priority-configure/SKILL.md +53 -12
  123. package/skills/service-itsm-swarming-configure/SKILL.md +212 -0
  124. package/skills/service-itsm-teams-configure/SKILL.md +395 -0
  125. package/skills/service-itsm-teams-configure/references/azure-credential-population.md +213 -0
  126. package/skills/service-itsm-teams-configure/references/gotchas.md +23 -0
  127. package/skills/service-itsm-teams-coordinate/SKILL.md +175 -0
  128. package/skills/service-itsm-teams-coordinate/examples/output-templates.md +85 -0
  129. package/skills/service-itsm-teams-debug/SKILL.md +144 -0
  130. package/skills/service-itsm-teams-debug/references/configuration-checklists.md +277 -0
  131. package/skills/service-itsm-teams-debug/references/report-generation.md +95 -0
  132. package/skills/service-itsm-teams-employee-agent-configure/SKILL.md +139 -0
  133. package/skills/service-itsm-teams-employee-agent-configure/assets/Teams_AgentForce.EmbeddedServiceConfig-meta.xml +43 -0
  134. package/skills/service-itsm-teams-employee-agent-configure/references/teams-embedded-employee-agent.md +480 -0
  135. package/skills/service-itsm-teams-itdesk-configure/SKILL.md +232 -0
  136. package/skills/service-itsm-teams-itservice-configure/SKILL.md +391 -0
@@ -0,0 +1,130 @@
1
+ # Data Mask API Surface Reference
2
+
3
+ The defining characteristic of Data Mask automation is that its entities live on **different API
4
+ surfaces**. Reaching for the wrong one is the primary cause of failed/stuck runs. This file is the
5
+ authoritative map.
6
+
7
+ ## Per-entity surface
8
+
9
+ | Entity | Role | Surface | Reachable by |
10
+ |--------|------|---------|--------------|
11
+ | `DataMaskPolicy` | The masking policy shell (config) | Tooling API / Metadata API | `sf data query --use-tooling-api`, MDAPI deploy (thin shell only) |
12
+ | `DataMaskPolicyObject` | Object targeted by a policy (+ its optional row filter) | **Tooling API only** | Tooling query + insert |
13
+ | `DataMaskPolicyField` | Field + masking treatment | **Tooling API only** | Tooling query + insert |
14
+ | `DataMaskPolicyJobRun` | The job (a single run) | **Standard data API** | `sf data query` (plain SOQL) |
15
+ | `DataMaskPolicyJobRunDtl` | Per-object job detail (child) | **Standard data API** | `sf data query` (plain SOQL) |
16
+ | `DataMaskCustomValueLibrary` | Custom replacement-value library | **Standard data API** | `sf data query` (plain SOQL) |
17
+
18
+ `DataMaskPolicyJobRunDtl` is a child of `DataMaskPolicyJobRun` via the lookup
19
+ **`DataMaskPolicyJobRunId`**. `DataMaskPolicy` Ids carry the **`8dm`** key prefix.
20
+
21
+ > **`DataMaskPolicy` is a THIN Metadata API type.** `sf org list metadata-types` returns
22
+ > `DataMaskPolicy` (directory `dataMaskPolicies`) with **no child components** (`childXmlNames: []`).
23
+ > Only `<label>`, `<description>`, and `<runOnRefresh>` serialize into its metadata — object and
24
+ > field membership is **NOT** part of the Metadata API shape (there is no inline `<policyObjects>` /
25
+ > `<policyFields>`, and no standalone `DataMaskPolicyObject` / `DataMaskPolicyField` Metadata API
26
+ > type). Membership lives entirely in the Tooling entities `DataMaskPolicyObject` and
27
+ > `DataMaskPolicyField`, which you both **query and insert**. So authoring a complete policy is a
28
+ > **two-step** operation: (1) Metadata-deploy the thin shell (in **mdapi format** — `--metadata-dir`
29
+ > + `package.xml`; a source-format `--source-dir` deploy fails "Could not infer a metadata type"),
30
+ > then (2) Tooling-insert the object + field rows against it. The Metadata deploy must come first:
31
+ > it creates the policy with an active revision, without which the Tooling child insert fails
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**.
35
+
36
+ > **Schedule fields live ON the policy** — there is no separate schedule entity. `RunFrequency`
37
+ > (`once` / `daily` / `weekly` / `monthly`), `ScheduledStart`, and `RunOnRefresh` are fields on
38
+ > `DataMaskPolicy` (confirmed via Tooling describe), so scheduling folds into policy create/update.
39
+
40
+ > **Row-subset ("sample") filtering lives ON `DataMaskPolicyObject`, NOT on the policy.** There is
41
+ > **no `sampleSize` field** anywhere on `DataMaskPolicy`, and **no `LIMIT`** — Data Mask has no
42
+ > row-cap concept. To mask only a subset, set `FilterEnabled = true` and a **selective** predicate
43
+ > that genuinely matches fewer rows in `WhereCriteria` (a **40-char** SOQL-style predicate, e.g.
44
+ > `LastName LIKE '%son%'`); `RawFilterData` holds the structured form the engine actually executes.
45
+ > The masked count equals the number of rows the predicate matches. A `LIMIT` is **silently
46
+ > ignored** (`LastName != 'X' LIMIT 20` masks the whole table — 407 rows — because the predicate is
47
+ > always-true). Confirmed live. See `policy-authoring.md` → "Masking only a subset".
48
+ > **`RawFilterData`'s `operation` must be one of** `eq`, `ne`, `lt`, `gt`, `ge`, `le`, `contains`,
49
+ > `not_contains`, `in`, `not_in` — anything else (e.g. `startsWith`) fails the run with a `422`.
50
+ > **The predicate must also be valid SOQL** — Data Mask runs a planning `SELECT count() ... WHERE
51
+ > <predicate>`, so `Id != 'null'` fails the whole job (`invalid ID field: null`). Filter on a text
52
+ > field like `LastName`, not `Id`.
53
+
54
+ ### What fails, and why
55
+
56
+ ```bash
57
+ # WRONG — these are Tooling/MDAPI entities, not standard-data-API objects:
58
+ sf sobject describe --sobject DataMaskPolicy --target-org <alias> # -> NOT_FOUND
59
+ sf data query --query "SELECT Id FROM DataMaskPolicy" --target-org <alias> # -> INVALID_TYPE
60
+
61
+ # RIGHT — policy config via Tooling API:
62
+ sf data query --use-tooling-api --target-org <alias> \
63
+ --query "SELECT Id, DeveloperName, MasterLabel FROM DataMaskPolicy"
64
+
65
+ # RIGHT — job + job-detail via standard API:
66
+ sf data query --target-org <alias> \
67
+ --query "SELECT Id, Status, Type, TotalRecords FROM DataMaskPolicyJobRun ORDER BY CreatedDate DESC LIMIT 5"
68
+ sf data query --target-org <alias> \
69
+ --query "SELECT Id, DataMaskPolicyJobRunId, Status FROM DataMaskPolicyJobRunDtl WHERE DataMaskPolicyJobRunId = '<jobRunId>'"
70
+ ```
71
+
72
+ ## Status picklist values (`DataMaskPolicyJobRun.Status`)
73
+
74
+ ```text
75
+ pending, scheduled, running, completed, completed_with_errors, failed, canceled
76
+ ```
77
+
78
+ - **Mid-run (not terminal):** `pending`, `scheduled`, `running`
79
+ - **Terminal (success/failure):** `completed`, `completed_with_errors`, `failed`
80
+ - **Terminal after abort:** `canceled` ← note the single "l"
81
+
82
+ `DataMaskPolicyJobRun.Type` picklist: `auto`, `manual`, `scheduled`.
83
+
84
+ Never report a mid-run value as the final status — poll until a terminal value appears.
85
+
86
+ > **Case differs by surface.** These SOQL picklist values are **lowercase**. The run/abort REST
87
+ > responses return the same states **UPPERCASE** (`RUNNING`, `CANCELED`). Poll for terminal state
88
+ > against the lowercase SOQL value — do not compare it to the run-API response string.
89
+
90
+ ## Run / abort REST endpoints
91
+
92
+ Base: `/services/data/v67.0/platform/data-resilience/data-mask`
93
+
94
+ - Version must be **`v67.0` or later** (Core release 262, where these endpoints were added).
95
+ - There is **no `/connect/` segment** — the path is `/services/data/v67.0/platform/...`. A `connect`
96
+ segment or a pre-v67 version returns `NOT_FOUND`. (This was the top failure mode — verified live.)
97
+ - The id is bound as a **path segment**, not a body field.
98
+
99
+ ### Start a run
100
+ ```text
101
+ POST /services/data/v67.0/platform/data-resilience/data-mask/policies/{policyId}/run
102
+ ```
103
+ - Empty JSON body `{}` (`sf api request rest` requires `--body` on a POST; the API takes no payload).
104
+ - **`200`** → accepted; response `{ jobRunId, policyId, status: "RUNNING", message: "Job started successfully" }` (status UPPERCASE).
105
+ - `403` → org is production (Data Mask runs are sandbox-only; runtime sandbox guard).
106
+ - `409`/`CONFLICT` → a run is already in progress for that policy.
107
+
108
+ ### Abort a run
109
+ ```text
110
+ POST /services/data/v67.0/platform/data-resilience/data-mask/jobs/{jobRunId}/abort
111
+ ```
112
+ - Empty JSON body `{}`.
113
+ - **`200`** → abort accepted, response `status: "CANCELED"`, `message: "Job abort requested"` (async — confirm with a SOQL re-query).
114
+ - `404` → unknown job run id.
115
+ - `409` → job is not in a `running` state (already terminal or still `scheduled`).
116
+ - After a successful abort, `DataMaskPolicyJobRun.Status` (SOQL, lowercase) becomes `canceled`.
117
+
118
+ ### Calling the run API from the CLI
119
+ ```bash
120
+ # Write an empty JSON object to a file, then pass it with an @ prefix:
121
+ printf '{}' > ./empty-body.json
122
+ sf api request rest \
123
+ "/services/data/v67.0/platform/data-resilience/data-mask/policies/<policyId>/run" \
124
+ --method POST --body @./empty-body.json --target-org <alias>
125
+ ```
126
+ `sf api request rest` handles auth/session automatically — no need to extract a token by hand.
127
+ **A file body needs the `@` prefix** (`--body @./empty-body.json`) — without it the literal path
128
+ string is sent as the body and the API returns `JSON_PARSER_ERROR`. The file just needs `{}`.
129
+ If you must call it from Apex (`HttpRequest`), use `URL.getOrgDomainURL()` +
130
+ `UserInfo.getSessionId()` for the base URL and bearer token.
@@ -0,0 +1,185 @@
1
+ # Authoring a DataMaskPolicy
2
+
3
+ `DataMaskPolicy` is a **thin Metadata API shell**. Only three elements serialize into its
4
+ metadata — `<label>`, `<description>`, `<runOnRefresh>`. Object and field membership is **NOT**
5
+ part of the Metadata API shape (`DataMaskPolicy` has `childXmlNames: []` — there is no inline
6
+ `<policyObjects>` / `<policyFields>`). Membership lives in two separate **Tooling API** entities,
7
+ `DataMaskPolicyObject` and `DataMaskPolicyField`, which you insert as rows.
8
+
9
+ Authoring a complete, runnable policy from scratch is therefore a **two-step** operation:
10
+
11
+ 1. **Metadata API deploy** the thin `DataMaskPolicy` shell. This is what creates the policy *with
12
+ an active revision* — the child rows in step 2 depend on it.
13
+ 2. **Tooling API insert** the `DataMaskPolicyObject` (one per target object) and its
14
+ `DataMaskPolicyField` rows (one per masked field).
15
+
16
+ > **Order matters.** If you create the parent via the Tooling API instead of a Metadata deploy,
17
+ > it has no active revision, and the child insert fails with
18
+ > `INSUFFICIENT_ACCESS_ON_CROSS_REFERENCE_ENTITY`. Always Metadata-deploy the shell first.
19
+
20
+ Reuse an existing policy when one fits; author a new one only when needed.
21
+
22
+ ## Reuse first
23
+ ```bash
24
+ sf data query --use-tooling-api --target-org <alias> \
25
+ --query "SELECT Id, DeveloperName, MasterLabel FROM DataMaskPolicy"
26
+ ```
27
+ If a policy already targets the object/fields you need, use its `Id` and skip authoring.
28
+
29
+ ## Step 1 — Metadata-deploy the thin shell
30
+
31
+ The `.dataMaskPolicy` metadata file carries ONLY these three elements. `<masterLabel>`,
32
+ `<developerName>`, `<sampleSize>`, and any membership element are **rejected** — they are not part
33
+ of the type. (There is **no `sampleSize` field anywhere** on `DataMaskPolicy` — to mask only a
34
+ subset of records, use the row filter on `DataMaskPolicyObject`; see "Masking only a subset" below.)
35
+
36
+ ```xml
37
+ <?xml version="1.0" encoding="UTF-8"?>
38
+ <DataMaskPolicy xmlns="http://soap.sforce.com/2006/04/metadata">
39
+ <label>Contact PII Mask</label>
40
+ <description>Masks core Contact PII in sandbox.</description>
41
+ <runOnRefresh>false</runOnRefresh>
42
+ </DataMaskPolicy>
43
+ ```
44
+
45
+ Deploy it in **mdapi (metadata) format** with a `package.xml`. `DataMaskPolicy` has **no
46
+ source-format SDR registry entry**, so a `--source-dir` deploy fails with *"Could not infer a
47
+ metadata type"*. Lay the file out as `dataMaskPolicies/Contact_PII_Mask.dataMaskPolicy` alongside
48
+ a `package.xml`:
49
+
50
+ ```xml
51
+ <?xml version="1.0" encoding="UTF-8"?>
52
+ <Package xmlns="http://soap.sforce.com/2006/04/metadata">
53
+ <types>
54
+ <members>Contact_PII_Mask</members>
55
+ <name>DataMaskPolicy</name>
56
+ </types>
57
+ <version>67.0</version>
58
+ </Package>
59
+ ```
60
+
61
+ ```bash
62
+ sf project deploy start --metadata-dir <mdapi-dir> --target-org <alias>
63
+ ```
64
+
65
+ The developer name of the created policy is the metadata member name (`Contact_PII_Mask`). Resolve
66
+ its Id before step 2:
67
+ ```bash
68
+ sf data query --use-tooling-api --target-org <alias> \
69
+ --query "SELECT Id FROM DataMaskPolicy WHERE DeveloperName = 'Contact_PII_Mask'"
70
+ ```
71
+
72
+ ## Step 2 — Tooling-insert the object + field membership
73
+
74
+ Insert one `DataMaskPolicyObject` per target object, then one `DataMaskPolicyField` per masked
75
+ field against that object's Id.
76
+
77
+ ```bash
78
+ # One object row
79
+ sf data create record --use-tooling-api --sobject DataMaskPolicyObject --target-org <alias> \
80
+ --values "ParentPolicyId=<policyId> ObjectReference=Contact FilterEnabled=false RunInSerialMode=false"
81
+ # → returns the DataMaskPolicyObject Id, use it as <objectId> below
82
+
83
+ # One field row per masked field
84
+ sf data create record --use-tooling-api --sobject DataMaskPolicyField --target-org <alias> \
85
+ --values "ParentPolicyObjectId=<objectId> FieldReference=FirstName MaskingCategory=library MaskValue=first_name"
86
+ ```
87
+
88
+ ### Field treatment columns
89
+
90
+ Each `DataMaskPolicyField` carries a `MaskingCategory` plus a `MaskValue` — there is **no
91
+ `MaskingRuleType` column** (older docs claiming values like `RandomEmail` / `RandomPhoneNumber`
92
+ were wrong; that column does not exist).
93
+
94
+ - `MaskingCategory` — `library` (pick a value from the built-in library, the common case) or
95
+ `replaceRandom` (random replacement).
96
+ - `MaskValue` — a snake_case library token identifying the value set. Verified tokens include:
97
+ `first_name`, `last_name`, `email`, `phone`, `street`, `city`, `state`, `postal_code`,
98
+ `country`, `account_name`, `URL`.
99
+
100
+ ## Choosing treatments
101
+
102
+ Match the `MaskValue` to the field's semantics — do **not** apply one blanket replacement:
103
+
104
+ | Field | MaskingCategory | MaskValue |
105
+ |-------|-----------------|-----------|
106
+ | FirstName | `library` | `first_name` |
107
+ | LastName | `library` | `last_name` |
108
+ | Email | `library` | `email` |
109
+ | Phone / MobilePhone | `library` | `phone` |
110
+ | MailingStreet | `library` | `street` |
111
+ | MailingCity | `library` | `city` |
112
+ | MailingState | `library` | `state` |
113
+ | MailingPostalCode | `library` | `postal_code` |
114
+ | MailingCountry | `library` | `country` |
115
+
116
+ Do not mask system fields (`Id`, `CreatedDate`, `OwnerId`, etc.).
117
+
118
+ ## Editing membership
119
+
120
+ To **add** a field, insert another `DataMaskPolicyField` row. To **remove** one, delete its row:
121
+ ```bash
122
+ sf data delete record --use-tooling-api --sobject DataMaskPolicyField \
123
+ --record-id <fieldRowId> --target-org <alias>
124
+ ```
125
+ The parent `DataMaskPolicy` shell does not need to be redeployed to change membership.
126
+
127
+ ## Masking only a subset of records (a "sample" run)
128
+
129
+ There is **no `sampleSize`** on `DataMaskPolicy`, and **there is no `LIMIT`** — Data Mask has no
130
+ row-cap concept. To process only a subset of an object's rows, you write a **selective `WHERE`
131
+ predicate** that genuinely matches fewer rows, on the **`DataMaskPolicyObject`** row (not on the
132
+ policy). The masked count equals however many rows satisfy that predicate — so the predicate itself
133
+ *is* the subset. Three columns control it:
134
+
135
+ | Column | Type | Purpose |
136
+ |--------|------|---------|
137
+ | `FilterEnabled` | boolean | `true` turns the row filter on (default `false` = mask the whole object) |
138
+ | `WhereCriteria` | string, **max 40 chars** | A SOQL-style predicate — the human-readable form, e.g. `LastName LIKE '%son%'` |
139
+ | `RawFilterData` | textarea (JSON) | The **structured** form the engine actually executes (see the op list below) |
140
+
141
+ > **The engine masks exactly the rows the predicate matches — there is no `LIMIT`.** A `LIMIT`
142
+ > clause in `WhereCriteria` is silently ignored: `LastName != 'X' LIMIT 20` masked **all 407**
143
+ > Contacts, because `LastName != 'X'` matches every row and the `LIMIT` did nothing. To mask a
144
+ > subset you must pick a predicate that is **actually selective** — e.g. `LastName LIKE '%son%'`
145
+ > (25 rows) or `MailingState = 'NY'` (3 rows), verified with a `SELECT COUNT()` first. An
146
+ > always-true predicate is **not** a subset.
147
+ >
148
+ > **`RawFilterData` is what runs — its `operation` must be one of the engine's supported ops:**
149
+ > `eq`, `ne`, `lt`, `gt`, `ge`, `le`, `contains`, `not_contains`, `in`, `not_in`. Anything else
150
+ > (e.g. `startsWith`) fails the run with a `422` `literal_error`. `LIKE '%son%'` maps to
151
+ > `{"operation":"contains","value":"son"}`; `= 'NY'` maps to `{"operation":"eq","value":"NY"}`.
152
+ > `WhereCriteria` and `RawFilterData` must express the **same** predicate.
153
+ >
154
+ > **The predicate must also be valid SOQL** (Data Mask runs a planning `SELECT count() FROM <object>
155
+ > WHERE <predicate>` from it). **Do NOT filter on `Id`.** `Id != 'null'` fails with `invalid ID
156
+ > field: null` / `INVALID_QUERY_FILTER_OPERATOR` and makes the whole **job fail** with 0 records
157
+ > masked. Filter on a text field (`LastName`, `MailingState`, …), not `Id`.
158
+
159
+ Set them when you insert (or update) the object row. Example — mask the Contacts whose last name
160
+ contains `son` (a real subset; verify the count with `SELECT count() FROM Contact WHERE LastName
161
+ LIKE '%son%'` first). `RawFilterData` mirrors it with `operation: contains`:
162
+ ```bash
163
+ sf data create record --use-tooling-api --sobject DataMaskPolicyObject --target-org <alias> \
164
+ --values "ParentPolicyId=<policyId> ObjectReference=Contact RunInSerialMode=false \
165
+ FilterEnabled=true WhereCriteria=\"LastName LIKE '%son%'\" \
166
+ RawFilterData={\"type\":\"and\",\"filters\":[{\"type\":\"field_filter\",\"field\":\"LastName\",\"operation\":\"contains\",\"value\":\"son\"}]}"
167
+ ```
168
+ To turn an existing object row into a subset run, update it instead:
169
+ ```bash
170
+ sf data update record --use-tooling-api --sobject DataMaskPolicyObject --record-id <objectId> \
171
+ --target-org <alias> --values "FilterEnabled=true WhereCriteria=\"LastName LIKE '%son%'\" \
172
+ RawFilterData={\"type\":\"and\",\"filters\":[{\"type\":\"field_filter\",\"field\":\"LastName\",\"operation\":\"contains\",\"value\":\"son\"}]}"
173
+ ```
174
+ After the run, confirm the masked count in `DataMaskPolicyJobRunDtl` matches the predicate's row
175
+ count and is **less than** `SELECT COUNT() FROM Contact` — proving it masked a subset, not the whole
176
+ table. `WhereCriteria` is only **40 characters**, so keep the predicate short.
177
+
178
+ ## Verifying the policy after creation
179
+ ```bash
180
+ # Confirm the object + field membership landed
181
+ sf data query --use-tooling-api --target-org <alias> \
182
+ --query "SELECT Id, FieldReference, MaskingCategory, MaskValue FROM DataMaskPolicyField \
183
+ WHERE ParentPolicyObjectId = '<objectId>'"
184
+ ```
185
+ Use the policy `Id` as `<policyId>` in the run/abort sequence (`run-and-abort.md`).
@@ -0,0 +1,116 @@
1
+ # Run / Poll / Report — and Cancel
2
+
3
+ The operational commands for a Data Mask run. Assumes a policy already exists (see
4
+ `policy-authoring.md` to create one) and the target org is a **sandbox**.
5
+
6
+ These map to the two workflows in `SKILL.md`: **Workflow A (mask & report)** uses steps 0–4;
7
+ **Workflow B (cancel a run)** uses steps 5–7 and is run only when the request is to abort/cancel.
8
+ Pick one by the request — a mask-and-report task does not include a cancel.
9
+
10
+ Throughout, `<alias>` is the target org, `<policyId>` the `DataMaskPolicy.Id`, `<jobRunId>` the id
11
+ returned by the run-start call.
12
+
13
+ ## 0. Confirm sandbox + context
14
+ ```bash
15
+ sf org display --target-org <alias> --json
16
+ ```
17
+ Check `result.isSandbox === true`. Note `result.instanceUrl`. Data Mask run endpoints return `403`
18
+ on production.
19
+
20
+ ## 1. Identify the policy
21
+ ```bash
22
+ sf data query --use-tooling-api --target-org <alias> \
23
+ --query "SELECT Id, DeveloperName, MasterLabel FROM DataMaskPolicy"
24
+ ```
25
+ Pick the policy that targets the PII you intend to mask (or create one — see `policy-authoring.md`).
26
+
27
+ ## 2. Start the run (REST run API)
28
+ ```bash
29
+ printf '{}' > ./empty-body.json
30
+ sf api request rest \
31
+ "/services/data/v67.0/platform/data-resilience/data-mask/policies/<policyId>/run" \
32
+ --method POST --body @./empty-body.json --target-org <alias>
33
+ ```
34
+ `--body` must point at a file containing `{}` **with an `@` prefix** (`--body @./empty-body.json`) —
35
+ `sf api request rest` requires a body on POST even though this endpoint takes no payload, and without
36
+ the `@` the literal path string is sent as the body (→ `JSON_PARSER_ERROR`). Version must be **`v67.0`+** and there is **no `/connect/`
37
+ segment** (either mistake returns `NOT_FOUND`). Sample response (HTTP **200**):
38
+ ```json
39
+ { "jobRunId": "1aG...", "policyId": "8dm...", "status": "RUNNING", "message": "Job started successfully" }
40
+ ```
41
+ Capture `jobRunId`. A `409`/`CONFLICT` means a run is already active for this policy.
42
+
43
+ > **Status case differs by surface.** The run API returns **UPPERCASE** status strings
44
+ > (`RUNNING`, `CANCELED`), while the `DataMaskPolicyJobRun.Status` SOQL picklist is **lowercase**
45
+ > (`running`, `canceled`). Poll for terminal state against the **SOQL** value (lowercase) — that is
46
+ > what `scripts/poll-job.sh` checks. `DataMaskPolicy` Ids carry the `8dm` prefix.
47
+
48
+ ## 3. Poll to terminal
49
+ ```bash
50
+ sf data query --target-org <alias> \
51
+ --query "SELECT Id, Status, Type, TotalRecords FROM DataMaskPolicyJobRun WHERE Id = '<jobRunId>'"
52
+ ```
53
+ Repeat on a bounded interval until `Status` is one of `completed`, `completed_with_errors`,
54
+ `failed`. `scheduled`/`running` are NOT terminal. `scripts/poll-job.sh` automates this with a
55
+ timeout.
56
+
57
+ ## 4. Report from the detail object
58
+ Per-object masked results live on the child, not the parent:
59
+ ```bash
60
+ sf data query --target-org <alias> \
61
+ --query "SELECT Id, DataMaskPolicyJobRunId, Status FROM DataMaskPolicyJobRunDtl WHERE DataMaskPolicyJobRunId = '<jobRunId>'"
62
+ ```
63
+ Report the concrete masked-record count and per-object success/failure from these rows. Do not
64
+ report a number you did not read from here.
65
+
66
+ ## 5. Abort the currently-running job — ONLY when the user asked to cancel
67
+ Steps 5–7 are the **abort** flow. Run them **only when the prompt explicitly asks to cancel/abort**
68
+ a run — a "create and run" or "edit and run" task is complete after step 4.
69
+
70
+ Aborting is an **on-demand action against a job that is already in progress**: take that job's
71
+ `jobRunId` (and note its `DataMaskPolicyId` — you need it to start a replacement run if the window is
72
+ missed), wait for `DataMaskPolicyJobRun.Status = running`, and go straight to the abort (step 6).
73
+ You can only abort a running job. Do **not** use the terminal poll from step 3 here — it waits until
74
+ the job is *done*. Use the poller's `running` mode, which returns the instant the job is abortable:
75
+ ```bash
76
+ POLL_MODE=running bash scripts/poll-job.sh <alias> <jobRunId> 900 15
77
+ ```
78
+ - Exit `0` (prints `running`) → abort now (step 6).
79
+ - Exit `3` → the job reached a terminal state before `running` was caught; the window is gone. Start
80
+ a fresh run (step 2, using the policy Id you noted) and poll the new `jobRunId`.
81
+ - Exit `1` (timeout) → re-query the job. If still non-terminal, re-run the poller once more; if
82
+ `running`, abort; if terminal, treat as exit 3 (fresh run + poll).
83
+
84
+ **Only if no job is currently running** (the one you were watching already finished, or you're
85
+ reproducing the run→abort flow end-to-end) start a fresh run (repeat step 2) to have a live job to
86
+ cancel, poll (`POLL_MODE=running`) until `Status = running`, then abort — targeting that live run,
87
+ never an older already-terminal one.
88
+
89
+ ## 6. Abort (REST run API)
90
+ ```bash
91
+ sf api request rest \
92
+ "/services/data/v67.0/platform/data-resilience/data-mask/jobs/<jobRunId>/abort" \
93
+ --method POST --body @./empty-body.json --target-org <alias>
94
+ ```
95
+ - `200` → accepted (response `status: "CANCELED"`, `message: "Job abort requested"`). `409` → not in `running` state.
96
+ - Do **not** abort by deleting/updating the `DataMaskPolicyJobRun` row via DML — that is not a real
97
+ cancellation.
98
+
99
+ ## 7. Confirm the abort
100
+ Cancellation is asynchronous. Re-query until the status settles:
101
+ ```bash
102
+ sf data query --target-org <alias> \
103
+ --query "SELECT Id, Status FROM DataMaskPolicyJobRun WHERE Id = '<jobRunId>'"
104
+ ```
105
+ Only report the abort as successful once `Status = canceled`.
106
+
107
+ ## Failure decision table
108
+
109
+ | Symptom | Meaning | Action |
110
+ |---------|---------|--------|
111
+ | run-start `403` | Production org | Data Mask is sandbox-only; switch to a sandbox |
112
+ | run-start `409` | Run already active | Poll the existing run or wait |
113
+ | abort `409` | Job not `running` | Re-check status; may already be terminal or still `scheduled` |
114
+ | abort `200`, SOQL status still `running` | Async cancel in flight | Keep polling until `canceled` |
115
+ | run/abort `NOT_FOUND` | Wrong path — `/connect/` segment present or version < v67 | Use `/services/data/v67.0/platform/data-resilience/data-mask/...` (no `connect`) |
116
+ | status stuck `scheduled` | Job hasn't picked up yet | Keep polling within the cap; not an error yet |
@@ -0,0 +1,115 @@
1
+ #!/usr/bin/env bash
2
+ # poll-job.sh — poll a DataMaskPolicyJobRun, with a bounded timeout. Two modes:
3
+ # terminal (default) — wait until the job REACHES a terminal state (used by Workflow A, run→report)
4
+ # running — wait until the job ENTERS the `running` state (used by Workflow B, abort)
5
+ #
6
+ # DataMaskPolicyJobRun is a STANDARD data-API object (unlike DataMaskPolicy, which is
7
+ # Tooling/MDAPI), so plain `sf data query` works here.
8
+ #
9
+ # Usage:
10
+ # poll-job.sh <orgAlias> <jobRunId> [maxSeconds] [intervalSeconds] # terminal mode (default)
11
+ # POLL_MODE=running poll-job.sh <orgAlias> <jobRunId> [maxSeconds] [intervalSeconds]
12
+ # Defaults: maxSeconds=600, intervalSeconds=20
13
+ #
14
+ # WHY THESE DEFAULTS: Data Mask jobs run on a backend pool/scheduler with a ~5-10 minute floor —
15
+ # even a tiny (20-row) job typically does not reach a terminal state or emit detail rows for several
16
+ # minutes after the run starts. A short cap (e.g. 180s) gives up BEFORE the job can possibly finish,
17
+ # so the default cap is 600s (10 min) with a 20s interval to keep the query volume low. Do NOT poll
18
+ # on a tight (sub-10s) interval — it just burns tool calls against a job that cannot finish sooner.
19
+ #
20
+ # TERMINAL MODE: The parent DataMaskPolicyJobRun.Status can LAG the real job state (it may read
21
+ # `pending`/`running` for a while after masking finished). So this poller ALSO checks
22
+ # DataMaskPolicyJobRunDtl: once a `total_records_masked` detail row exists, the job is effectively
23
+ # done and we stop, even if the parent status hasn't caught up. Ground truth is the detail rows.
24
+ #
25
+ # RUNNING MODE (for abort): exits the instant the parent status reads `running` — the only state in
26
+ # which the abort endpoint accepts the request (a `pending`/`scheduled` job 409s). Because the pool
27
+ # floor delays the `running` window by minutes, poll with a modest interval (e.g. 15s). If the job
28
+ # races past `running` straight to a terminal state before we catch it, that window is gone and the
29
+ # abort would be pointless — the poller reports the terminal status and exits 3 so the caller can
30
+ # start a fresh run rather than abort a job that already finished.
31
+ #
32
+ # Exit codes:
33
+ # terminal mode: 0 = terminal status OR masked-count detail row observed (signal on stdout);
34
+ # 1 = timed out; 2 = bad args.
35
+ # running mode: 0 = `running` observed (prints `running`); 1 = timed out; 2 = bad args;
36
+ # 3 = job reached a terminal state before `running` was seen (prints that status).
37
+
38
+ set -euo pipefail
39
+
40
+ ORG="${1:-}"
41
+ JOB_ID="${2:-}"
42
+ MAX_SECONDS="${3:-600}"
43
+ INTERVAL="${4:-20}"
44
+ POLL_MODE="${POLL_MODE:-terminal}"
45
+
46
+ if [[ -z "$ORG" || -z "$JOB_ID" ]]; then
47
+ echo "usage: [POLL_MODE=running] poll-job.sh <orgAlias> <jobRunId> [maxSeconds] [intervalSeconds]" >&2
48
+ exit 2
49
+ fi
50
+
51
+ case "$POLL_MODE" in
52
+ terminal|running) ;;
53
+ *) echo "poll-job: POLL_MODE must be 'terminal' or 'running' (got '${POLL_MODE}')" >&2; exit 2 ;;
54
+ esac
55
+
56
+ # Terminal statuses (incl. canceled after an abort). scheduled/running/pending are NOT terminal.
57
+ is_terminal() {
58
+ case "$1" in
59
+ completed|completed_with_errors|failed|canceled) return 0 ;;
60
+ *) return 1 ;;
61
+ esac
62
+ }
63
+
64
+ elapsed=0
65
+ while (( elapsed < MAX_SECONDS )); do
66
+ status="$(
67
+ sf data query --target-org "$ORG" \
68
+ --query "SELECT Status FROM DataMaskPolicyJobRun WHERE Id = '${JOB_ID}'" \
69
+ --json 2>/dev/null \
70
+ | python3 -c "import json,sys; r=json.load(sys.stdin).get('result',{}).get('records',[]); print(r[0]['Status'] if r else 'UNKNOWN')"
71
+ )"
72
+ echo "[poll-job] ${JOB_ID} status=${status} (${elapsed}s/${MAX_SECONDS}s) mode=${POLL_MODE}"
73
+
74
+ if [[ "$POLL_MODE" == "running" ]]; then
75
+ # Abort window: stop the instant the job is `running` (the only abortable state).
76
+ if [[ "$status" == "running" ]]; then
77
+ echo "running"
78
+ exit 0
79
+ fi
80
+ # If it slipped past `running` to a terminal state, the abort window is gone — signal the caller.
81
+ if is_terminal "$status"; then
82
+ echo "[poll-job] job reached terminal '${status}' before 'running' was observed — abort window missed" >&2
83
+ echo "$status"
84
+ exit 3
85
+ fi
86
+ else
87
+ if is_terminal "$status"; then
88
+ echo "$status"
89
+ exit 0
90
+ fi
91
+
92
+ # Ground-truth check: a masked-count detail row means the job finished even if the parent lags.
93
+ masked="$(
94
+ sf data query --target-org "$ORG" \
95
+ --query "SELECT Value FROM DataMaskPolicyJobRunDtl WHERE DataMaskPolicyJobRunId = '${JOB_ID}' AND Subtype = 'total_records_masked'" \
96
+ --json 2>/dev/null \
97
+ | python3 -c "import json,sys; r=json.load(sys.stdin).get('result',{}).get('records',[]); print(r[0]['Value'] if r else '')"
98
+ )"
99
+ if [[ -n "$masked" ]]; then
100
+ echo "[poll-job] detail row total_records_masked=${masked} — job effectively complete (parent status=${status})"
101
+ echo "completed"
102
+ exit 0
103
+ fi
104
+ fi
105
+
106
+ sleep "$INTERVAL"
107
+ elapsed=$(( elapsed + INTERVAL ))
108
+ done
109
+
110
+ if [[ "$POLL_MODE" == "running" ]]; then
111
+ echo "[poll-job] TIMEOUT after ${MAX_SECONDS}s — job never reached 'running'" >&2
112
+ else
113
+ echo "[poll-job] TIMEOUT after ${MAX_SECONDS}s — neither terminal status nor masked-count detail row observed" >&2
114
+ fi
115
+ exit 1
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: platform-dataspace-access-configure
3
- description: "Use this skill to configure Salesforce Data Cloud DataSpace access for permission sets. Grants dataspace-level access via MDAPI PermissionSet XML with dataspaceScopes elements, and optionally grants object-level access to specific DMO, DLO, or CIO objects via the Object Access Grants Connect API. TRIGGER when: user needs to create or update a permission set that includes DataSpace access, grant a permission set access to a specific dataspace, configure dataAccessLevel or objectAccessLevel for a dataspace scope, add RBAC object access grants for Data Cloud objects, or list or remove object access grants for a permission set and DataSpace pair. DO NOT TRIGGER when: the task is a generic permission set without any dataspace access (use platform-permission-set-generate), the request is about data ingestion or streams (use data360-prepare), or the work involves creating dataspaces themselves rather than granting access to them."
3
+ description: "Use this skill to configure or inspect Salesforce Data Cloud DataSpace access for permission sets. Grants dataspace-level access via MDAPI PermissionSet XML with dataspaceScopes elements, optionally grants object-level access to DMO, DLO, or CIO objects via the Object Access Grants Connect API, and inspects existing scopes via read-only PermissionSet metadata retrieval. TRIGGER when: user needs to create or update a permission set with DataSpace access, grant access to a specific dataspace, list permission sets with access to a DataSpace, configure dataAccessLevel/objectAccessLevel, add RBAC object access grants, or list/remove object access grants for a permission set + DataSpace pair. DO NOT TRIGGER when: the task is a generic permission set without dataspace access (use platform-permission-set-generate), the request is about data ingestion/streams (use data360-prepare), or the work involves creating dataspaces themselves rather than granting access to them."
4
4
  metadata:
5
5
  version: "1.0"
6
6
  domains: ["Platform", "Data 360"]
@@ -8,6 +8,8 @@ metadata:
8
8
  cliTools:
9
9
  - tool: ["jq"]
10
10
  semver: ">=1.6.0"
11
+ - tool: ["python3"]
12
+ semver: ">=3.6"
11
13
  - tool: ["sf"]
12
14
  semver: ">=2.0.0"
13
15
  ---
@@ -32,6 +34,7 @@ Pick exactly one case from the table below before writing any files. Each case h
32
34
  | **A. Create new permset with DS access** | "create a permission set called X with dataspace scope Y" | does NOT exist yet | `permissionsets/<Name>.permissionset-meta.xml` **and** `package.xml` |
33
35
  | **B. Add DS access to existing permset** | "grant existing permission set X access to dataspace Y" | already deployed (may contain other permissions) | patched `permissionsets/<Name>.permissionset-meta.xml` **and** `package.xml` — see Case B workflow below |
34
36
  | **C. Object-level grant only** | "grant permset X access to object Z (in dataspace Y)" — permset + scope already configured | already deployed with `dataspaceScopes` | `api-request.json` (Connect API body). NO permission set XML, NO `package.xml` |
37
+ | **D. Inspect existing DS access** | "which permission sets have access to dataspace Y?" | any | chat/report only. NO deployable files, NO runtime API mutation |
35
38
 
36
39
  Only emit the files listed for the case you picked. Emitting Case A/B files for a Case C prompt (or vice versa) is a correctness failure — extra files change the deployment shape.
37
40
 
@@ -44,7 +47,7 @@ Only emit the files listed for the case you picked. Emitting Case A/B files for
44
47
  sf project retrieve start --metadata PermissionSet:<Name> --target-org <alias>
45
48
  ```
46
49
  2. Open the retrieved `permissionsets/<Name>.permissionset-meta.xml`. Keep every element already there.
47
- 3. Insert the `<dataspaceScopes>` block for the target DataSpace (element order in the file does not matter for MDAPI). If the file already has a `<dataspaceScopes>` block **for this same DataSpace**, replace only that block. Leave every `<dataspaceScopes>` block for other DataSpaces untouched — one block per DataSpace, and removing a block revokes that DataSpace grant.
50
+ 3. Insert the `<dataspaceScopes>` block for the target DataSpace (element order in the file does not matter for MDAPI). If the file already has a `<dataspaceScopes>` block **for this same DataSpace**, replace only that block. Leave every `<dataspaceScopes>` block for other DataSpaces untouched — one block per DataSpace. For a requested scope removal, remove only the matching block and deploy; omitting the block revokes that DataSpace grant. Verify by retrieving the PermissionSet and confirming the matching `<dataspaceScopes>` block is absent.
48
51
  4. Write `package.xml` listing the permset in `<members>`.
49
52
  5. Redeploy with `sf project deploy start`.
50
53
 
@@ -56,6 +59,7 @@ Trigger this skill when the user wants to:
56
59
  - Create a permission set that grants access to a Data Cloud DataSpace
57
60
  - Add or modify `dataspaceScopes` on an existing permission set
58
61
  - Grant a permission set access to specific DMO / DLO / CIO objects in a DataSpace
62
+ - List which permission sets have a DataSpace scope and inspect its access levels
59
63
  - Configure `dataAccessLevel` and `objectAccessLevel` for a DataSpace scope
60
64
  - List or remove object access grants for a permission set + DataSpace pair
61
65
 
@@ -131,6 +135,50 @@ sf project deploy start --source-dir force-app/main/default/permissionsets/ --ta
131
135
 
132
136
  ---
133
137
 
138
+ ## Read-Only DataSpace Scope Inspection — Case D
139
+
140
+ Use Case D when the user asks which permission sets have access to a DataSpace,
141
+ or asks to inspect `dataAccessLevel` and `objectAccessLevel` without making a
142
+ change.
143
+
144
+ **Never query `DataspaceScope` or `DataspaceScopeAccess` with SOQL.** Those
145
+ objects are not a supported query surface for this relationship. Do not try
146
+ SOQL as discovery, fallback, or troubleshooting.
147
+
148
+ 1. Retrieve PermissionSet metadata into an isolated temporary local project and
149
+ output directory. Do not retrieve into the user's existing metadata tree. Save
150
+ the skill repository path first (as an absolute path if possible):
151
+ ```bash
152
+ REPO_ROOT="<absolute/path/to/sf-skills-internal>" # or $(pwd) if you're in the repo root
153
+ WORK_DIR=$(mktemp -d)
154
+ sf project generate --name dataspace-scope-inspection --output-dir "$WORK_DIR"
155
+ cd "$WORK_DIR/dataspace-scope-inspection"
156
+ sf project retrieve start --json \
157
+ --metadata "PermissionSet:*" --target-org <alias> \
158
+ --output-dir "$WORK_DIR/retrieved"
159
+ ```
160
+ Inspect the JSON result before continuing. A nonzero command status, a failed
161
+ `result.status`, warnings, or an unexpectedly low `result.fileProperties`
162
+ count means the retrieve may be incomplete. Report that limitation rather
163
+ than treating the result as empty, and do not fall back to SOQL.
164
+ 2. Run the inspection script from the skill directory (do not rely on relative paths
165
+ after `cd` changed the working directory). Supply the retrieved directory and optional DataSpace name:
166
+ ```bash
167
+ "$REPO_ROOT/skills/platform-dataspace-access-configure/scripts/inspect-dataspace-scopes.sh" \
168
+ "$WORK_DIR/retrieved" "<DataSpace API name, if given>"
169
+ ```
170
+ Report the output to the user, one result per line.
171
+
172
+ Case D is read-only with respect to the org. Do not deploy metadata, assign a
173
+ permission set, execute Apex, perform record DML, or make a POST, PUT, PATCH, or
174
+ DELETE request. Do not generate `package.xml`, PermissionSet XML, or
175
+ `api-request.json` in the user's workspace as part of inspection. Remove the
176
+ temporary work directory after reporting: `rm -rf "$WORK_DIR"`. The scope-level
177
+ `objectAccessLevel` is not an inventory of explicit object grants; inspect those
178
+ separately with the read-only object-access-grants endpoint only when requested.
179
+
180
+ ---
181
+
134
182
  ## Layer 2 — Object-Level Access (Connect API) — Case C
135
183
 
136
184
  Only needed when `objectAccessLevel` is not `BY_POLICY`, or when governance policies do not cover the target objects. Grants are runtime — **no MDAPI deploy, no `package.xml`, no permission set XML**. The only artifact for a Case C task is a single `api-request.json` describing the Connect API call.
@@ -304,7 +352,7 @@ sf org api rest --target-org <alias> \
304
352
  | Prefer `BY_POLICY` when data governance policies exist | Delegates row/column filtering to central policy — no per-object grants needed |
305
353
  | One `<dataspaceScopes>` block per DataSpace | Repeat the block for multiple DataSpaces on the same permission set |
306
354
  | Org must have Data Cloud provisioned to deploy `<dataspaceScopes>` | On non-Data-Cloud orgs, the element is ignored or rejected |
307
- | Do not query `DataspaceScope` / `DataspaceScopeAccess` via SOQL | Not queryable; use MDAPI retrieve to inspect existing grants |
355
+ | Do not query `DataspaceScope` / `DataspaceScopeAccess` via SOQL | Not queryable; use Case D PermissionSet Metadata API retrieval to inspect existing scopes. Never use SOQL as a fallback. |
308
356
 
309
357
  ---
310
358