@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
@@ -2,10 +2,11 @@
2
2
  name: service-itsm-agentic-setup-employee-agent-configure
3
3
  description: "Create and activate an IT Service Employee agent as a Next-Gen Authoring (NGA) native agent from an ITSM Employee agent template's Agent Script, via the Salesforce CLI (sf): read the template, check idempotency, create the NGA bundle then publish and activate, verify live. Defaults to the broad IT Service Employee template; when the user names a specialized Employee template (Password Manager Assistance, Certificate Management, Onboarding, Hardware Request, and ~47 others catalogued in references/specialized-templates.md — all under the `svc_emp_intelligence__` namespace), pins that one instead. Idempotent per developer name. TRIGGER when the user asks to create/set up/provision/activate the Employee agent, the IT Service Employee agent, or a specialized Employee agent (password manager, certificate, onboarding, hardware request, etc.). DO NOT TRIGGER: prerequisite checks (service-itsm-agentic-setup-agentforce-studio-validate), CMDB CRUD, Fulfiller setup (service-itsm-agentic-setup-fulfiller-agent-configure)."
4
4
  metadata:
5
- version: "2.3"
5
+ version: "2.5"
6
6
  domains: ["Service", "Agentforce"]
7
7
  minApiVersion: "67.0"
8
8
  relatedSkills:
9
+ - "service-itsm-agentic-setup-agent-runtime-access-assign"
9
10
  - "service-itsm-agentic-setup-agentforce-studio-configure"
10
11
  - "service-itsm-agentic-setup-agentforce-studio-validate"
11
12
  - "service-itsm-agentic-setup-fulfiller-agent-configure"
@@ -60,7 +61,7 @@ If any of these are unmet, `sf` surfaces an auth error or a `401`/`403`/`404`; *
60
61
  |---------|---------|-------|
61
62
  | Studio access (precondition read) | `sf api request rest "/services/data/v67.0/agentforce-studio/access/Agents" --method GET -o <alias>` | `hasAccess=false` ⇒ prerequisite hand-off |
62
63
  | List agent templates + Agent Script (read) | `sf api request rest "/services/data/v67.0/connect/service-itsm/agent-templates?agentType=AgentforceEmployeeAgent" --method GET -o <alias>` | `agentType=AgentforceEmployeeAgent` required; confirms resolved `<masterLabel>` template + non-empty `agentScript` |
63
- | Enumerate existing agent + latest version status (read) | `sf data query -q "SELECT Id,DeveloperName,MasterLabel,(SELECT Id,Status FROM BotVersions ORDER BY VersionNumber DESC LIMIT 1) FROM BotDefinition WHERE Id='<botDefinitionId>' OR DeveloperName='<developerName>'" -o <alias> --json` | Keyed PRIMARILY on the template's `botDefinitionId` (Phase-1 row); the `OR DeveloperName=` clause is both the null-`botDefinitionId` fallback AND the guard for a dangling Id link (deleted target). Classified by `scripts/classify-agent-existence.mjs`; Active latest ⇒ ALREADY-CREATED; Inactive latest ⇒ offer reactivation |
64
+ | Enumerate existing agent + latest version status (read) | `sf data query -q "SELECT Id,DeveloperName,MasterLabel,AgentTemplate,(SELECT Id,Status FROM BotVersions ORDER BY VersionNumber DESC LIMIT 1) FROM BotDefinition WHERE Id='<botDefinitionId>' OR AgentTemplate='<agentTemplate>' OR DeveloperName='<developerName>'" -o <alias> --json` | Keyed PRIMARILY on the template's `botDefinitionId` (Phase-1 row); `OR AgentTemplate=` is the first fallback that catches the pre-provisioned broad `IT_Service_Employee` agent by its OOTB namespaced source template (`svc_emp_intelligence__ItEmployeeAssistance` = Phase-1 `template.id`) regardless of the collected DeveloperName guess; `OR DeveloperName=` is the last fallback for self-created agents (null `AgentTemplate`) and the guard for a dangling Id link (deleted target). Classified by `scripts/classify-agent-existence.mjs`; Active latest ⇒ ALREADY-CREATED; Inactive latest ⇒ offer reactivation |
64
65
  | **Create the NGA bundle** (write) | `sf api request rest "/services/data/v67.0/nextgen-authoring/bundles" --method POST --body @<body-file> -o <alias>` | Body built by `scripts/build-create-body.mjs`; response `id` = the bundle **version** Id |
65
66
  | **Publish the bundle version** (write) | `sf api request rest "/services/data/v67.0/nextgen-authoring/bundle-versions/<bundleVersionId>/publish" --method POST --body '{}' -o <alias>` | Returns `publishedBotId`/`publishedBotVersionId` — creates the underlying `BotDefinition`/`BotVersion` |
66
67
  | **Activate the bundle version** (write) | `sf api request rest "/services/data/v67.0/nextgen-authoring/bundle-versions/<bundleVersionId>/activate" --method POST --body '{}' -o <alias>` | Empty response on success; agent is now live and NGA-native |
@@ -87,9 +88,9 @@ Collect from the user (ask only what is not already in conversation context):
87
88
  | Label | Broad: `IT Service Employee Agent`. Specialized: the picked template's `masterLabel` verbatim (e.g. `Password Manager Assistance`) |
88
89
  | Confirm the write | **REQUIRED** — present resolved template + developerName + label, then require "yes" via `AskUserQuestion` |
89
90
 
90
- The collected `<masterLabel>`, `<developerName>`, `<label>` are threaded through every call — `<masterLabel>` selects the row in `agent-templates.data[]` (which also carries the `botDefinitionId` idempotency key); `<developerName>`/`<label>` are used in the `createBundleWithVersion` body (both outer `apiName`/`label` AND the substituted internal `config.developer_name`/`config.agent_label`). The **idempotency + verify reads key PRIMARILY on the template's `botDefinitionId`** (or, after a fresh create, the publish response's `publishedBotId`) and **fall back to the collected `<developerName>`** when that is null. A hardcode/collect mismatch on the create body diverges the bundle's outer identity from the script's internal identity.
91
+ The collected `<masterLabel>`, `<developerName>`, `<label>` are threaded through every call — `<masterLabel>` selects the row in `agent-templates.data[]` (which also carries the `botDefinitionId` idempotency key); `<developerName>`/`<label>` are used in the `createBundleWithVersion` body (both outer `apiName`/`label` AND the substituted internal `config.developer_name`/`config.agent_label`). The **idempotency + verify reads key PRIMARILY on the template's `botDefinitionId`** (or, after a fresh create, the publish response's `publishedBotId`) and **fall back to the BotDefinition's `AgentTemplate`** (the OOTB source template = Phase-1 `template.id`), then to the collected `<developerName>`, when that is null. A hardcode/collect mismatch on the create body diverges the bundle's outer identity from the script's internal identity.
91
92
 
92
- **Idempotency**: keyed PRIMARILY on the **template's `botDefinitionId`** (Phase-1 `agent-templates` row — the platform's authoritative template→`BotDefinition` link) and FALLING BACK to the collected `<developerName>`. The Phase-2 read is `BotDefinition WHERE Id='<botDefinitionId>' OR DeveloperName='<developerName>'` (the `OR` half is both the null-`botDefinitionId` fallback AND the guard for a **dangling** Id link — one whose target `BotDefinition` was deleted — so a stale link can't slip through to create), + latest `BotVersion.Status`. Outcomes: no match on either key ⇒ create; `Active` ⇒ ALREADY-CREATED (skip write); `Inactive` ⇒ Phase-2b reactivation offer. **Why both keys:** the broad agent ships pre-provisioned as `IT_Service_Employee` ≠ the guess `IT_Service_Employee_Agent`, so `botDefinitionId` catches it — but an agent this skill creates never back-fills `botDefinitionId` (the create path omits `templateName`), so its template row stays null and the `developerName` fallback is what catches a repeat run. The server does reject a duplicate `DeveloperName` at publish (unique-constraint → bundle cleanup), but only this read turns a repeat into a graceful skip instead of a `DUPLICATE_VALUE`.
93
+ **Idempotency**: keyed PRIMARILY on the **template's `botDefinitionId`** (Phase-1 `agent-templates` row — the platform's authoritative template→`BotDefinition` link), FALLING BACK first to the **BotDefinition's `AgentTemplate`** (the OOTB namespaced source template = Phase-1 `template.id`) and then to the collected `<developerName>`. The Phase-2 read is `BotDefinition WHERE Id='<botDefinitionId>' OR AgentTemplate='<agentTemplate>' OR DeveloperName='<developerName>'` (the `OR AgentTemplate=` half catches the broad pre-provisioned agent by its platform-stamped source template regardless of what `DeveloperName` it carries; the `OR DeveloperName=` half is both the last fallback for self-created agents — whose `AgentTemplate` is null — AND the guard for a **dangling** Id link whose target `BotDefinition` was deleted), + latest `BotVersion.Status`. Outcomes: no match on any key ⇒ create; `Active` ⇒ ALREADY-CREATED (skip write); `Inactive` ⇒ Phase-2b reactivation offer. **Why the fallback keys:** the broad agent ships pre-provisioned as `IT_Service_Employee` ≠ the guess `IT_Service_Employee_Agent` (a null-`botDefinitionId` template row too), so neither the primary key nor the `developerName` guess catches it — its `AgentTemplate` (`svc_emp_intelligence__ItEmployeeAssistance`) is the reliable, rename-immune key that matches it; and for an agent this skill creates (which back-fills neither `botDefinitionId` nor `AgentTemplate`), the `developerName` fallback is what catches a repeat run. The server does reject a duplicate `DeveloperName` at publish (unique-constraint → bundle cleanup), but only this read turns a repeat into a graceful skip instead of a `DUPLICATE_VALUE`.
93
94
 
94
95
  ---
95
96
 
@@ -98,8 +99,8 @@ The collected `<masterLabel>`, `<developerName>`, `<label>` are threaded through
98
99
  Substitute `<alias>` with the collected target org and `<developerName>` / `<label>` with the collected values. Full command shapes + per-phase verdict-branch handling live in `references/workflow-detail.md` — the phase summary below names each step and its load-bearing rule; the reference file holds the exact `sf` / `node` invocations to copy.
99
100
 
100
101
  0. **Phase 0 — Establish `${SCRATCH_DIR}`.** Before any phase writes a transient JSON file, invoke the deterministic helper (path is skill-root-qualified so it resolves regardless of the shell's CWD): `SCRATCH_DIR="$(node "<skill_dir>/scripts/create-scratch-dir.mjs" "${outputDir:-}")"`. The helper picks the base dir (`${TMPDIR}`, else `/tmp`, else the harness `${outputDir}` last-resort — scratch stays OUT of the scored `${outputDir}` tree) and emits the created dir's absolute path on stdout. Every subsequent phase writes its transient JSON under `${SCRATCH_DIR}`; the durable `${outputDir}/report.md` stays under the harness dir.
101
- 1. **Phase 1 — Preflight.** Capture the Studio-access read + `agent-templates` read (with the **required** `agentType=AgentforceEmployeeAgent` query param), then classify via `scripts/classify-preflight.mjs "<masterLabel>"` — pass the resolved `<masterLabel>` (`"IT Service Employee"` for the broad path or the picked specialization's `masterLabel`). The classifier also emits `template.botDefinitionId` from the matched row — **capture it; it is the primary Phase-2 idempotency key (the collected `<developerName>` is the fallback key).** Branch on `verdict`: `READY` ⇒ Phase 2; `NOT-READY` ⇒ prerequisite hand-off via `AskUserQuestion` (delegate to `service-itsm-agentic-setup-agentforce-studio-validate` employee path on "yes"); `ERROR` ⇒ surface + stop; `studio.signal="CANNOT-CONFIRM"` (confirmed 404) does not block.
102
- 2. **Phase 2 — Idempotency (primary key `botDefinitionId`, fallback key `<developerName>`).** Take `template.botDefinitionId` from Phase 1. **Present** ⇒ SOQL `BotDefinition WHERE Id='<botDefinitionId>' OR DeveloperName='<developerName>'` (+ `BotVersions` subquery — required, else `needsActivation` is permanently false; the `OR` clause makes a **dangling** Id link — deleted target — fall back to the live same-name agent instead of a false `exists:false` → duplicate create). **Empty/null** ⇒ do NOT skip to create; fall back to `WHERE DeveloperName='<developerName>'` (a self-created agent's template row is never back-filled, so its `botDefinitionId` stays null even though the agent exists). Either way classify via `scripts/classify-agent-existence.mjs ${SCRATCH_DIR}/bot-existing.json "<botDefinitionId-or-empty>" "<developerName>"`. Branch: `exists:false` ⇒ Phase 3 (create); `exists:true` + `needsActivation:false` ⇒ **ALREADY-CREATED** (skip to Phase 7); `exists:true` + `needsActivation:true` ⇒ Phase 2b. Non-zero exit ⇒ surface CLI error; never assume absent. **Why both keys:** `botDefinitionId` catches the pre-provisioned `IT_Service_Employee` (≠ the guess `IT_Service_Employee_Agent`); the `developerName` fallback catches self-created repeats — a miss on both re-creates and hits `DUPLICATE_VALUE`.
102
+ 1. **Phase 1 — Preflight.** Capture the Studio-access read into `${SCRATCH_DIR}/studio-access.json` and the `agent-templates` read (with the **required** `agentType=AgentforceEmployeeAgent` query param) into `${SCRATCH_DIR}/agent-templates.json`, then classify by passing **both file paths, then the label** (that arg order): `node "<skill_dir>/scripts/classify-preflight.mjs" ${SCRATCH_DIR}/studio-access.json ${SCRATCH_DIR}/agent-templates.json "<masterLabel>"` — pass the resolved `<masterLabel>` (`"IT Service Employee"` for the broad path or the picked specialization's `masterLabel`). The classifier emits `template.botDefinitionId`, `template.id`, and `template.masterLabel` from the matched row — **capture all three; `botDefinitionId` is the primary Phase-2 idempotency key, `template.id` (the BotDefinition's `AgentTemplate`) the first fallback, and `<developerName>` the last fallback; `masterLabel` is the report's display label, not a key.** Branch on `verdict`: `READY` ⇒ Phase 2; `NOT-READY` ⇒ prerequisite hand-off via `AskUserQuestion` (delegate to `service-itsm-agentic-setup-agentforce-studio-validate` employee path on "yes"); `ERROR` ⇒ surface + stop; `studio.signal="CANNOT-CONFIRM"` (confirmed 404) does not block.
103
+ 2. **Phase 2 — Idempotency (primary key `botDefinitionId`, fallbacks `AgentTemplate` then `<developerName>`).** Take `template.botDefinitionId` and `template.id` from Phase 1. **Present `botDefinitionId`** ⇒ SOQL `BotDefinition WHERE Id='<botDefinitionId>' OR AgentTemplate='<agentTemplate>' OR DeveloperName='<developerName>'` (+ `BotVersions` subquery — required, else `needsActivation` is permanently false; the `OR` clauses make a **dangling** Id link — deleted target — fall back to the live agent instead of a false `exists:false` → duplicate create). **Empty/null `botDefinitionId`** ⇒ do NOT skip to create; read `WHERE AgentTemplate='<agentTemplate>' OR DeveloperName='<developerName>'` (the broad pre-provisioned agent is recovered by its OOTB source template even when its DeveloperName differs from the guess; a self-created agent carries a null `AgentTemplate`, so `DeveloperName` is its guard). `<agentTemplate>` is Phase-1 `template.id`. Either way classify via `node "<skill_dir>/scripts/classify-agent-existence.mjs" ${SCRATCH_DIR}/bot-existing.json "<botDefinitionId-or-empty>" "<developerName>" "<agentTemplate>"`. Branch: `exists:false` ⇒ Phase 3 (create); `exists:true` + `needsActivation:false` ⇒ **ALREADY-CREATED** (skip to Phase 7); `exists:true` + `needsActivation:true` ⇒ Phase 2b. Non-zero exit ⇒ surface CLI error; never assume absent. **Why the fallback keys:** the broad agent ships pre-provisioned as `IT_Service_Employee` with a null template `botDefinitionId`, so neither the primary key nor the `developerName` guess (`IT_Service_Employee_Agent`) catches it — its `AgentTemplate` (`svc_emp_intelligence__ItEmployeeAssistance`) matches it by the platform-stamped source template; the `developerName` fallback catches self-created repeats — a miss on all three re-creates and hits `DUPLICATE_VALUE`.
103
104
  3. **Phase 2b — Reactivation offer.** `AskUserQuestion`: _"Employee agent `<developerName>` exists but latest version is Inactive. Activate it?"_. On **Yes**: `POST /connect/bot-versions/<latestVersionId>/activation` with `{"status":"Active"}` — skips Phases 3–6, straight to Phase 7. Aggregate verdict is **ACTIVATED**, not CREATED. On **No**: stop, no writes.
104
105
  4. **Phase 3 — Confirm-to-Write (REQUIRED, create path only).** If `${outputDir}` was provided, first render the checkpoint file via `render-report.mjs` with `verdict:"PENDING CONFIRMATION"` (skip for interactive runs). THEN raise the `AskUserQuestion` gate presenting developerName + label + "NGA-native from the Employee template's Agent Script". Proceed **only** on explicit "yes"; on "no" (including "hold off on activation" / "not yet" / any decline of the atomic chain), re-render with `verdict:"DECLINED"` and a one-line `reason`.
105
106
  5. **Phase 4 — Create.** `scripts/build-create-body.mjs ${SCRATCH_DIR}/agent-templates.json "<masterLabel>" "<developerName>" "<label>" ${SCRATCH_DIR}/create-bundle-body.json` (helper re-reads Phase-1 templates JSON, HTML-decodes the matched `agentScript`, substitutes internal `config.developer_name`/`config.agent_label`, writes body to file — pass the same `<masterLabel>` used in Phase 1), then `POST /nextgen-authoring/bundles --body @${SCRATCH_DIR}/create-bundle-body.json`. **Capture response `id`** — that is the `bundleVersionId` for Phases 5–6, not `bundleId`. `403 FUNCTIONALITY_NOT_ENABLED`/`404` ⇒ trigger the Phase-1 hand-off; build-script exit 3 ⇒ surface stderr.
@@ -130,7 +131,7 @@ Substitute `<alias>` with the collected target org and `<developerName>` / `<lab
130
131
 
131
132
  - [ ] Resolved `<masterLabel>` before Phase 1 (broad default or specialization from `references/specialized-templates.md`, `data[]` filtered to `svc_emp_intelligence__`, disambiguated on `id`).
132
133
  - [ ] Preflight classified by `classify-preflight.mjs` (PASS or documented CANNOT-CONFIRM); hand-off offered on FAIL; raw error surfaced on ERROR.
133
- - [ ] Idempotency keyed on the template's `botDefinitionId` (Phase-1 row) with the collected developerName as fallback; `BotDefinition WHERE Id='<botDefinitionId>' OR DeveloperName='<developerName>'` (the `OR` covers both a null and a dangling `botDefinitionId`) + latest `BotVersion.Status` (subquery present) read + classified before any write.
134
+ - [ ] Idempotency keyed on the template's `botDefinitionId` (Phase-1 row) with the BotDefinition's `AgentTemplate` (Phase-1 `template.id`) as the first fallback and the collected developerName as the last; `BotDefinition WHERE Id='<botDefinitionId>' OR AgentTemplate='<agentTemplate>' OR DeveloperName='<developerName>'` (the `OR`s cover a null/dangling `botDefinitionId`, the pre-provisioned broad agent whose DeveloperName differs from the guess, and self-created agents with a null `AgentTemplate`) + latest `BotVersion.Status` (subquery present) read + classified before any write.
134
135
  - [ ] If `needsActivation:true`, Phase-2b reactivation offer presented — no silent skip, no duplicate create.
135
136
  - [ ] Explicit user confirmation at Phase 3 (create) or Phase 2b (reactivation) before any write.
136
137
  - [ ] Bundle body built by `build-create-body.mjs`, POSTed via `--body @<file>` with the collected `developerName`/`label`; or write correctly skipped.
@@ -151,13 +151,13 @@ Classifier output:
151
151
  ```json
152
152
  {
153
153
  "studio": { "hasAccess": true, "signal": "PASS|FAIL|CANNOT-CONFIRM|ERROR", "reason": "..." },
154
- "template": { "present": true, "id": "svc_emp_intelligence__ItEmployeeAssistance", "hasAgentScript": true, "botDefinitionId": "0Xx...", "signal": "PASS|FAIL|CANNOT-CONFIRM", "reason": "..." },
154
+ "template": { "present": true, "id": "svc_emp_intelligence__ItEmployeeAssistance", "hasAgentScript": true, "botDefinitionId": "0Xx...", "masterLabel": "IT Service Employee", "signal": "PASS|FAIL|CANNOT-CONFIRM", "reason": "..." },
155
155
  "verdict": "READY | NOT-READY | CANNOT-CONFIRM | ERROR",
156
156
  "reasons": ["..."]
157
157
  }
158
158
  ```
159
159
 
160
- `template.botDefinitionId` is copied from the matched `agent-templates` row. **`botDefinitionId` is the PRIMARY Phase-2 idempotency key** — a populated value means the template has already been instantiated into a live `BotDefinition` (the broad Employee agent ships pre-provisioned this way); `null` means either it has not been created yet OR it was created by this skill (a self-created agent never back-fills the template row — see Phase 2). Capture `botDefinitionId` for the Phase-2 read below and always carry the collected `<developerName>` as the fallback key. (`isInstalled`/`isActivated` are no longer emitted: they ride the same AgentTemplate join as `botDefinitionId`, so they read false for self-created agents and nothing consumes them.)
160
+ `template.botDefinitionId`, `template.id`, and `template.masterLabel` are copied from the matched `agent-templates` row. **`botDefinitionId` is the PRIMARY Phase-2 idempotency key** — a populated value means the template has already been instantiated into a live `BotDefinition`; `null` means it has not been created yet OR it was created by this skill (a self-created agent never back-fills the template row) OR — as with the broad pre-provisioned Employee agent — the platform instantiated it without joining the template row back (see Phase 2). Capture `botDefinitionId` for the Phase-2 read below and always carry `template.id` (the BotDefinition's `AgentTemplate` — the first fallback) and the collected `<developerName>` (the last fallback) as the fallback keys; `masterLabel` is the report's display label, not an idempotency key. (`isInstalled`/`isActivated` are no longer emitted: they ride the same AgentTemplate join as `botDefinitionId`, so they read false for self-created agents and nothing consumes them.)
161
161
 
162
162
  `verdict=ERROR` (`studio.signal="ERROR"` — a parseable non-404 Studio-access
163
163
  error, e.g. `401`/`403`) ⇒ surface the raw error and stop; takes priority over
@@ -165,53 +165,65 @@ template state so a present template cannot outrun a failed prerequisite read.
165
165
  `verdict=NOT-READY` (studio FAIL or template FAIL) ⇒ hand off / stop.
166
166
  `verdict=READY` ⇒ proceed to Phase 2. It exits `0` on usable args.
167
167
 
168
- ## Enumerate the existing agent — SOQL on `BotDefinition` BY Id (falling back to DeveloperName)
168
+ ## Enumerate the existing agent — SOQL on `BotDefinition` BY Id (falling back to AgentTemplate, then DeveloperName)
169
169
 
170
170
  Idempotency is keyed **PRIMARILY** on the template's `botDefinitionId` (from the
171
- Phase-1 row) and **FALLS BACK** to the collected `<developerName>`. The broad
172
- "IT Service Employee" agent ships pre-provisioned+active under DeveloperName
173
- `IT_Service_Employee`, which never matches this skill's default guess
174
- `IT_Service_Employee_Agent`; a name-only read false-negatives on THAT agent
175
- (`exists:false`) and the create then collides on `apiName` with
176
- `DUPLICATE_VALUE` — so `botDefinitionId` (the platform's authoritative link from
177
- the template to the `BotDefinition` it was instantiated into) is the primary key.
178
- But that link is back-filled onto the template row **only** for pre-provisioned
179
- agents: an agent THIS skill creates never stamps `templateName`, so its template
180
- `botDefinitionId` stays `null` on every later read. For that self-created case a
181
- `DeveloperName`-keyed read is the reliable guard — so use it as the fallback,
182
- never short-circuit straight to create when `botDefinitionId` is absent.
183
-
184
- - **`botDefinitionId` empty/null** (template row never joined — either not
185
- created yet, or created by this skill) ⇒ fall back to a DeveloperName-keyed
186
- read and let the classifier match on it:
171
+ Phase-1 row), **FALLS BACK** first to the BotDefinition's `AgentTemplate` (the
172
+ OOTB namespaced source template = Phase-1 `template.id`), and then to the
173
+ collected `<developerName>`. The broad "IT Service Employee" agent ships
174
+ pre-provisioned+active under DeveloperName `IT_Service_Employee` **with a `null`
175
+ template `botDefinitionId`** (the platform instantiated it without joining the
176
+ template row back), and `IT_Service_Employee` never matches this skill's default
177
+ guess `IT_Service_Employee_Agent` — so both the primary key AND the
178
+ `DeveloperName` fallback miss on THAT agent, a create is attempted, and it
179
+ collides on `apiName` with `DUPLICATE_VALUE`. The `AgentTemplate`-keyed fallback
180
+ closes that gap far more reliably than a display name would: the live
181
+ pre-provisioned agent carries `AgentTemplate=svc_emp_intelligence__ItEmployeeAssistance`
182
+ (the exact template id this skill installs), the platform-stamped source template,
183
+ immune to any `DeveloperName` rename. `botDefinitionId` remains the
184
+ primary key for a genuinely joined instantiation, and the `DeveloperName`
185
+ fallback is the guard for a self-created repeat (an agent THIS skill creates never
186
+ stamps `templateName`, so BOTH its template `botDefinitionId` AND its
187
+ `AgentTemplate` stay `null`). Never short-circuit straight to create when
188
+ `botDefinitionId` is absent.
189
+
190
+ - **`botDefinitionId` empty/null** (the broad pre-provisioned agent, and every
191
+ agent this skill creates) ⇒ fall back to an `AgentTemplate` **`OR` `DeveloperName`**
192
+ read and let the classifier match on either:
187
193
  ```bash
188
- sf data query -q "SELECT Id,DeveloperName,MasterLabel,(SELECT Id,Status FROM BotVersions ORDER BY VersionNumber DESC LIMIT 1) FROM BotDefinition WHERE DeveloperName='<developerName>'" \
194
+ sf data query -q "SELECT Id,DeveloperName,MasterLabel,AgentTemplate,(SELECT Id,Status FROM BotVersions ORDER BY VersionNumber DESC LIMIT 1) FROM BotDefinition WHERE AgentTemplate='<agentTemplate>' OR DeveloperName='<developerName>'" \
189
195
  --target-org <alias> --json > ${SCRATCH_DIR}/bot-existing.json 2>${SCRATCH_DIR}/bot-existing.err || true
190
- node "<skill_dir>/scripts/classify-agent-existence.mjs" ${SCRATCH_DIR}/bot-existing.json "" "<developerName>"
196
+ node "<skill_dir>/scripts/classify-agent-existence.mjs" ${SCRATCH_DIR}/bot-existing.json "" "<developerName>" "<agentTemplate>"
191
197
  ```
192
198
  - **`botDefinitionId` present** ⇒ read the `BotDefinition` by Id **`OR` by the
193
- collected `<developerName>`** in one query, then classify. The `OR DeveloperName=`
194
- clause catches a **dangling** link — a `botDefinitionId` whose target row was
195
- since deleted: the by-Id half returns nothing, the live same-name agent still
196
- surfaces, and the classifier falls back to it (`matchedBy:"developerName"`)
197
- rather than concluding `exists:false` and colliding with `DUPLICATE_VALUE`:
199
+ template's `AgentTemplate` `OR` by the collected `<developerName>`** in one
200
+ query, then classify. The `OR AgentTemplate=` clause is what catches the
201
+ pre-provisioned agent by its platform-stamped source template
202
+ (`matchedBy:"agentTemplate"`) regardless of its DeveloperName. The `OR
203
+ DeveloperName=` clause catches a **dangling** link — a `botDefinitionId` whose
204
+ target row was since deleted: the by-Id half returns nothing, the live same-name
205
+ agent still surfaces, and the classifier falls back to it
206
+ (`matchedBy:"developerName"`) rather than concluding `exists:false` and
207
+ colliding with `DUPLICATE_VALUE`:
198
208
  ```bash
199
- sf data query -q "SELECT Id,DeveloperName,MasterLabel,(SELECT Id,Status FROM BotVersions ORDER BY VersionNumber DESC LIMIT 1) FROM BotDefinition WHERE Id='<botDefinitionId>' OR DeveloperName='<developerName>'" \
209
+ sf data query -q "SELECT Id,DeveloperName,MasterLabel,AgentTemplate,(SELECT Id,Status FROM BotVersions ORDER BY VersionNumber DESC LIMIT 1) FROM BotDefinition WHERE Id='<botDefinitionId>' OR AgentTemplate='<agentTemplate>' OR DeveloperName='<developerName>'" \
200
210
  --target-org <alias> --json > ${SCRATCH_DIR}/bot-existing.json 2>${SCRATCH_DIR}/bot-existing.err || true
201
- node "<skill_dir>/scripts/classify-agent-existence.mjs" ${SCRATCH_DIR}/bot-existing.json "<botDefinitionId>" "<developerName>"
211
+ node "<skill_dir>/scripts/classify-agent-existence.mjs" ${SCRATCH_DIR}/bot-existing.json "<botDefinitionId>" "<developerName>" "<agentTemplate>"
202
212
  ```
203
213
 
204
- Only when **both** `botDefinitionId` and `<developerName>` are absent does the
205
- classifier return `exists:false` without reading a query file — in practice the
206
- collected `<developerName>` is always present, so the fallback read always runs.
214
+ Only when **all** of `botDefinitionId`, `<agentTemplate>`, and `<developerName>`
215
+ are absent does the classifier return `exists:false` without reading a query
216
+ file — in practice the collected `<developerName>` and `template.id` (AgentTemplate)
217
+ are always present, so the fallback read always runs.
207
218
 
208
219
  The `BotVersions` subquery (child relationship on `BotDefinition`) is what lets
209
220
  the classifier see the latest version's `Status` — omit it and
210
221
  `latestVersionStatus`/`needsActivation` come back `null`/`false` even when the
211
222
  existing agent is actually inactive. The classifier prints
212
223
  `{ exists, count, matchedBy, agentId, botDefinitionId, developerName, latestVersionId, latestVersionStatus, needsActivation }`
213
- (`matchedBy` is `"botDefinitionId"` | `"developerName"` | `null`; `developerName`
214
- is the ACTUAL live agent's DeveloperName read from the record — surface it in the
224
+ (`matchedBy` is `"botDefinitionId"` | `"agentTemplate"` | `"developerName"` | `null`;
225
+ `developerName` is the ACTUAL live agent's DeveloperName read from the record —
226
+ surface it in the
215
227
  report instead of the collected guess):
216
228
  - `exists:false` ⇒ proceed to create.
217
229
  - `exists:true` and `needsActivation:false` (latest version `Active`) ⇒
@@ -9,13 +9,15 @@ just reactivate what's already there".
9
9
  ## When this path fires
10
10
 
11
11
  `scripts/classify-agent-existence.mjs` (called with the template's
12
- `botDefinitionId` as the PRIMARY idempotency key and the collected
13
- `DeveloperName` as the FALLBACK — see `cli-invocation.md` → "Enumerate the
14
- existing agent") returns
12
+ `botDefinitionId` as the PRIMARY idempotency key, the BotDefinition's
13
+ `AgentTemplate` — the template's OOTB source id — as the first FALLBACK, and the
14
+ collected `DeveloperName` as the last FALLBACK —
15
+ see `cli-invocation.md` → "Enumerate the existing agent") returns
15
16
  `{ exists:true, needsActivation:true, latestVersionId, latestVersionStatus:"Inactive" }`.
16
- That means a `BotDefinition` matching the template (by `botDefinitionId`) or the
17
- collected `DeveloperName` already exists (created on a prior run, or — for the
18
- broad Employee agent — by the platform) but its most-recent `BotVersion` is
17
+ That means a `BotDefinition` matching the template (by `botDefinitionId`), its
18
+ `AgentTemplate` source id, or the collected `DeveloperName` already exists (created
19
+ on a prior run, or — for the broad Employee agent, matched by its source template —
20
+ by the platform) but its most-recent `BotVersion` is
19
21
  inactive; creating a new bundle would produce a duplicate. Instead, flip the
20
22
  existing version to `Active`.
21
23
 
@@ -28,10 +28,10 @@ node "<skill_dir>/scripts/classify-preflight.mjs" ${SCRATCH_DIR}/studio-access.j
28
28
 
29
29
  Pass the resolved `<masterLabel>` — `"IT Service Employee"` for the default broad path, or the picked specialized template's `masterLabel` (e.g. `"Password Manager Assistance"`) for a specialized path. The classifier matches `data[]` case-insensitively on `masterLabel` and confirms the matched row has a non-empty `agentScript`.
30
30
 
31
- The classifier prints `{ studio:{hasAccess,signal}, template:{present,id,hasAgentScript,botDefinitionId,signal}, verdict, reasons }`:
31
+ The classifier prints `{ studio:{hasAccess,signal}, template:{present,id,hasAgentScript,botDefinitionId,masterLabel,signal}, verdict, reasons }`:
32
32
 
33
33
  - `template.hasAgentScript:true` confirms the template carries the Agent Script content Phase 4 needs; capture the raw `agent-templates.json` file path — Phase 4 re-reads it directly (via `scripts/build-create-body.mjs`) rather than the classifier re-emitting the full script content.
34
- - **`template.botDefinitionId` is the PRIMARY Phase-2 idempotency key.** A populated value means this template has already been instantiated into a live `BotDefinition` (the broad Employee agent ships pre-provisioned this way); a `null` value means it has not been created yet OR it was created by this skill (self-created agents never back-fill the template row). Capture `botDefinitionId` for Phase 2, and carry the collected `<developerName>` as the fallback key.
34
+ - **`template.botDefinitionId` is the PRIMARY Phase-2 idempotency key.** A populated value means this template has already been instantiated into a live `BotDefinition`; a `null` value means it has not been created yet OR it was created by this skill (self-created agents never back-fill the template row) OR — as with the broad pre-provisioned Employee agent — the platform instantiated it without joining the template row back. Capture `botDefinitionId` for Phase 2, and carry the collected `<developerName>` (first fallback) and `template.masterLabel` (second fallback) as the fallback keys.
35
35
  - `verdict:"READY"` ⇒ continue to Phase 2.
36
36
  - `verdict:"ERROR"` because `studio.signal="ERROR"` (the Studio-access read returned a parseable but non-404 error body — e.g. `401`/`403`) ⇒ do NOT proceed — surface the raw error and stop. This is a failed prerequisite read, not an unwired gate; do not let a present template push the verdict to READY.
37
37
  - `verdict:"NOT-READY"` because `studio.signal="FAIL"` (`hasAccess=false`) ⇒ do NOT proceed — **offer the prerequisite hand-off**:
@@ -44,22 +44,22 @@ The classifier prints `{ studio:{hasAccess,signal}, template:{present,id,hasAgen
44
44
 
45
45
  ## Phase 2 — Idempotency: does the target agent already exist, and is it active?
46
46
 
47
- Idempotency is keyed **PRIMARILY** on the template's `botDefinitionId` (captured in Phase 1) and **FALLS BACK** to the collected `<developerName>`. The `botDefinitionId` is the platform's authoritative link from the `agent-templates` row to the `BotDefinition` it was instantiated into — it catches the pre-provisioned broad Employee agent (which ships active under DeveloperName `IT_Service_Employee`, never matching the default guess `IT_Service_Employee_Agent`, so a name-only read would false-negative and the create would fail with `DUPLICATE_VALUE`). But that link is back-filled **only** for pre-provisioned agents — an agent this skill creates never stamps `templateName`, so its template `botDefinitionId` stays `null`; for that self-created case the `DeveloperName`-keyed fallback is the guard, so never short-circuit to create when `botDefinitionId` is absent.
47
+ Idempotency is keyed **PRIMARILY** on the template's `botDefinitionId` (captured in Phase 1), **FALLS BACK** first to the BotDefinition's `AgentTemplate` (the OOTB namespaced source template = Phase-1 `template.id`), and then to the collected `<developerName>`. The `botDefinitionId` is the platform's authoritative link from the `agent-templates` row to the `BotDefinition` it was instantiated into, but that link is populated **only** when the platform joined the row back — which it does NOT do for the broad pre-provisioned Employee agent (it ships active under DeveloperName `IT_Service_Employee` with a `null` template `botDefinitionId`) nor for an agent this skill creates (which never stamps `templateName`). So for the broad agent both the primary key and the `DeveloperName` guess miss (`IT_Service_Employee` ≠ the default `IT_Service_Employee_Agent`), and the `AgentTemplate`-keyed fallback — matching `svc_emp_intelligence__ItEmployeeAssistance`, the platform-stamped source template on the live `BotDefinition`, immune to any rename — is what recovers it; for a self-created repeat (null `AgentTemplate` too) the `DeveloperName` fallback is the guard. Never short-circuit to create when `botDefinitionId` is absent.
48
48
 
49
- - **`template.botDefinitionId` is empty/null** ⇒ fall back to a DeveloperName-keyed read (do **not** short-circuit to create — a self-created agent from a prior run has a null template `botDefinitionId` but still exists). The `BotVersions` subquery is required — omitting it leaves `needsActivation` permanently false and hides the reactivation path:
49
+ - **`template.botDefinitionId` is empty/null** (the broad pre-provisioned agent AND every self-created agent) ⇒ fall back to an `AgentTemplate` **`OR` `DeveloperName`** read (do **not** short-circuit to create — the broad agent is recovered by its OOTB source template, and a self-created agent from a prior run has a null template `botDefinitionId` but still exists). The `BotVersions` subquery is required — omitting it leaves `needsActivation` permanently false and hides the reactivation path:
50
50
  ```bash
51
- sf data query -q "SELECT Id,DeveloperName,MasterLabel,(SELECT Id,Status FROM BotVersions ORDER BY VersionNumber DESC LIMIT 1) FROM BotDefinition WHERE DeveloperName='<developerName>'" \
51
+ sf data query -q "SELECT Id,DeveloperName,MasterLabel,AgentTemplate,(SELECT Id,Status FROM BotVersions ORDER BY VersionNumber DESC LIMIT 1) FROM BotDefinition WHERE AgentTemplate='<agentTemplate>' OR DeveloperName='<developerName>'" \
52
52
  --target-org <alias> --json > ${SCRATCH_DIR}/bot-existing.json 2>${SCRATCH_DIR}/bot-existing.err || true
53
- node "<skill_dir>/scripts/classify-agent-existence.mjs" ${SCRATCH_DIR}/bot-existing.json "" "<developerName>"
53
+ node "<skill_dir>/scripts/classify-agent-existence.mjs" ${SCRATCH_DIR}/bot-existing.json "" "<developerName>" "<agentTemplate>"
54
54
  ```
55
- - **`template.botDefinitionId` is present** ⇒ read the `BotDefinition` by Id **`OR` by the collected `<developerName>`** in one query, INCLUDING its latest version's status, then classify. The `OR DeveloperName=` clause is what catches a **dangling** template→BotDefinition link — a `botDefinitionId` whose target row was since deleted: the by-Id half returns nothing, but the live same-name agent still surfaces, so the classifier falls back to it (`matchedBy:"developerName"`) instead of concluding `exists:false` and letting the create collide with `DUPLICATE_VALUE`:
55
+ - **`template.botDefinitionId` is present** ⇒ read the `BotDefinition` by Id **`OR` by the template's `AgentTemplate` `OR` by the collected `<developerName>`** in one query, INCLUDING its latest version's status, then classify. The `OR AgentTemplate=` clause catches the pre-provisioned agent by its platform-stamped source template (`matchedBy:"agentTemplate"`) regardless of its DeveloperName. The `OR DeveloperName=` clause catches a **dangling** template→BotDefinition link — a `botDefinitionId` whose target row was since deleted: the by-Id half returns nothing, but the live same-name agent still surfaces, so the classifier falls back to it (`matchedBy:"developerName"`) instead of concluding `exists:false` and letting the create collide with `DUPLICATE_VALUE`:
56
56
  ```bash
57
- sf data query -q "SELECT Id,DeveloperName,MasterLabel,(SELECT Id,Status FROM BotVersions ORDER BY VersionNumber DESC LIMIT 1) FROM BotDefinition WHERE Id='<botDefinitionId>' OR DeveloperName='<developerName>'" \
57
+ sf data query -q "SELECT Id,DeveloperName,MasterLabel,AgentTemplate,(SELECT Id,Status FROM BotVersions ORDER BY VersionNumber DESC LIMIT 1) FROM BotDefinition WHERE Id='<botDefinitionId>' OR AgentTemplate='<agentTemplate>' OR DeveloperName='<developerName>'" \
58
58
  --target-org <alias> --json > ${SCRATCH_DIR}/bot-existing.json 2>${SCRATCH_DIR}/bot-existing.err || true
59
- node "<skill_dir>/scripts/classify-agent-existence.mjs" ${SCRATCH_DIR}/bot-existing.json "<botDefinitionId>" "<developerName>"
59
+ node "<skill_dir>/scripts/classify-agent-existence.mjs" ${SCRATCH_DIR}/bot-existing.json "<botDefinitionId>" "<developerName>" "<agentTemplate>"
60
60
  ```
61
61
 
62
- The classifier prints `{ exists, count, matchedBy, agentId, botDefinitionId, developerName, latestVersionId, latestVersionStatus, needsActivation }` — `matchedBy` is `"botDefinitionId"` | `"developerName"` | `null`, and `developerName` is the ACTUAL live agent's DeveloperName read from the record (e.g. `IT_Service_Employee`), surface it in the report rather than the collected guess:
62
+ The classifier prints `{ exists, count, matchedBy, agentId, botDefinitionId, developerName, latestVersionId, latestVersionStatus, needsActivation }` — `matchedBy` is `"botDefinitionId"` | `"agentTemplate"` | `"developerName"` | `null`, and `developerName` is the ACTUAL live agent's DeveloperName read from the record (e.g. `IT_Service_Employee`), surface it in the report rather than the collected guess:
63
63
 
64
64
  - `exists:false` ⇒ proceed to Phase 3 (create path).
65
65
  - `exists:true` and `needsActivation:false` (latest version `Active`) ⇒ **ALREADY-CREATED** — skip the write, fall through to Phase 7 verification.
@@ -3,50 +3,66 @@
3
3
  //
4
4
  // Idempotency for this flow is keyed PRIMARILY on the TEMPLATE's own
5
5
  // `botDefinitionId` — the platform's authoritative link from an `agent-templates`
6
- // row to the live `BotDefinition` it was instantiated into — and FALLS BACK to a
7
- // `DeveloperName`-keyed read when that row carries no `botDefinitionId`.
6
+ // row to the live `BotDefinition` it was instantiated into — and FALLS BACK
7
+ // first to the BotDefinition's own `AgentTemplate` field (the OOTB, namespaced
8
+ // source-template API name it was instantiated from, e.g.
9
+ // `svc_emp_intelligence__ItEmployeeAssistance`), then to a `DeveloperName`-keyed
10
+ // read.
8
11
  // The broad "IT Service Employee" agent is pre-provisioned+active under
9
12
  // DeveloperName `IT_Service_Employee`, which does NOT match this skill's default
10
- // collected developerName `IT_Service_Employee_Agent`; a DeveloperName-only read
11
- // false-negatives on THAT agent (exists:false) and the create then collides on
12
- // `apiName` with `DUPLICATE_VALUE` — hence botDefinitionId is the primary key.
13
- // But botDefinitionId is back-filled onto the template row ONLY for platform-
14
- // pre-provisioned agents: the ITSM create path never stamps `templateName`, so
15
- // agents THIS skill creates keep a null template botDefinitionId forever, and a
16
- // DeveloperName-keyed read is the reliable guard for them — used as the FALLBACK.
17
- // (Preflight emits `template.botDefinitionId`; the workflow passes it as the
18
- // primary key and the collected developerName as the fallback key.)
13
+ // collected developerName `IT_Service_Employee_Agent`, and its template row
14
+ // carries a NULL botDefinitionId — so a read keyed on botDefinitionId + collected
15
+ // DeveloperName false-negatives (exists:false) and the create then collides on
16
+ // `apiName` with `DUPLICATE_VALUE`. `AgentTemplate` is what closes that gap: the
17
+ // live pre-provisioned agent carries `AgentTemplate=svc_emp_intelligence__ItEmployeeAssistance`
18
+ // (the exact template id the skill installs — preflight `template.id`), so matching
19
+ // on it recovers the agent regardless of its DeveloperName. This is far more
20
+ // reliable than a display name (MasterLabel) — AgentTemplate is the platform-
21
+ // stamped source template, immune to renames, and namespaced OOTB. botDefinitionId
22
+ // is back-filled onto the template row ONLY for platform-pre-provisioned agents:
23
+ // the ITSM create path never stamps `templateName`, so agents THIS skill creates
24
+ // keep BOTH a null template botDefinitionId AND a null `AgentTemplate` forever —
25
+ // for those, the DeveloperName-keyed read is the reliable guard, used as the last
26
+ // FALLBACK. (Preflight emits `template.botDefinitionId`, `template.id`, and the
27
+ // collected developerName; the workflow passes the botDefinitionId as the primary
28
+ // key, template.id as the AgentTemplate key, and the developerName as the fallback.)
19
29
  //
20
30
  // This classifier consumes the raw JSON of a `sf data query ... --json` read of
21
- // BotDefinition (BY Id primarily, BY DeveloperName on the fallback) and decides
31
+ // BotDefinition (BY Id primarily, BY AgentTemplate then DeveloperName on the
32
+ // fallback) and decides
22
33
  // whether the agent already exists — and if so, whether its latest BotVersion is
23
34
  // Inactive (so the workflow can offer to activate it instead of creating a
24
35
  // duplicate). This is the deterministic decision that gates whether the write is
25
36
  // skipped — it MUST NOT be interpreted by the model in prose (authoring standard A9).
26
37
  //
27
38
  // Usage:
28
- // node classify-agent-existence.mjs <bot-query.json> <botDefinitionId> [developerName]
39
+ // node classify-agent-existence.mjs <bot-query.json> <botDefinitionId> [developerName] [agentTemplate]
29
40
  //
30
41
  // When <botDefinitionId> is empty, `-`, or the literal `null`/`undefined` (the
31
- // matched template carries no botDefinitionId), the classifier FALLS BACK to
32
- // keying on <developerName>, and <bot-query.json> must be a DeveloperName-keyed
33
- // BotDefinition read: self-created agents are never back-filled with a
34
- // botDefinitionId (the ITSM create path doesn't stamp `templateName`, so the
35
- // platform never joins the agent-templates row to its BotDefinition), so
36
- // DeveloperName is the only idempotency guard left for them. Only when BOTH
37
- // <botDefinitionId> and <developerName> are absent is exists:false emitted
38
- // without reading a query file. Otherwise <bot-query.json> is a FILE PATH to the
39
- // stdout captured from:
40
- // sf data query -q "SELECT Id,DeveloperName,MasterLabel,
42
+ // matched template carries no botDefinitionId — the normal Employee case), the
43
+ // classifier FALLS BACK to keying on <agentTemplate> (the source template API
44
+ // name — this is what catches the pre-provisioned `IT_Service_Employee`), and then
45
+ // to <developerName> when the AgentTemplate misses too. <bot-query.json> must
46
+ // therefore be a read that ORs all three keys together: self-created agents are
47
+ // never back-filled with a botDefinitionId AND carry a null AgentTemplate (the
48
+ // ITSM create path doesn't stamp `templateName`), so DeveloperName is their only
49
+ // idempotency guard; the pre-provisioned broad agent — whose DeveloperName differs
50
+ // from this skill's collected guess — is caught by AgentTemplate. Only when ALL of
51
+ // <botDefinitionId>, <agentTemplate>, and <developerName> are absent is
52
+ // exists:false emitted without reading a query file. Otherwise <bot-query.json> is
53
+ // a FILE PATH to the stdout captured from:
54
+ // sf data query -q "SELECT Id,DeveloperName,MasterLabel,AgentTemplate,
41
55
  // (SELECT Id,Status FROM BotVersions ORDER BY VersionNumber DESC LIMIT 1)
42
- // FROM BotDefinition WHERE Id='<botDefinitionId>' OR DeveloperName='<developerName>'" \
56
+ // FROM BotDefinition WHERE Id='<botDefinitionId>' OR AgentTemplate='<agentTemplate>'
57
+ // OR DeveloperName='<developerName>'" \
43
58
  // --target-org <org> --json > file.json
44
- // (The present-id read ORs in DeveloperName so a DANGLING template→BotDefinition
45
- // link — a botDefinitionId whose target BotDefinition was since deleted — still
46
- // surfaces the live same-name agent: the classifier tries the Id first, then
47
- // falls back to the name on the SAME records. The pure FALLBACK read, taken when
48
- // the template row carries no botDefinitionId at all, keys on
49
- // `DeveloperName='<developerName>'` only; everything else is identical.
59
+ // (The present-id read ORs in AgentTemplate AND DeveloperName so a DANGLING
60
+ // template→BotDefinition link — a botDefinitionId whose target BotDefinition was
61
+ // since deleted — still surfaces the live agent: the classifier tries the Id
62
+ // first, then the AgentTemplate, then the DeveloperName on the SAME records. The
63
+ // pure FALLBACK read, taken when the template row carries no botDefinitionId at
64
+ // all, keys on `AgentTemplate='<agentTemplate>' OR DeveloperName='<developerName>'`;
65
+ // everything else is identical.
50
66
  // `sf data query --json` wraps results in a `.result.records[]` envelope. The
51
67
  // BotVersions subquery is required so this classifier can see the latest
52
68
  // version's Status — a query without it leaves latestVersionStatus null and
@@ -61,16 +77,17 @@
61
77
  // child row was returned), and needsActivation is true only when exists is true
62
78
  // AND latestVersionStatus is "Inactive" — the signal to offer activating the
63
79
  // existing version instead of creating a new agent. `matchedBy` is
64
- // "botDefinitionId" | "developerName" | null, recording which key hit. Exit code
80
+ // "botDefinitionId" | "agentTemplate" | "developerName" | null, recording which
81
+ // key hit. Exit code
65
82
  // is always 0 on a parseable body (or when neither key is supplied); the verdict
66
83
  // is carried in the payload. On an unparseable/failed query it exits 3 so the
67
84
  // workflow surfaces the raw error rather than assuming NOT-EXISTS.
68
85
 
69
86
  import { readFileSync } from 'node:fs';
70
87
 
71
- const [queryPath, rawBotDefinitionId, rawDeveloperName] = process.argv.slice(2);
88
+ const [queryPath, rawBotDefinitionId, rawDeveloperName, rawAgentTemplate] = process.argv.slice(2);
72
89
  if (!queryPath) {
73
- process.stderr.write('usage: node classify-agent-existence.mjs <bot-query.json> <botDefinitionId> [developerName]\n');
90
+ process.stderr.write('usage: node classify-agent-existence.mjs <bot-query.json> <botDefinitionId> [developerName] [agentTemplate]\n');
74
91
  process.exit(2);
75
92
  }
76
93
 
@@ -88,8 +105,10 @@ function notSet(v) {
88
105
 
89
106
  const botDefinitionId = String(rawBotDefinitionId ?? '').trim();
90
107
  const developerName = String(rawDeveloperName ?? '').trim();
108
+ const agentTemplate = String(rawAgentTemplate ?? '').trim();
91
109
  const noId = notSet(botDefinitionId);
92
110
  const noName = notSet(developerName);
111
+ const noTemplate = notSet(agentTemplate);
93
112
 
94
113
  const NOT_EXISTS = {
95
114
  exists: false,
@@ -103,11 +122,12 @@ const NOT_EXISTS = {
103
122
  needsActivation: false,
104
123
  };
105
124
 
106
- // Nothing to key on — no botDefinitionId AND no developerName fallback. There is
107
- // no way to detect an existing agent, so this is the create path. (The workflow
108
- // should always pass the collected developerName so the fallback below can run;
109
- // this bare branch only fires when neither key is supplied.)
110
- if (noId && noName) {
125
+ // Nothing to key on — no botDefinitionId AND no AgentTemplate fallback AND no
126
+ // DeveloperName fallback. There is no way to detect an existing agent, so this is
127
+ // the create path. (The workflow should always pass the collected developerName
128
+ // and the template's id/AgentTemplate so the fallbacks below can run; this bare
129
+ // branch only fires when none of the three keys is supplied.)
130
+ if (noId && noTemplate && noName) {
111
131
  process.stdout.write(JSON.stringify(NOT_EXISTS, null, 2) + '\n');
112
132
  process.exit(0);
113
133
  }
@@ -131,19 +151,26 @@ if (!data || (typeof data.status === 'number' && data.status !== 0) || data.resu
131
151
  const records = Array.isArray(data.result?.records) ? data.result.records : [];
132
152
 
133
153
  // PRIMARY key: the template's `botDefinitionId` (the platform's authoritative
134
- // template→BotDefinition link) — catches the pre-provisioned broad agent whose
135
- // live DeveloperName differs from this skill's collected guess.
136
- // FALLBACK: the collected DeveloperName. Self-created agents are NOT
137
- // back-filled — the ITSM create path never stamps `templateName`, so the
138
- // platform never joins the agent-templates row to its BotDefinition and
139
- // `botDefinitionId` stays null on every later read — so DeveloperName is their
140
- // only guard. We ALSO fall back to DeveloperName when a PRESENT botDefinitionId
141
- // matches nothing (a dangling link whose target BotDefinition was deleted): the
142
- // present-id Phase-2 SOQL reads `WHERE Id=... OR DeveloperName=...`, so a stale
143
- // link still surfaces the live same-name agent instead of slipping through to the
144
- // create path and colliding with DUPLICATE_VALUE. Try the Id first, then the
145
- // name; each filter is self-checking on top of the SOQL WHERE so a wrong-keyed
146
- // read can't false-positive.
154
+ // template→BotDefinition link) — catches a pre-provisioned agent whose live
155
+ // DeveloperName differs from this skill's collected guess. For the broad Employee
156
+ // agent this is null (see header), so in practice the AgentTemplate fallback below
157
+ // is what fires.
158
+ // FALLBACK: the BotDefinition's own `AgentTemplate`, then the collected
159
+ // DeveloperName. `AgentTemplate` is the OOTB source-template API name the platform
160
+ // stamps on a template-instantiated agent — the reliable "an agent from THIS
161
+ // template already exists" signal for the pre-provisioned broad/specialized
162
+ // agents, independent of any DeveloperName rename (the pre-provisioned
163
+ // `IT_Service_Employee` carries `AgentTemplate=svc_emp_intelligence__ItEmployeeAssistance`
164
+ // even though its DeveloperName differs from this skill's guess). Self-created
165
+ // agents are NOT stamped (the ITSM create path never sets `templateName`), so their
166
+ // `AgentTemplate` is null and DeveloperName is their guard. We ALSO fall back this
167
+ // way when a PRESENT botDefinitionId matches nothing (a dangling link whose target
168
+ // BotDefinition was deleted): the present-id Phase-2 SOQL reads `WHERE Id=... OR
169
+ // AgentTemplate=... OR DeveloperName=...`, so a stale link still surfaces the live
170
+ // agent instead of slipping through to the create path and colliding with
171
+ // DUPLICATE_VALUE. Try the Id first, then the template, then the name; each filter
172
+ // is self-checking on top of the SOQL WHERE so a wrong-keyed read can't
173
+ // false-positive.
147
174
  let matches = [];
148
175
  let matchedBy = null;
149
176
  if (!noId) {
@@ -151,12 +178,40 @@ if (!noId) {
151
178
  matches = records.filter((r) => idKey(r?.Id) === wantedIdKey);
152
179
  if (matches.length) matchedBy = 'botDefinitionId';
153
180
  }
181
+ // FIRST FALLBACK: the BotDefinition's `AgentTemplate` (source template API name).
182
+ // The broad pre-provisioned "IT Service Employee" agent lives under DeveloperName
183
+ // `IT_Service_Employee` — which the DeveloperName filter below misses when this
184
+ // skill collects its default `IT_Service_Employee_Agent` — while its template row
185
+ // carries no botDefinitionId, so the Id and DeveloperName keys both miss and the
186
+ // create then collides with DUPLICATE_VALUE. Its `AgentTemplate` is
187
+ // `svc_emp_intelligence__ItEmployeeAssistance` (the template this skill installs),
188
+ // so matching on it recovers the live agent reliably. SOQL text comparison is
189
+ // case-insensitive, so compare the same way. `AgentTemplate` is NOT unique (a
190
+ // template can be instantiated more than once), so if more than one record shares
191
+ // it we keep them all but surface an Active version first (see below) — the report
192
+ // should treat the already-active one as the real match.
193
+ if (!matches.length && !noTemplate) {
194
+ const wantedTemplate = agentTemplate.toLowerCase();
195
+ matches = records.filter((r) => String(r?.AgentTemplate ?? '').trim().toLowerCase() === wantedTemplate);
196
+ if (matches.length) matchedBy = 'agentTemplate';
197
+ }
198
+ // SECOND FALLBACK: the collected DeveloperName — the guard for self-created agents
199
+ // (null AgentTemplate). DeveloperName uniqueness is case-insensitive; compare
200
+ // accordingly.
154
201
  if (!matches.length && !noName) {
155
- // DeveloperName uniqueness is case-insensitive; compare accordingly.
156
202
  const wantedName = developerName.toLowerCase();
157
203
  matches = records.filter((r) => String(r?.DeveloperName ?? '').trim().toLowerCase() === wantedName);
158
204
  if (matches.length) matchedBy = 'developerName';
159
205
  }
206
+ if (matchedBy === 'agentTemplate' && matches.length > 1) {
207
+ // Prefer a record whose latest BotVersion is Active so `matches[0]` (used for
208
+ // latestVersion/agentId below) reflects the live agent rather than a stale draft.
209
+ matches = [...matches].sort((a, b) => {
210
+ const aActive = a?.BotVersions?.records?.[0]?.Status === 'Active' ? 0 : 1;
211
+ const bActive = b?.BotVersions?.records?.[0]?.Status === 'Active' ? 0 : 1;
212
+ return aActive - bActive;
213
+ });
214
+ }
160
215
  const exists = matches.length > 0;
161
216
 
162
217
  // The BotVersions child subquery (if present in the SOQL) surfaces the latest
@@ -171,12 +226,15 @@ const needsActivation = exists && latestVersionStatus === 'Inactive';
171
226
  process.stdout.write(JSON.stringify({
172
227
  exists,
173
228
  count: matches.length,
174
- // Which key matched — `botDefinitionId` (primary) or `developerName` (the
175
- // fallback for self-created agents). null when nothing matched.
229
+ // Which key matched — `botDefinitionId` (primary), `agentTemplate` (the
230
+ // reliable catcher for the pre-provisioned broad agent whose DeveloperName
231
+ // differs from the collected guess), or `developerName` (the fallback for
232
+ // self-created agents). null when nothing matched.
176
233
  matchedBy: exists ? matchedBy : null,
177
234
  agentId: exists ? (matches[0].Id ?? null) : null,
178
- // On a DeveloperName-fallback hit this surfaces the live BotDefinition Id the
179
- // template row was missing; on the Id path it echoes the queried id.
235
+ // On an AgentTemplate- or DeveloperName-fallback hit this surfaces the live
236
+ // BotDefinition Id the template row was missing; on the Id path it echoes the
237
+ // queried id.
180
238
  botDefinitionId: exists ? (matches[0].Id ?? null) : (noId ? null : botDefinitionId),
181
239
  developerName: exists ? (matches[0].DeveloperName ?? null) : null,
182
240
  latestVersionId: latestVersion?.Id ?? null,