@salesforce/afv-skills 1.44.0 → 1.45.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 (97) hide show
  1. package/package.json +1 -1
  2. package/skills/consumer-goods-rtr-datacloud-export-configure/SKILL.md +72 -0
  3. package/skills/consumer-goods-rtr-datacloud-export-configure/references/inputs-and-namespace.md +38 -0
  4. package/skills/consumer-goods-rtr-datacloud-export-configure/references/procedure.md +158 -0
  5. package/skills/consumer-goods-rtr-datacloud-export-configure/scripts/detect-namespace.js +86 -0
  6. package/skills/consumer-goods-rtr-datacloud-export-configure/scripts/render-apex.js +64 -0
  7. package/skills/consumer-goods-rtr-datacloud-export-configure/scripts/resolve-id-by-name.js +47 -0
  8. package/skills/consumer-goods-rtr-datacloud-export-configure/scripts/sf-rest.js +171 -0
  9. package/skills/consumer-goods-rtr-datacloud-export-configure/scripts/soql-escape.js +26 -0
  10. package/skills/consumer-goods-rtr-datacloud-export-configure/scripts/upsert-report-config.apex +51 -0
  11. package/skills/consumer-goods-rtr-datacloud-export-configure/scripts/upsert-system-setting.apex +21 -0
  12. package/skills/consumer-goods-tpe-dashboard-configure/SKILL.md +74 -0
  13. package/skills/consumer-goods-tpe-dashboard-configure/references/phases-1-6.md +112 -0
  14. package/skills/consumer-goods-tpe-dashboard-configure/references/phases-7-12.md +157 -0
  15. package/skills/consumer-goods-tpe-dashboard-configure/scripts/find-failure-reason.js +132 -0
  16. package/skills/consumer-goods-tpe-dashboard-configure/scripts/poll-status.js +116 -0
  17. package/skills/consumer-goods-tpe-dashboard-configure/scripts/render-apex.js +64 -0
  18. package/skills/consumer-goods-tpe-dashboard-configure/scripts/run-data-transform.js +121 -0
  19. package/skills/consumer-goods-tpe-dashboard-configure/scripts/schedule-business-period-export.apex +27 -0
  20. package/skills/consumer-goods-tpe-dashboard-configure/scripts/sf-rest.js +171 -0
  21. package/skills/consumer-goods-tpe-dashboard-configure/scripts/soql-escape.js +25 -0
  22. package/skills/consumer-goods-tpe-dashboard-custom-kpi-configure/SKILL.md +141 -0
  23. package/skills/consumer-goods-tpe-dashboard-custom-kpi-configure/references/payload-shapes.md +447 -0
  24. package/skills/consumer-goods-tpe-dashboard-custom-kpi-configure/references/procedure.md +263 -0
  25. package/skills/consumer-goods-tpe-dashboard-custom-kpi-configure/scripts/clone-tpe-dashboards.js +537 -0
  26. package/skills/consumer-goods-tpe-dashboard-custom-kpi-configure/scripts/sf-rest.js +195 -0
  27. package/skills/consumer-goods-tpe-datakit-deploy/SKILL.md +157 -0
  28. package/skills/consumer-goods-tpe-datakit-deploy/scripts/detect-namespace.js +86 -0
  29. package/skills/consumer-goods-tpe-datakit-deploy/scripts/download-static-resource.js +151 -0
  30. package/skills/consumer-goods-tpe-datakit-deploy/scripts/extract-crm-field-permissions.js +115 -0
  31. package/skills/consumer-goods-tpe-datakit-deploy/scripts/sf-rest.js +109 -0
  32. package/skills/consumer-goods-tpe-datakit-deploy/scripts/update-field-permissions.js +433 -0
  33. package/skills/service-catalog-template-coordinate/SKILL.md +263 -0
  34. package/skills/service-catalog-template-coordinate/examples/output-templates.md +44 -0
  35. package/skills/service-catalog-template-coordinate/references/mcp-invocation.md +183 -0
  36. package/skills/service-catalog-template-coordinate/references/operations.md +230 -0
  37. package/skills/service-itsm-agentic-setup-agent-runtime-access-assign/SKILL.md +243 -0
  38. package/skills/service-itsm-agentic-setup-agent-runtime-access-assign/references/cli-invocation.md +205 -0
  39. package/skills/service-itsm-agentic-setup-agent-runtime-access-assign/references/helper-contracts.md +236 -0
  40. package/skills/service-itsm-agentic-setup-agent-runtime-access-assign/references/permset-topology.md +132 -0
  41. package/skills/service-itsm-agentic-setup-agent-runtime-access-assign/scripts/classify-activated-agents.mjs +106 -0
  42. package/skills/service-itsm-agentic-setup-agent-runtime-access-assign/scripts/classify-agent-access-state.mjs +113 -0
  43. package/skills/service-itsm-agentic-setup-agent-runtime-access-assign/scripts/classify-assignment-state.mjs +99 -0
  44. package/skills/service-itsm-agentic-setup-agent-runtime-access-assign/scripts/classify-platform-permset-availability.mjs +155 -0
  45. package/skills/service-itsm-agentic-setup-agent-runtime-access-assign/scripts/gate-unified-catalog-tiers.mjs +100 -0
  46. package/skills/service-itsm-agentic-setup-agent-runtime-access-assign/scripts/rank-candidate-users.mjs +95 -0
  47. package/skills/service-itsm-agentic-setup-agent-runtime-access-assign/scripts/resolve-target-user.mjs +86 -0
  48. package/skills/service-itsm-agentic-setup-agentforce-coordinate/SKILL.md +44 -23
  49. package/skills/service-itsm-agentic-setup-agentforce-coordinate/examples/output-templates.md +33 -9
  50. package/skills/service-itsm-agentic-setup-agentforce-studio-configure/SKILL.md +17 -18
  51. package/skills/service-itsm-agentic-setup-cmdb-coordinate/SKILL.md +3 -1
  52. package/skills/service-itsm-agentic-setup-configure/SKILL.md +20 -12
  53. package/skills/service-itsm-agentic-setup-configure/examples/output-templates.md +73 -5
  54. package/skills/service-itsm-agentic-setup-employee-agent-configure/SKILL.md +8 -7
  55. package/skills/service-itsm-agentic-setup-employee-agent-configure/references/cli-invocation.md +45 -33
  56. package/skills/service-itsm-agentic-setup-employee-agent-configure/references/reactivation.md +8 -6
  57. package/skills/service-itsm-agentic-setup-employee-agent-configure/references/workflow-detail.md +10 -10
  58. package/skills/service-itsm-agentic-setup-employee-agent-configure/scripts/classify-agent-existence.mjs +114 -56
  59. package/skills/service-itsm-agentic-setup-employee-agent-configure/scripts/classify-preflight.mjs +33 -17
  60. package/skills/service-itsm-agentic-setup-employee-agent-configure/scripts/render-report.mjs +9 -3
  61. package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/SKILL.md +8 -7
  62. package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/references/cli-invocation.md +43 -32
  63. package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/references/reactivation.md +6 -4
  64. package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/references/workflow-detail.md +10 -10
  65. package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/scripts/classify-agent-existence.mjs +106 -55
  66. package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/scripts/classify-preflight.mjs +27 -13
  67. package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/scripts/render-report.mjs +9 -3
  68. package/skills/service-itsm-agentic-setup-incident-sla-configure/SKILL.md +159 -161
  69. package/skills/service-itsm-agentic-setup-incident-sla-configure/assets/attach-milestone-action.json +51 -0
  70. package/skills/service-itsm-agentic-setup-incident-sla-configure/assets/attach-milestone.json +1 -1
  71. package/skills/service-itsm-agentic-setup-incident-sla-configure/assets/predefined-incident-policy.json +120 -0
  72. package/skills/service-itsm-agentic-setup-incident-sla-configure/examples/milestone-patterns.md +28 -5
  73. package/skills/service-itsm-agentic-setup-incident-sla-configure/examples/output-templates.md +19 -1
  74. package/skills/service-itsm-agentic-setup-incident-sla-configure/references/mcp-invocation.md +350 -30
  75. package/skills/service-itsm-channels-coordinate/SKILL.md +80 -213
  76. package/skills/service-itsm-slack-itservice-configure/SKILL.md +363 -0
  77. package/skills/service-itsm-slack-itservice-configure/references/connect-agentforce-to-slack.md +159 -0
  78. package/skills/service-itsm-slack-itservice-configure/references/manage-slack-connection.md +88 -0
  79. package/skills/service-itsm-slack-itservice-configure/references/manage-user-access.md +117 -0
  80. package/skills/service-itsm-slack-itservice-configure/references/record-visibility.md +78 -0
  81. package/skills/service-itsm-slack-itservice-configure/references/site-membership-verification.md +126 -0
  82. package/skills/service-itsm-slack-itservice-configure/scripts/classify-user-access.mjs +167 -0
  83. package/skills/service-itsm-teams-configure/SKILL.md +50 -47
  84. package/skills/service-itsm-teams-configure/references/azure-credential-population.md +42 -28
  85. package/skills/service-itsm-teams-configure/references/gotchas.md +1 -2
  86. package/skills/service-itsm-teams-coordinate/SKILL.md +22 -18
  87. package/skills/service-itsm-teams-coordinate/examples/output-templates.md +12 -9
  88. package/skills/service-itsm-teams-itdesk-configure/SKILL.md +60 -44
  89. package/skills/service-itsm-teams-itservice-configure/SKILL.md +56 -70
  90. package/skills/service-catalog-template-deploy/SKILL.md +0 -310
  91. package/skills/service-catalog-template-deploy/references/cli-invocation.md +0 -258
  92. package/skills/service-catalog-template-deploy/scripts/activate-verify.mjs +0 -164
  93. package/skills/service-catalog-template-deploy/scripts/build-deploy-payload.mjs +0 -94
  94. package/skills/service-catalog-template-deploy/scripts/resolve-template.mjs +0 -331
  95. package/skills/service-catalog-template-search/SKILL.md +0 -212
  96. package/skills/service-catalog-template-search/references/cli-invocation.md +0 -128
  97. package/skills/service-catalog-template-search/scripts/classify-catalog.mjs +0 -205
@@ -0,0 +1,117 @@
1
+ # Manage User Access (Step 4) — assign Employee Slack Notifications
2
+
3
+ The Go page's **"Manage User Access"** step (Salesforce Go → *Slack for Employee Service* → *Set Up the
4
+ Basics* → **Manage**) centers on the permission set **`EmployeeSlackNotifications`** (label *"Employee
5
+ Slack Notifications"*) — *"gives employee users the ability to receive notifications on ticket updates
6
+ in Slack."* This skill assigns **all three core permsets** to each confirmed user —
7
+ `EmployeeSlackNotifications` **plus** `ItsmPortalUelUserPermset` (record visibility) **plus** an
8
+ API-Enabled permset (both detailed below) — never a notifications-only subset. Assign only to the
9
+ specific employee user(s) who will use Slack — **never all active users**.
10
+
11
+ > **Assign the right permset.** This employee-side step is **`EmployeeSlackNotifications`** (PSL
12
+ > `EmployeeSlackNotificationsPsl`), **not** `SlackServiceUser` (PSL `SlackServiceUserPsl`) — that's a
13
+ > separate fulfiller/service-side permset on a different path and does **not** satisfy this step. Query
14
+ > by exact `Name = 'EmployeeSlackNotifications'`.
15
+
16
+ > **The user's email must match their Slack user's email.** Assigning the permset only grants the
17
+ > *ability* to receive Slack notifications — it does **not** create the Salesforce↔Slack user link. That
18
+ > link is made by the connection's **account-mapping field** (default `Email`, chosen when the connection
19
+ > was requested/approved): the Salesforce `User.Email` must equal the email of that person's Slack
20
+ > account, otherwise they map to no Slack user and notifications never arrive. Warn the user and verify
21
+ > the match — a `null` `currentUserMapping.salesforceUserId` from `GET /connect/slackbridge/connections`
22
+ > means the current user isn't mapped yet. If the connection mapped on **SAML NameID** instead of Email,
23
+ > the same requirement applies to that identifier.
24
+
25
+ ## Pick the user(s)
26
+
27
+ Ask which user(s) should get access. If they want the list, page it — **≤10 users per page**, then ask
28
+ "want to see more?" before the next page:
29
+
30
+ ```text
31
+ mcp__headless-360__dispatch_readonly(
32
+ method: "GET", url: "/services/data/v67.0/query",
33
+ queryParams: { "q": "SELECT Id, Name, Username, Email FROM User WHERE IsActive = true ORDER BY Name LIMIT 10 OFFSET <page * 10>" }
34
+ )
35
+ ```
36
+
37
+ **Verified gotcha:** early pages are often dominated by non-employee system/integration accounts
38
+ (`Automated Process`, `Bot User`, `Insights Integration`, `*.ext`, `ESW_*` / `*Site Guest User`,
39
+ `DigitalAgent.*`). Do **not** classify rows by eye. Save the response to a file and let the helper
40
+ script tag each row `employee`/`system` deterministically **without dropping any row**:
41
+
42
+ ```text
43
+ node scripts/classify-user-access.mjs flag-users <captured-users.json>
44
+ ```
45
+
46
+ Prints `{users:[{id,name,username,email,kind}], employeeCount, systemCount}`. Present `employee` rows
47
+ first; surface `system`-tagged rows as "these look like system/integration accounts" rather than hiding.
48
+
49
+ ## Look up the permset Id and assign
50
+
51
+ ```text
52
+ mcp__headless-360__dispatch_readonly(
53
+ method: "GET", url: "/services/data/v67.0/query",
54
+ queryParams: { "q": "SELECT Id, Name, Label FROM PermissionSet WHERE Name = 'EmployeeSlackNotifications'" }
55
+ )
56
+ ```
57
+
58
+ Then assign to each confirmed user (one record per user):
59
+
60
+ ```text
61
+ mcp__headless-360__dispatch(
62
+ method: "POST", url: "/services/data/v67.0/sobjects/PermissionSetAssignment",
63
+ body: { "AssigneeId": "<user id>", "PermissionSetId": "<EmployeeSlackNotifications id>" }
64
+ )
65
+ ```
66
+
67
+ Do **not** read the POST body yourself to decide success. Save it and classify:
68
+
69
+ ```text
70
+ node scripts/classify-user-access.mjs classify-assignment <captured-response.json>
71
+ ```
72
+
73
+ Prints `{status, retryable, message}` where `status` is `success` / `wrong-license` / `other-error`;
74
+ `retryable` is always `false`. Relay the plain-language `message`; never surface the raw error code.
75
+ Optionally verify a `success`: `SELECT Id FROM PermissionSetAssignment WHERE AssigneeId = '<user id>'
76
+ AND PermissionSetId = '<permset id>'`.
77
+
78
+ > **License gotcha (`wrong-license`):** a Slack permission set can carry a permission-set license only
79
+ > some user licenses can hold. Assigning to an incompatible license fails `400 FIELD_INTEGRITY_EXCEPTION`
80
+ > (*"…user license doesn't support it"*). Observed live: a **Unified Employee** license user was rejected
81
+ > against a Slack PSL; a **Standard User** succeeded (`201`). `EmployeeSlackNotifications` is backed by
82
+ > `EmployeeSlackNotificationsPsl` — its cross-license compatibility wasn't separately verified, so treat
83
+ > any `wrong-license` as authoritative. It is **not** transient — pick a user whose license supports it
84
+ > (or add the PSL), don't retry.
85
+
86
+ > **API Enabled + agent access (assign here too).** Beyond notifications, this step also grants:
87
+ > **API Enabled** — needed for the Slack/employee app to call Connect APIs on the user's behalf. This is
88
+ > the *API Enabled* system permission carried by a permission set. **Reuse an existing API-Enabled permset
89
+ > if the org already has one** (discover it — query below — don't assume any particular one exists). If none
90
+ > exists, **create one** — a minimal `PermissionSet` with `PermissionsApiEnabled = true` (e.g. Name
91
+ > `Slack_ApiAccess`, label "Slack API Access"). Discover first, create only if none is found, then assign.
92
+ > Also — only for users who will reach the Agentforce agent in Slack — the **per-agent `Agent_Access`**
93
+ > permset carrying that agent's `BotDefinition` `SetupEntityAccess` grant. Discover `Agent_Access` by
94
+ > reverse-lookup on the BotDefinition and create it if absent (mechanics: `connect-agentforce-to-slack.md`);
95
+ > **never** assign the broad `Access_Agents` — it fails on an employee license (*"View Setup Audit Trail"*).
96
+ >
97
+ > **Discover an API-Enabled permset (reuse before create):**
98
+ > ```text
99
+ > mcp__headless-360__dispatch_readonly(
100
+ > method: "GET", url: "/services/data/v67.0/query",
101
+ > queryParams: { "q": "SELECT Id, Name, Label FROM PermissionSet WHERE PermissionsApiEnabled = true AND IsCustom = true" }
102
+ > )
103
+ > ```
104
+ > If a row comes back, reuse it — assign it. If none, create one via
105
+ > `POST /services/data/v67.0/sobjects/PermissionSet` with `{ "Name": "Slack_ApiAccess", "Label":
106
+ > "Slack API Access", "PermissionsApiEnabled": true }`, then assign the returned Id.
107
+
108
+ > **Record visibility — always assigned, one of the three core permsets.** For a UEL employee to actually
109
+ > see records (not just notifications) in the Slack Salesforce app, assign **`ItsmPortalUelUserPermset`**
110
+ > ("ITSM Portal Uel User") — it grants **read/create/edit on Incident, Service Request, and Case** (the
111
+ > self-service scope), alongside an API-Enabled permset. **Always assign it** (with the other two core
112
+ > permsets) to each confirmed user — do **not** offer a "notifications-only" scope; under-assigning
113
+ > silently leaves the employee unable to see records. This deliberately does **not** include **Problem** or
114
+ > **Change Request** — those are fulfiller/manager queues and their Slack lists staying empty ("Nothing
115
+ > turned up") is expected for a self-service employee, not a bug. If a specific user genuinely needs
116
+ > Problem/Change visibility, that is a separate fulfiller-side grant (e.g. `IncidentManager`), handled
117
+ > outside this self-service flow — do not assign it. Full mechanics: `references/record-visibility.md`.
@@ -0,0 +1,78 @@
1
+ # Record visibility — let a UEL employee view their Incident / Service Request / Case records in Slack
2
+
3
+ Record visibility is part of the baseline employee role — **always** grant it during Step 4 (it's one of
4
+ the three core permsets) so a Unified Employee (UEL) user can **view and work Incident, Service Request,
5
+ and Case records** (the self-service ITSM entities) in the Slack Salesforce app (e.g. in the Salesforce
6
+ **List views**). It goes beyond the literal Go-page "Manage User Access" click but is what makes the ITSM
7
+ experience actually usable in Slack — say so when you assign it.
8
+
9
+ > **Always assign — no scope choice.** Assign this to every confirmed user alongside the other two core
10
+ > permsets; do **not** offer a "notifications-only" option. Under-assigning silently leaves the employee
11
+ > able to receive notifications but unable to open the records those notifications point to.
12
+
13
+ > **Scope note — this is a self-service employee grant, not a fulfiller grant.** The employee gets
14
+ > **Incident + Service Request + Case** (read/create/edit) so they can log and track their own issues and
15
+ > raise service requests. It does **not** include **Problem** or **Change Request** — those are
16
+ > fulfiller/manager queues, out of scope for a self-service employee on Slack. If a specific user genuinely
17
+ > needs Problem/Change visibility, that is a separate fulfiller-side grant (e.g. `IncidentManager`), handled
18
+ > outside this self-service flow — do not assign it by default here.
19
+
20
+ ## Why `EmployeeSlackNotifications` alone isn't enough
21
+
22
+ - **`EmployeeSlackNotifications`** (Step 4) only grants notification receipt — **no object access**.
23
+
24
+ So a UEL user assigned only `EmployeeSlackNotifications` will get **"Nothing turned up"** when they open
25
+ the Incident / Service Request / Case lists in Slack. The list *definitions* still appear in the picker
26
+ (they are org-shared metadata), but they return no records without object read.
27
+
28
+ ## What to assign for record visibility
29
+
30
+ Two things, both assigned to the specific UEL user (never "all active users"):
31
+
32
+ 1. **API Enabled** — required for the Slack/employee app to call Connect APIs on the user's behalf.
33
+ Any permission set that enables the **API Enabled** system permission works. **Reuse** an existing
34
+ API-Enabled permset if the org already has one (confirm the user has it before adding); otherwise
35
+ **create** a Slack-named one (e.g. Name `Slack_ApiAccess`, label "Slack API Access", with
36
+ `PermissionsApiEnabled = true`).
37
+ 2. **`ItsmPortalUelUserPermset`** ("ITSM Portal Uel User") — the standard ITSM self-service permset that
38
+ grants **read/create/edit on Incident, Service Request, and Case** (plus read on supporting objects
39
+ like Account, Contact, Asset, and Service Catalog items). This is the correct baseline for a
40
+ self-service employee — it deliberately does **not** grant Problem or Change Request.
41
+
42
+ ## How to assign (headless-360, one record per user/permset)
43
+
44
+ Resolve the `ItsmPortalUelUserPermset` Id fresh (stable per org, but don't hardcode); resolve or create
45
+ the API-Enabled permset per the reuse-or-create flow above and use its Id too:
46
+
47
+ ```text
48
+ mcp__headless-360__dispatch_readonly(
49
+ method: "GET",
50
+ url: "/services/data/v67.0/query",
51
+ queryParams: { "q": "SELECT Id, Name, Label FROM PermissionSet WHERE Name = 'ItsmPortalUelUserPermset'" }
52
+ )
53
+ ```
54
+
55
+ Assign each to the confirmed user (two POSTs — skip either permset the user already has, checked via
56
+ `PermissionSetAssignment`):
57
+
58
+ ```text
59
+ mcp__headless-360__dispatch(
60
+ method: "POST",
61
+ url: "/services/data/v67.0/sobjects/PermissionSetAssignment",
62
+ body: { "AssigneeId": "<user id>", "PermissionSetId": "<permset id>" }
63
+ )
64
+ ```
65
+
66
+ Verify (each self-service object should read RCE for Incident/ServiceRequest/Case):
67
+
68
+ ```text
69
+ mcp__headless-360__dispatch_readonly(
70
+ method: "GET",
71
+ url: "/services/data/v67.0/query",
72
+ queryParams: { "q": "SELECT SobjectType, PermissionsRead, PermissionsCreate, PermissionsEdit FROM ObjectPermissions WHERE ParentId IN (SELECT PermissionSetId FROM PermissionSetAssignment WHERE AssigneeId = '<user id>') AND SobjectType IN ('Incident','ServiceRequest','Case')" }
73
+ )
74
+ ```
75
+
76
+ Then tell the user to reload the Slack Salesforce app; the Incident / Service Request / Case lists that
77
+ previously returned "Nothing turned up" will now resolve records. (Problem and Change Request remain empty
78
+ by design — they are fulfiller queues, not part of the self-service scope.)
@@ -0,0 +1,126 @@
1
+ # Site publish + membership verification (Step 5b)
2
+
3
+ Setting the **Preferred Digital Experience Site** (`SLACK_PREFERRED_SITE`) and assigning the Step 4
4
+ permission sets are necessary but **not sufficient** for an employee to actually see IT Service records in
5
+ the Slack app. Three silent gaps remain — each of which lets every low-level write return `200`/`201` while
6
+ the end result is still broken. **Verify them separately — they are three distinct states, not one:**
7
+
8
+ - **(a) activation** — the preferred site (its **`Network`**) is not **`Live`** — a portal in
9
+ `UnderConstruction` (or `DownForMaintenance`) serves nothing to employees;
10
+ - **(b) membership** — the target employee is **not a `NetworkMember`** of that site — a non-member can't
11
+ see the portal's records even when the site is Live; and
12
+ - **(c) publication** — the site's **Builder pages are unpublished**. Activation (`Network.Status = 'Live'`)
13
+ and publication are **separate go-live states**: a Live network with unpublished pages still 404s / shows
14
+ "under construction". `Network.Status` does **not** prove the pages were published.
15
+
16
+ On any org where the portal hasn't been activated/published or the user's profile/permset isn't on the
17
+ site's Members list, the Slack "IT Service" record lists come back **empty / access-denied** — yet no
18
+ permset assignment failed, no org preference is wrong, and the Slack connection is fine. So the skill's
19
+ terminal "setup complete" message is materially wrong. **Run all three checks after Step 5 and before any
20
+ success summary; a failure of any one is a blocking finding.**
21
+
22
+ > **Programmatic remediation exists — this is not UI-only.** Activation and membership go through the
23
+ > **Metadata API** (`Network` status → `Live`; `NetworkMemberGroup` for members) and publication goes
24
+ > through the **community-publish** step `experience-portal-create` owns — there is no Connect API for any of them (a `PATCH /connect/communities/<id>`
25
+ > returns 405). These are the same verified paths `experience-portal-create` uses in its Step 3
26
+ > (Activate → Add Members → Publish); hand the remediation to that skill rather than treating it as a
27
+ > manual builder-only action. Builder clicks are only the fallback when the operator prefers the UI.
28
+
29
+ ## Check (a) — the preferred site is Live
30
+
31
+ The site Id to check is whatever `SLACK_PREFERRED_SITE` resolves to (read it back in Step 5.2) — a
32
+ `Network` Id (`0DB...`).
33
+
34
+ ```text
35
+ mcp__headless-360__dispatch_readonly(
36
+ method: "GET",
37
+ url: "/services/data/v67.0/query",
38
+ queryParams: { "q": "SELECT Id, Name, Status FROM Network WHERE Id = '<preferred site Network Id>'" }
39
+ )
40
+ ```
41
+
42
+ - **`Status = 'Live'`** → published; check passes.
43
+ - **Anything else** (`UnderConstruction`, `DownForMaintenance`) → **blocking**. Surface it in plain
44
+ language — "the employee portal *\<Name\>* isn't published yet, so employees won't see anything in Slack
45
+ until it's live" — and give the remediation below. Do **not** report setup complete.
46
+
47
+ ### Remediation — activate the Network (programmatic path preferred)
48
+
49
+ `Network.Status` is not writable through the org-values or a direct SObject/Connect `PATCH` here (a
50
+ `PATCH /connect/communities/<id>` returns **405**). The **verified programmatic path is a Metadata-API
51
+ `Network` deploy** setting `<status>Live</status>` — the same activation step `experience-portal-create`
52
+ runs in its Step 3. **Hand activation to `experience-portal-create` (Activate → Add Members → Publish)**;
53
+ that skill owns the `Network` metadata deploy and the reachability check. After it activates, re-run
54
+ check (a) to confirm `Status = 'Live'`.
55
+
56
+ Builder fallback (only if the operator prefers the UI): **Digital Experience Builder → open the site →
57
+ Publish** — but note that clicking Publish in the builder both activates *and* publishes, whereas the
58
+ programmatic path treats them separately (check (c)). Don't claim an API activated it if you handed it off
59
+ to the UI. (If the org needs the portal *created* in the first place, that is also the
60
+ `experience-portal-create` hand-off from Step 3, not this check.)
61
+
62
+ ## Check (b) — the target user is a site member
63
+
64
+ For **each** user assigned in Step 4 (the same confirmed employee user Ids):
65
+
66
+ ```text
67
+ mcp__headless-360__dispatch_readonly(
68
+ method: "GET",
69
+ url: "/services/data/v67.0/query",
70
+ queryParams: { "q": "SELECT Id FROM NetworkMember WHERE NetworkId = '<preferred site Network Id>' AND MemberId = '<user Id>'" }
71
+ )
72
+ ```
73
+
74
+ - **≥1 row** → the user is a member; check passes for that user.
75
+ - **`0` rows** → **blocking** for that user. Surface it — "\<user\> isn't a member of the *\<Name\>*
76
+ portal, so they won't see IT Service records in Slack" — with the remediation below.
77
+
78
+ `NetworkMember.MemberId` is the `User` Id. Membership is usually granted **by profile or permission set**
79
+ (the site's Members list), not per-user — so one fix typically covers many users at once.
80
+
81
+ ### Remediation — add members (programmatic path preferred)
82
+
83
+ `NetworkMember` is not directly writable through a public Connect/SObject write here, but membership **is**
84
+ grantable programmatically: adding a **Profile** or **permission set** to the site's Members list is a
85
+ **Metadata-API `NetworkMemberGroup`** deploy — the same "Add Members" step `experience-portal-create` runs
86
+ in its Step 3. **Hand membership to `experience-portal-create`**, which owns the `NetworkMemberGroup`
87
+ metadata (with its column discipline). Adding a profile/permset covers all users who hold it at once.
88
+
89
+ Builder fallback (only if the operator prefers the UI): **Experience Builder → open the site →
90
+ Administration → Members** → add the employee's **Profile** (or a **permission set** the employee holds) to
91
+ the *Selected* Members list → **Save**.
92
+
93
+ After membership is granted, re-run check (b) for the affected user(s) to confirm a `NetworkMember` row now
94
+ exists. Don't claim an API added the member if you handed it off to the UI.
95
+
96
+ ## Check (c) — the site's Builder pages are published
97
+
98
+ Activation (check (a)) makes the `Network` addressable; it does **not** publish the Builder pages. A Live
99
+ network whose pages were never published still serves "under construction" / 404s, so employees see nothing
100
+ in Slack. Publication is a **separate go-live mutation** and must be verified on its own.
101
+
102
+ There is no `Network` column that cleanly reports "pages published," so verify publication the way
103
+ `experience-portal-create` does — confirm the site is reachable end to end after a publish, and treat a
104
+ never-published site (one that has only ever been activated) as **not published**:
105
+
106
+ - If the site was provisioned/activated but the **community-publish step has never completed** for it →
107
+ **blocking**. Surface it — "the employee portal *\<Name\>* is active but its pages aren't published yet,
108
+ so employees will see 'under construction' in Slack" — with the remediation below.
109
+
110
+ ### Remediation — publish the site (programmatic path preferred)
111
+
112
+ Publication has **no Connect API**; the verified path is the community-publish CLI step, polled to
113
+ completion on `BackgroundOperation` — exactly `experience-portal-create`'s Step 3 "Publish" step. **Hand
114
+ publication to `experience-portal-create`**; it publishes the site by name against the org and waits for
115
+ the returned `jobId` to reach `Status: Complete`.
116
+
117
+ Builder fallback (only if the operator prefers the UI): **Digital Experience Builder → open the site →
118
+ Publish**. After publication completes, re-confirm reachability. Don't claim an API published it if you
119
+ handed it off to the UI.
120
+
121
+ ## Reporting
122
+
123
+ Only after **all three** checks — (a) activation, (b) membership, (c) publication — pass for the chosen site
124
+ and every Step-4 user may the skill proceed to Step 6 (or emit the success summary). If any check fails, the
125
+ user-facing message must name the specific blocking finding(s) and the remediation (prefer the
126
+ `experience-portal-create` programmatic hand-off; UI as fallback) — not a green "setup complete."
@@ -0,0 +1,167 @@
1
+ #!/usr/bin/env node
2
+ // Deterministic helpers for Step 4 (Manage User Access) of the Slack Employee
3
+ // Service setup, so the model never eyeballs a response body.
4
+ //
5
+ // Two sub-commands:
6
+ //
7
+ // flag-users <users-query.json>
8
+ // Reads the User SOQL response (the {records:[...]} envelope from
9
+ // /services/data/vXX.0/query) and tags each row as "employee" or "system"
10
+ // using a fixed heuristic — WITHOUT dropping any row. System/integration
11
+ // accounts (Automated Process, Bot User, Insights Integration, *.ext,
12
+ // ESW_* / *Site Guest User, DigitalAgent.*, *@00d* platform users) are
13
+ // flagged so the user isn't asked to guess, but they are still listed.
14
+ // Prints {users:[{id,name,username,email,kind}], employeeCount, systemCount}.
15
+ //
16
+ // classify-assignment <assignment-response.json>
17
+ // Reads the PermissionSetAssignment POST result and classifies it as
18
+ // "success" (201 / has an id), "wrong-license" (a 400
19
+ // FIELD_INTEGRITY_EXCEPTION about the Slack permission-set license not
20
+ // being supported by the user's license — a HARD stop, do not retry), or
21
+ // "other-error". Prints {status, retryable, message}.
22
+ //
23
+ // Both read their input from a file path (pass the captured response JSON).
24
+ // Exits 2 on missing/bad args, 3 on unreadable/unparseable input.
25
+
26
+ import { readFileSync } from 'node:fs';
27
+
28
+ const [cmd, inPath] = process.argv.slice(2);
29
+
30
+ // headless-360 `dispatch`/`dispatch_readonly` may hand back either the raw
31
+ // Salesforce payload or an envelope like { status_code, body } / { statusCode,
32
+ // body } / { data }. Unwrap to the underlying Salesforce payload so both the
33
+ // user-query and the assignment-response classifiers see the shape they expect
34
+ // regardless of how the response was captured. (Idempotent: a raw payload with
35
+ // no envelope key is returned unchanged.)
36
+ function unwrapEnvelope(data) {
37
+ if (data && typeof data === 'object' && !Array.isArray(data)) {
38
+ const hasEnvelopeMarker =
39
+ 'status_code' in data || 'statusCode' in data || 'httpStatusCode' in data;
40
+ if (hasEnvelopeMarker && data.body !== undefined) return data.body;
41
+ // A lone { data: ... } wrapper (no status marker) is also an envelope.
42
+ if (
43
+ data.body === undefined &&
44
+ data.data !== undefined &&
45
+ !('records' in data) &&
46
+ !('id' in data) &&
47
+ !('Id' in data)
48
+ ) {
49
+ return data.data;
50
+ }
51
+ }
52
+ return data;
53
+ }
54
+
55
+ function die(code, msg) {
56
+ process.stderr.write(`${msg}\n`);
57
+ process.exit(code);
58
+ }
59
+
60
+ function loadJson(path) {
61
+ if (!path) die(2, 'usage: node classify-user-access.mjs <flag-users|classify-assignment> <response.json>');
62
+ let raw;
63
+ try {
64
+ raw = readFileSync(path, 'utf8');
65
+ } catch {
66
+ die(3, `error: could not read ${path}`);
67
+ }
68
+ try {
69
+ return JSON.parse(raw);
70
+ } catch {
71
+ die(3, `error: could not parse JSON in ${path}`);
72
+ }
73
+ }
74
+
75
+ // A row is a system/integration account (flag, don't drop) if its Name or
76
+ // Username matches any of these fixed patterns. Case-insensitive.
77
+ const SYSTEM_NAME_PATTERNS = [
78
+ /^automated process$/i,
79
+ /^bot user$/i,
80
+ /insights? integration/i,
81
+ /integration user/i,
82
+ /site guest user/i,
83
+ /^chatter expert$/i,
84
+ /^security user$/i,
85
+ /^analytics cloud/i,
86
+ /platform integration user/i,
87
+ ];
88
+ const SYSTEM_USERNAME_PATTERNS = [
89
+ /\.ext@/i, // *.ext users
90
+ /^esw_/i, // ESW_* embedded-service users
91
+ /^digitalagent\./i, // DigitalAgent.* agent users
92
+ /@00d/i, // *@00d... platform/site users
93
+ /autoproc@/i,
94
+ /insightssecurity@/i,
95
+ ];
96
+
97
+ function classifyUserKind(name, username) {
98
+ const n = String(name ?? '');
99
+ const u = String(username ?? '');
100
+ const isSystem =
101
+ SYSTEM_NAME_PATTERNS.some((re) => re.test(n)) ||
102
+ SYSTEM_USERNAME_PATTERNS.some((re) => re.test(u));
103
+ return isSystem ? 'system' : 'employee';
104
+ }
105
+
106
+ function flagUsers(data) {
107
+ const records = Array.isArray(data?.records) ? data.records : Array.isArray(data) ? data : [];
108
+ const users = records.map((r) => {
109
+ const kind = classifyUserKind(r?.Name, r?.Username);
110
+ return {
111
+ id: r?.Id ?? null,
112
+ name: r?.Name ?? null,
113
+ username: r?.Username ?? null,
114
+ email: r?.Email ?? null,
115
+ kind,
116
+ };
117
+ });
118
+ const employeeCount = users.filter((u) => u.kind === 'employee').length;
119
+ const systemCount = users.length - employeeCount;
120
+ return { users, employeeCount, systemCount };
121
+ }
122
+
123
+ // Salesforce error responses come back as an array of {errorCode, message} or
124
+ // a single object; normalize and classify.
125
+ function classifyAssignment(data) {
126
+ // Success shapes: {id, success:true} (sObject POST) or {Id: "0Pa..."}.
127
+ const id = data?.id ?? data?.Id ?? null;
128
+ if (id && data?.success !== false) {
129
+ return { status: 'success', retryable: false, message: 'Permission set assigned.' };
130
+ }
131
+
132
+ const errors = Array.isArray(data) ? data : Array.isArray(data?.errors) ? data.errors : [data];
133
+ const first = errors.find((e) => e && (e.errorCode || e.message)) ?? {};
134
+ const errorCode = String(first.errorCode ?? '');
135
+ const message = String(first.message ?? '');
136
+
137
+ const looksLikeLicenseMismatch =
138
+ /FIELD_INTEGRITY_EXCEPTION/i.test(errorCode) &&
139
+ /(permission set license|user license)/i.test(message) &&
140
+ /(support|assigned)/i.test(message);
141
+
142
+ if (looksLikeLicenseMismatch) {
143
+ return {
144
+ status: 'wrong-license',
145
+ retryable: false,
146
+ message:
147
+ "This user's license can't hold this permission set. Pick a user on a compatible " +
148
+ 'license (a Standard User works) rather than retrying.',
149
+ };
150
+ }
151
+
152
+ return {
153
+ status: 'other-error',
154
+ retryable: false,
155
+ message: message || errorCode || 'Assignment failed for an unknown reason.',
156
+ };
157
+ }
158
+
159
+ const data = unwrapEnvelope(loadJson(inPath));
160
+
161
+ if (cmd === 'flag-users') {
162
+ process.stdout.write(JSON.stringify(flagUsers(data), null, 2) + '\n');
163
+ } else if (cmd === 'classify-assignment') {
164
+ process.stdout.write(JSON.stringify(classifyAssignment(data), null, 2) + '\n');
165
+ } else {
166
+ die(2, 'usage: node classify-user-access.mjs <flag-users|classify-assignment> <response.json>');
167
+ }