@salesforce/afv-skills 1.47.0 → 1.49.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 (160) hide show
  1. package/package.json +1 -1
  2. package/skills/education-cloud-domain-configure/SKILL.md +234 -0
  3. package/skills/education-cloud-domain-configure/references/dependent-features.md +198 -0
  4. package/skills/education-cloud-domain-configure/references/domain-features/academic-operations.md +386 -0
  5. package/skills/education-cloud-domain-configure/references/domain-features/alumni-relations.md +155 -0
  6. package/skills/education-cloud-domain-configure/references/domain-features/cross-domain-dependencies.md +18 -0
  7. package/skills/education-cloud-domain-configure/references/domain-features/industriessettings-toggle-reference.md +43 -0
  8. package/skills/education-cloud-domain-configure/references/domain-features/mentoring.md +198 -0
  9. package/skills/education-cloud-domain-configure/references/domain-features/recruitment-admissions.md +315 -0
  10. package/skills/education-cloud-domain-configure/references/domain-features/student-management.md +155 -0
  11. package/skills/education-cloud-domain-configure/references/domain-features/student-success.md +249 -0
  12. package/skills/education-cloud-domain-configure/references/domain-features.md +27 -0
  13. package/skills/education-cloud-domain-configure/references/gotchas-extended.md +23 -0
  14. package/skills/education-cloud-domain-configure/references/toggle-write-path.md +67 -0
  15. package/skills/education-cloud-domain-configure/scripts/check_license_provisioned.py +62 -0
  16. package/skills/education-cloud-domain-configure/scripts/check_org_edition.py +44 -0
  17. package/skills/education-cloud-domain-configure/scripts/check_permission_assigned.py +39 -0
  18. package/skills/education-cloud-multi-campus-configure/SKILL.md +2 -0
  19. package/skills/education-cloud-multi-campus-configure/references/foundation_prerequisites.md +4 -3
  20. package/skills/service-agentforce-channel-configure/SKILL.md +33 -28
  21. package/skills/service-agentforce-human-escalation-configure/SKILL.md +205 -0
  22. package/skills/service-agentforce-human-escalation-configure/assets/escalation-thresholds.instructions.md +37 -0
  23. package/skills/service-agentforce-human-escalation-configure/assets/force-app/main/default/flows/Human_Escalation_Outbound_Flow.flow-meta.xml +173 -0
  24. package/skills/service-agentforce-human-escalation-configure/assets/package.xml +8 -0
  25. package/skills/service-agentforce-human-escalation-configure/assets/sfdx-project.json +5 -0
  26. package/skills/service-agentforce-human-escalation-configure/references/classifier-contracts.md +103 -0
  27. package/skills/service-agentforce-human-escalation-configure/references/escalation-mechanism.md +63 -0
  28. package/skills/service-agentforce-human-escalation-configure/references/failure-policy.md +44 -0
  29. package/skills/service-agentforce-human-escalation-configure/references/nga-escalation.md +57 -0
  30. package/skills/service-agentforce-human-escalation-configure/references/runtime-verification.md +72 -0
  31. package/skills/service-agentforce-human-escalation-configure/scripts/classify-agent-active.mjs +49 -0
  32. package/skills/service-agentforce-human-escalation-configure/scripts/classify-nga-escalation.mjs +166 -0
  33. package/skills/service-agentforce-human-escalation-configure/scripts/extract-outbound-route.mjs +67 -0
  34. package/skills/service-agentforce-human-escalation-configure/scripts/patch-escalation-surfaces.mjs +131 -0
  35. package/skills/service-agentforce-human-escalation-configure/scripts/tests/__init__.py +0 -0
  36. package/skills/service-agentforce-human-escalation-configure/scripts/tests/_bootstrap.py +249 -0
  37. package/skills/service-agentforce-human-escalation-configure/scripts/tests/test_escalation_contracts.py +899 -0
  38. package/skills/service-agentforce-human-escalation-configure/scripts/verify-and-configure.sh +705 -0
  39. package/skills/service-agentforce-human-escalation-configure/scripts/verify-escalation-config.mjs +89 -0
  40. package/skills/service-de-channel-routing-configure/SKILL.md +13 -5
  41. package/skills/service-de-channel-routing-configure/scripts/tests/test_skill_contract.py +25 -0
  42. package/skills/service-itsm-agentic-setup-agentforce-coordinate/SKILL.md +23 -10
  43. package/skills/service-itsm-agentic-setup-agentforce-coordinate/examples/output-templates.md +63 -48
  44. package/skills/service-itsm-agentic-setup-agentforce-coordinate/scripts/verify-child-verdict.mjs +6 -2
  45. package/skills/service-omni-agent-users-create/SKILL.md +96 -0
  46. package/skills/service-omni-agent-users-create/assets/create-users.apex.template +80 -0
  47. package/skills/service-omni-agent-users-create/references/apex-patterns.md +135 -0
  48. package/skills/service-omni-agent-users-create/references/apex-template-notes.md +40 -0
  49. package/skills/service-omni-agent-users-create/scripts/detect-and-create.sh +206 -0
  50. package/skills/service-omni-agent-users-create/scripts/detect-existing.sh +122 -0
  51. package/skills/service-omni-agent-users-create/scripts/run-create.sh +298 -0
  52. package/skills/service-omni-agent-users-create/scripts/tests/test_user_create_security.py +171 -0
  53. package/skills/service-omni-agent-work-sharing-configure/SKILL.md +78 -0
  54. package/skills/service-omni-agent-work-sharing-configure/references/api-notes.md +31 -0
  55. package/skills/service-omni-agent-work-sharing-configure/scripts/configure-and-report.sh +186 -0
  56. package/skills/service-omni-agent-work-sharing-configure/scripts/tests/_bootstrap.py +120 -0
  57. package/skills/service-omni-agent-work-sharing-configure/scripts/tests/test_agent_work_sharing_contracts.py +93 -0
  58. package/skills/service-omni-base-settings-configure/SKILL.md +77 -0
  59. package/skills/service-omni-base-settings-configure/assets/force-app/main/default/settings/OmniChannel.settings-meta.xml +8 -0
  60. package/skills/service-omni-base-settings-configure/assets/package.xml +8 -0
  61. package/skills/service-omni-base-settings-configure/assets/sfdx-project.json +12 -0
  62. package/skills/service-omni-base-settings-configure/references/api-notes.md +121 -0
  63. package/skills/service-omni-base-settings-configure/scripts/configure-and-report.sh +164 -0
  64. package/skills/service-omni-base-settings-configure/scripts/detect-existing.sh +69 -0
  65. package/skills/service-omni-channel-setup-coordinate/SKILL.md +168 -0
  66. package/skills/service-omni-channel-setup-coordinate/examples/output-templates.md +161 -0
  67. package/skills/service-omni-channel-setup-coordinate/references/gap-catalog.md +99 -0
  68. package/skills/service-omni-channel-setup-coordinate/scripts/integration-driver.sh +1542 -0
  69. package/skills/service-omni-channel-setup-coordinate/scripts/tests/__init__.py +0 -0
  70. package/skills/service-omni-channel-setup-coordinate/scripts/tests/_bootstrap.py +223 -0
  71. package/skills/service-omni-channel-setup-coordinate/scripts/tests/test_omni_contracts.py +804 -0
  72. package/skills/service-omni-command-center-analyze/SKILL.md +84 -0
  73. package/skills/service-omni-command-center-analyze/references/api-notes.md +54 -0
  74. package/skills/service-omni-command-center-analyze/scripts/analyze.sh +198 -0
  75. package/skills/service-omni-command-center-analyze/scripts/tests/_bootstrap.py +115 -0
  76. package/skills/service-omni-command-center-analyze/scripts/tests/test_command_center_analyze_contracts.py +165 -0
  77. package/skills/service-omni-permission-set-assign/SKILL.md +88 -0
  78. package/skills/service-omni-permission-set-assign/assets/package.xml +8 -0
  79. package/skills/service-omni-permission-set-assign/references/api-notes.md +143 -0
  80. package/skills/service-omni-permission-set-assign/scripts/verify-and-assign.sh +397 -0
  81. package/skills/service-omni-presence-status-deploy/SKILL.md +87 -0
  82. package/skills/service-omni-presence-status-deploy/assets/package.xml +12 -0
  83. package/skills/service-omni-presence-status-deploy/references/api-notes.md +115 -0
  84. package/skills/service-omni-presence-status-deploy/scripts/deploy-and-report.sh +422 -0
  85. package/skills/service-omni-presence-user-config-deploy/SKILL.md +80 -0
  86. package/skills/service-omni-presence-user-config-deploy/references/api-notes.md +60 -0
  87. package/skills/service-omni-presence-user-config-deploy/scripts/deploy-and-report.sh +261 -0
  88. package/skills/service-omni-presence-user-config-deploy/scripts/tests/_bootstrap.py +106 -0
  89. package/skills/service-omni-presence-user-config-deploy/scripts/tests/test_presence_user_config_contracts.py +142 -0
  90. package/skills/service-omni-queue-deploy/SKILL.md +91 -0
  91. package/skills/service-omni-queue-deploy/references/api-notes.md +140 -0
  92. package/skills/service-omni-queue-deploy/scripts/verify-and-align.sh +470 -0
  93. package/skills/service-omni-queue-members-assign/SKILL.md +93 -0
  94. package/skills/service-omni-queue-members-assign/references/api-notes.md +144 -0
  95. package/skills/service-omni-queue-members-assign/scripts/tests/_bootstrap.py +67 -0
  96. package/skills/service-omni-queue-members-assign/scripts/tests/test_queue_members_contracts.py +73 -0
  97. package/skills/service-omni-queue-members-assign/scripts/verify-and-bind.sh +362 -0
  98. package/skills/service-omni-queue-routing-config-deploy/SKILL.md +101 -0
  99. package/skills/service-omni-queue-routing-config-deploy/references/api-notes.md +127 -0
  100. package/skills/service-omni-queue-routing-config-deploy/scripts/tests/_bootstrap.py +90 -0
  101. package/skills/service-omni-queue-routing-config-deploy/scripts/tests/test_queue_routing_config_contracts.py +70 -0
  102. package/skills/service-omni-queue-routing-config-deploy/scripts/upsert-and-report.sh +538 -0
  103. package/skills/service-omni-routing-flow-deploy/SKILL.md +111 -0
  104. package/skills/service-omni-routing-flow-deploy/assets/force-app/main/default/flows/Omni_Route_Case_Trigger.SkillsBased.flow-meta.xml +94 -0
  105. package/skills/service-omni-routing-flow-deploy/assets/force-app/main/default/flows/Omni_Route_Case_Trigger.flow-meta.xml +88 -0
  106. package/skills/service-omni-routing-flow-deploy/assets/force-app/main/default/flows/Omni_Route_Cases.flow-meta.xml +144 -0
  107. package/skills/service-omni-routing-flow-deploy/assets/force-app/main/default/flows/Omni_Route_VoiceCall_Trigger.SkillsBased.flow-meta.xml +94 -0
  108. package/skills/service-omni-routing-flow-deploy/assets/force-app/main/default/flows/Omni_Route_VoiceCall_Trigger.flow-meta.xml +88 -0
  109. package/skills/service-omni-routing-flow-deploy/assets/force-app/main/default/flows/Omni_Route_VoiceCalls.flow-meta.xml +144 -0
  110. package/skills/service-omni-routing-flow-deploy/assets/package.xml +11 -0
  111. package/skills/service-omni-routing-flow-deploy/assets/sfdx-project.json +11 -0
  112. package/skills/service-omni-routing-flow-deploy/references/api-notes.md +265 -0
  113. package/skills/service-omni-routing-flow-deploy/scripts/deploy-and-report.sh +684 -0
  114. package/skills/service-omni-service-channel-configure/SKILL.md +83 -0
  115. package/skills/service-omni-service-channel-configure/assets/force-app/main/default/serviceChannels/Cases.serviceChannel-meta.xml +9 -0
  116. package/skills/service-omni-service-channel-configure/assets/force-app/main/default/serviceChannels/Incidents.serviceChannel-meta.xml +9 -0
  117. package/skills/service-omni-service-channel-configure/assets/force-app/main/default/serviceChannels/MessagingSessions.serviceChannel-meta.xml +9 -0
  118. package/skills/service-omni-service-channel-configure/assets/force-app/main/default/serviceChannels/VoiceCalls.serviceChannel-meta.xml +9 -0
  119. package/skills/service-omni-service-channel-configure/assets/package.xml +11 -0
  120. package/skills/service-omni-service-channel-configure/assets/sfdx-project.json +12 -0
  121. package/skills/service-omni-service-channel-configure/references/api-notes.md +84 -0
  122. package/skills/service-omni-service-channel-configure/scripts/deploy-and-report.sh +383 -0
  123. package/skills/service-omni-sidebar-configure/SKILL.md +77 -0
  124. package/skills/service-omni-sidebar-configure/references/api-notes.md +51 -0
  125. package/skills/service-omni-sidebar-configure/scripts/enable-and-report.sh +315 -0
  126. package/skills/service-omni-sidebar-configure/scripts/tests/_bootstrap.py +142 -0
  127. package/skills/service-omni-sidebar-configure/scripts/tests/test_sidebar_contracts.py +139 -0
  128. package/skills/service-omni-skills-based-routing-configure/SKILL.md +81 -0
  129. package/skills/service-omni-skills-based-routing-configure/references/api-notes.md +50 -0
  130. package/skills/service-omni-skills-based-routing-configure/scripts/configure-and-report.sh +270 -0
  131. package/skills/service-omni-skills-based-routing-configure/scripts/tests/_bootstrap.py +120 -0
  132. package/skills/service-omni-skills-based-routing-configure/scripts/tests/test_sbr_contracts.py +129 -0
  133. package/skills/service-omni-supervisor-config-deploy/SKILL.md +97 -0
  134. package/skills/service-omni-supervisor-config-deploy/assets/force-app/main/default/omniSupervisorConfigs/Omni_Supervisor.omniSupervisorConfig-meta.xml +9 -0
  135. package/skills/service-omni-supervisor-config-deploy/assets/package.xml +8 -0
  136. package/skills/service-omni-supervisor-config-deploy/assets/sfdx-project.json +12 -0
  137. package/skills/service-omni-supervisor-config-deploy/references/api-notes.md +205 -0
  138. package/skills/service-omni-supervisor-config-deploy/scripts/deploy-and-report.sh +586 -0
  139. package/skills/service-omni-supervisor-config-deploy/scripts/tests/_bootstrap.py +153 -0
  140. package/skills/service-omni-supervisor-config-deploy/scripts/tests/test_supervisor_config_contracts.py +116 -0
  141. package/skills/service-omni-supervisor-permset-assign/SKILL.md +88 -0
  142. package/skills/service-omni-supervisor-permset-assign/references/api-notes.md +48 -0
  143. package/skills/service-omni-supervisor-permset-assign/scripts/verify-and-assign.sh +316 -0
  144. package/skills/service-omni-supervisor-surface-deploy/SKILL.md +85 -0
  145. package/skills/service-omni-supervisor-surface-deploy/references/api-notes.md +47 -0
  146. package/skills/service-omni-supervisor-surface-deploy/scripts/deploy-and-report.sh +216 -0
  147. package/skills/service-omni-supervisor-surface-deploy/scripts/tests/_bootstrap.py +95 -0
  148. package/skills/service-omni-supervisor-surface-deploy/scripts/tests/test_supervisor_surface_contracts.py +154 -0
  149. package/skills/service-omni-supervisor-users-create/SKILL.md +92 -0
  150. package/skills/service-omni-supervisor-users-create/assets/create-supervisors.apex.template +72 -0
  151. package/skills/service-omni-supervisor-users-create/references/apex-patterns.md +73 -0
  152. package/skills/service-omni-supervisor-users-create/references/apex-template-notes.md +41 -0
  153. package/skills/service-omni-supervisor-users-create/scripts/detect-and-create.sh +197 -0
  154. package/skills/service-omni-supervisor-users-create/scripts/detect-existing.sh +112 -0
  155. package/skills/service-omni-supervisor-users-create/scripts/run-create.sh +296 -0
  156. package/skills/service-omni-work-skill-routing-configure/SKILL.md +80 -0
  157. package/skills/service-omni-work-skill-routing-configure/references/api-notes.md +67 -0
  158. package/skills/service-omni-work-skill-routing-configure/scripts/configure-and-report.sh +262 -0
  159. package/skills/service-omni-work-skill-routing-configure/scripts/tests/_bootstrap.py +121 -0
  160. package/skills/service-omni-work-skill-routing-configure/scripts/tests/test_work_skill_routing_contracts.py +161 -0
@@ -0,0 +1,470 @@
1
+ #!/usr/bin/env bash
2
+ # verify-and-align.sh - verify or (with --create-if-missing) create the Queue for a routable sObject and align its QueueRoutingConfigId to a target QRC; production-guarded, idempotent. Args, queue-selection policy, exit codes: SKILL.md.
3
+
4
+ set -euo pipefail
5
+
6
+ if [ $# -lt 1 ]; then
7
+ echo '{"error":"Missing required argument: org-alias","usage":"bash verify-and-align.sh <org-alias> [Case|Incident|MessagingSession|VoiceCall] [routing_config_dn] [queue_developer_name] [--create-if-missing]"}' >&2
8
+ exit 1
9
+ fi
10
+
11
+ # Extract --create-if-missing from anywhere in the arg list, leaving positionals intact.
12
+ CREATE_IF_MISSING="${QUEUE_CREATE_IF_MISSING:-0}"
13
+ POSITIONAL=()
14
+ for arg in "$@"; do
15
+ case "$arg" in
16
+ --create-if-missing) CREATE_IF_MISSING="1" ;;
17
+ *) POSITIONAL+=("$arg") ;;
18
+ esac
19
+ done
20
+ set -- "${POSITIONAL[@]}"
21
+
22
+ ORG="$1"
23
+ SOBJECT_TYPE="${2:-Case}"
24
+ ROUTING_CONFIG_DN="${3:-}"
25
+ EXPLICIT_QUEUE_DN="${4:-}"
26
+ API_VERSION="v66.0"
27
+ QUEUE_CREATED="false"
28
+
29
+ # Validate explicit queue DeveloperName shape early (prevents SOQL-injection via arg).
30
+ if [ -n "$EXPLICIT_QUEUE_DN" ] && ! [[ "$EXPLICIT_QUEUE_DN" =~ ^[A-Za-z0-9_]{1,80}$ ]]; then
31
+ echo "{\"error\":\"Invalid queue_developer_name '$EXPLICIT_QUEUE_DN'. Allowed: A-Z a-z 0-9 _ (max 80).\"}" >&2
32
+ exit 1
33
+ fi
34
+
35
+ # ROUTING_CONFIG_DN is interpolated into SOQL, so validate its DeveloperName shape before any sf call.
36
+ if [ -n "$ROUTING_CONFIG_DN" ] && ! [[ "$ROUTING_CONFIG_DN" =~ ^[A-Za-z][A-Za-z0-9_]{0,79}$ ]]; then
37
+ echo "{\"error\":\"Invalid routing_config_developer_name '$ROUTING_CONFIG_DN'. Must start with a letter and contain only letters, digits, and underscores (max 80 chars).\"}" >&2
38
+ exit 1
39
+ fi
40
+
41
+ case "$SOBJECT_TYPE" in
42
+ Case)
43
+ [ -z "$ROUTING_CONFIG_DN" ] && ROUTING_CONFIG_DN="Case_Routing_Config"
44
+ SUGGESTED_QUEUE_DN="CaseQueue"
45
+ ;;
46
+ Incident)
47
+ SUGGESTED_QUEUE_DN="IncidentQueue"
48
+ ;;
49
+ MessagingSession)
50
+ SUGGESTED_QUEUE_DN="MessagingQueue"
51
+ ;;
52
+ VoiceCall)
53
+ SUGGESTED_QUEUE_DN="VoiceQueue"
54
+ ;;
55
+ *)
56
+ echo "{\"error\":\"Unsupported sobject_type: '$SOBJECT_TYPE'. Supported: Case, Incident, MessagingSession, VoiceCall\"}" >&2
57
+ exit 2
58
+ ;;
59
+ esac
60
+
61
+ if ! sf org display --target-org "$ORG" --json >/dev/null 2>&1; then
62
+ echo "{\"error\":\"Org alias '$ORG' is not authenticated.\"}" >&2
63
+ exit 1
64
+ fi
65
+
66
+ # Write guard: mutates Group.QueueRoutingConfigId, so self-enforce the production-org guard (directly invokable).
67
+ ORG_JSON=$(sf data query --target-org "$ORG" \
68
+ --query "SELECT IsSandbox, TrialExpirationDate, OrganizationType FROM Organization LIMIT 1" \
69
+ --json 2>/dev/null) || {
70
+ echo "{\"error\":\"Failed to query Organization for safe_to_write guard: $(printf '%s' "$ORG_JSON" | head -c 300)\"}" >&2
71
+ exit 1
72
+ }
73
+ IS_SANDBOX=$(echo "$ORG_JSON" | jq -r '.result.records[0].IsSandbox')
74
+ TRIAL_EXP=$(echo "$ORG_JSON" | jq -r '.result.records[0].TrialExpirationDate // "null"')
75
+ ORG_TYPE=$(echo "$ORG_JSON" | jq -r '.result.records[0].OrganizationType')
76
+
77
+ SAFE_TO_WRITE=false
78
+ if [ "$IS_SANDBOX" = "true" ] \
79
+ || [ "$TRIAL_EXP" != "null" ] \
80
+ || [ "$ORG_TYPE" = "Developer Edition" ] \
81
+ || [ "$ORG_TYPE" = "Base Edition" ]; then
82
+ SAFE_TO_WRITE=true
83
+ fi
84
+
85
+ if [ "$SAFE_TO_WRITE" != "true" ]; then
86
+ echo "{\"skill\":\"service-omni-queue-deploy\",\"status\":\"blocked\",\"blocking_issue\":\"Refusing to align queue routing on a real production customer org (IsSandbox=$IS_SANDBOX, TrialExpirationDate=$TRIAL_EXP, OrganizationType=$ORG_TYPE). Target a sandbox, trial CDO, or Developer Edition org.\",\"safe_to_write\":false}" >&2
87
+ exit 1
88
+ fi
89
+
90
+ # Escape the five XML predefined entities so a caller-supplied QUEUE_LABEL with & < > " ' cannot
91
+ # break or inject into the hand-built Queue metadata XML below.
92
+ xml_escape() {
93
+ local s="$1"
94
+ s="${s//&/&amp;}"; s="${s//</&lt;}"; s="${s//>/&gt;}"; s="${s//\"/&quot;}"; s="${s//\'/&apos;}"
95
+ printf '%s' "$s"
96
+ }
97
+
98
+ # create_queue - create a Queue (Group Type=Queue) bound to $SOBJECT_TYPE. Idempotent (deploy upserts
99
+ # by fullName); sets QUEUE_CREATED, or calls emit_blocked on failure.
100
+ create_queue() {
101
+ local dn="$1"
102
+ local label="${QUEUE_LABEL:-}"
103
+ if [ -z "$label" ]; then
104
+ label="$(printf '%s' "$dn" | tr '_' ' ')"
105
+ fi
106
+
107
+ # Collision guard: a Queue:<dn> deploy replaces the whole component, dropping an existing queue's
108
+ # other bindings/members. If the queue already exists, additively bind the sObject via Data API instead.
109
+ local existing_json existing_id
110
+ existing_json=$(sf data query --target-org "$ORG" \
111
+ --query "SELECT Id FROM Group WHERE DeveloperName='$dn' AND Type='Queue'" --json 2>/dev/null || echo '{}')
112
+ existing_id=$(echo "$existing_json" | jq -r '.result.records[0].Id // ""' 2>/dev/null || echo "")
113
+ if [ -n "$existing_id" ]; then
114
+ local bound_json
115
+ bound_json=$(sf data query --target-org "$ORG" \
116
+ --query "SELECT Id FROM QueueSobject WHERE QueueId='$existing_id' AND SobjectType='$SOBJECT_TYPE'" \
117
+ --json 2>/dev/null || echo '{}')
118
+ if [ "$(echo "$bound_json" | jq -r '.result.totalSize // 0' 2>/dev/null || echo 0)" -gt 0 ]; then
119
+ # Already exists and already bound - nothing to do.
120
+ QUEUE_CREATED="false"
121
+ return 0
122
+ fi
123
+ local qs_post qs_err
124
+ qs_post=$(sf api request rest --target-org "$ORG" \
125
+ "/services/data/$API_VERSION/sobjects/QueueSobject" --method POST \
126
+ --body "$(jq -n --arg q "$existing_id" --arg s "$SOBJECT_TYPE" '{QueueId:$q, SobjectType:$s}')" 2>/dev/null || true)
127
+ if [ "$(echo "$qs_post" | jq -r '.success // false' 2>/dev/null)" != "true" ]; then
128
+ qs_err=$(echo "$qs_post" | jq -r '.[0].message // .message // "unknown error"' 2>/dev/null)
129
+ emit_blocked "Queue '$dn' already exists but is bound to a different object, and additively binding '$SOBJECT_TYPE' failed: $qs_err. Refusing to overwrite an existing queue via Metadata API (that would drop its other object bindings and members)." \
130
+ "Setup → Users → Queues → '$dn' → Supported Objects → add '$SOBJECT_TYPE' → Save" "$existing_id" "$dn"
131
+ fi
132
+ QUEUE_CREATED="true"
133
+ return 0
134
+ fi
135
+
136
+ local work; work="$(mktemp -d)"
137
+ mkdir -p "$work/force-app/main/default/queues"
138
+ cat > "$work/sfdx-project.json" <<'JSON'
139
+ {
140
+ "packageDirectories": [{ "path": "force-app", "default": true }],
141
+ "namespace": "",
142
+ "sfdcLoginUrl": "https://login.salesforce.com",
143
+ "sourceApiVersion": "66.0"
144
+ }
145
+ JSON
146
+ # Queue XML: fullName from the file name; <name> is the label (Queue has no <label>). Child order
147
+ # follows the MD-API schema (doesSendEmailToMembers, name, queueSobject). QRC is bound later at
148
+ # Step 3 (Data API). Full schema note: references/api-notes.md.
149
+ cat > "$work/force-app/main/default/queues/${dn}.queue-meta.xml" <<XML
150
+ <?xml version="1.0" encoding="UTF-8"?>
151
+ <Queue xmlns="http://soap.sforce.com/2006/04/metadata">
152
+ <doesSendEmailToMembers>false</doesSendEmailToMembers>
153
+ <name>$(xml_escape "$label")</name>
154
+ <queueSobject>
155
+ <sobjectType>${SOBJECT_TYPE}</sobjectType>
156
+ </queueSobject>
157
+ </Queue>
158
+ XML
159
+
160
+ local deploy_json
161
+ deploy_json=$(cd "$work" && sf project deploy start --target-org "$ORG" \
162
+ --metadata "Queue:${dn}" --json 2>/dev/null || true)
163
+ rm -rf "$work"
164
+
165
+ local ok
166
+ ok=$(echo "$deploy_json" | jq -r '.result.success // false' 2>/dev/null)
167
+ if [ "$ok" != "true" ]; then
168
+ local err
169
+ err=$(echo "$deploy_json" | jq -r '.result.details.componentFailures // [] | map(.problem) | join("; ")' 2>/dev/null)
170
+ [ -z "$err" ] || [ "$err" = "null" ] && err=$(echo "$deploy_json" | jq -r '.message // "Unknown deploy error"' 2>/dev/null)
171
+ emit_blocked "Queue creation for '$dn' (sObject $SOBJECT_TYPE) failed via Metadata API: $err" "" "null" "$dn"
172
+ fi
173
+ QUEUE_CREATED="true"
174
+ }
175
+
176
+ emit_blocked() {
177
+ local msg="$1"
178
+ local click_path="${2:-}"
179
+ local queue_id="${3:-null}"
180
+ local queue_dn="${4:-null}"
181
+
182
+ local manual_actions='[]'
183
+ if [ -n "$click_path" ]; then
184
+ manual_actions=$(jq -n --arg cp "$click_path" '[{id: "MANUAL_QUEUE_CREATE", title: "Manual queue creation required", click_path: $cp}]')
185
+ fi
186
+
187
+ jq -n \
188
+ --arg msg "$msg" \
189
+ --arg sobject "$SOBJECT_TYPE" \
190
+ --arg dn "$queue_dn" \
191
+ --arg qid "$queue_id" \
192
+ --arg rc_dn "$ROUTING_CONFIG_DN" \
193
+ --argjson ma "$manual_actions" \
194
+ '{
195
+ skill: "service-omni-queue-deploy",
196
+ status: "blocked",
197
+ sobject_type: $sobject,
198
+ developer_name: (if $dn == "null" then null else $dn end),
199
+ id: (if $qid == "null" then null else $qid end),
200
+ queue_routing_config_id: null,
201
+ queue_routing_config_developer_name: (if $rc_dn == "" then null else $rc_dn end),
202
+ queue_sobjects: [],
203
+ before: {existed: (if $qid == "null" then false else true end), queue_routing_config_id: null, queue_sobjects: []},
204
+ manual_actions: $ma,
205
+ blocking_issue: $msg
206
+ }'
207
+ exit 1
208
+ }
209
+
210
+ # emit read-only plan preview JSON and exit 0 (PLAN_ONLY mode). Includes developer_name/id so the
211
+ # coordinator can chain the discovered queue into the queue-members detector, exactly as in --run.
212
+ emit_plan() {
213
+ local status="$1"
214
+ local detail="$2"
215
+ jq -n \
216
+ --arg status "$status" \
217
+ --arg detail "$detail" \
218
+ --arg sobject "$SOBJECT_TYPE" \
219
+ --arg dn "$QUEUE_DN" \
220
+ --arg qid "$QUEUE_ID" \
221
+ '{
222
+ skill: "service-omni-queue-deploy",
223
+ status: $status,
224
+ plan_mode: true,
225
+ plan_detail: $detail,
226
+ sobject_type: $sobject,
227
+ developer_name: $dn,
228
+ id: $qid,
229
+ manual_actions: [],
230
+ blocking_issue: null
231
+ }'
232
+ exit 0
233
+ }
234
+
235
+ # Step 0 - Prereq: target sObject exists on org
236
+ ENTITY_CHECK=$(sf data query --target-org "$ORG" \
237
+ --query "SELECT QualifiedApiName FROM EntityDefinition WHERE QualifiedApiName = '$SOBJECT_TYPE'" \
238
+ --json 2>/dev/null | jq -r '.result.totalSize // 0')
239
+
240
+ if [ "$ENTITY_CHECK" != "1" ]; then
241
+ CLICK=""
242
+ case "$SOBJECT_TYPE" in
243
+ Incident) CLICK="Enable the Incident Management feature via Setup, then re-run" ;;
244
+ MessagingSession) CLICK="Enable Enhanced Messaging via Setup → Messaging, then re-run" ;;
245
+ VoiceCall) CLICK="Enable Service Cloud Voice and set up a Contact Center via Setup → Voice, then re-run" ;;
246
+ Case) CLICK="Contact admin - Case sObject expected on all Service Cloud orgs" ;;
247
+ esac
248
+ emit_blocked "Target sObject '$SOBJECT_TYPE' is not queryable on this org" "$CLICK"
249
+ fi
250
+
251
+ # Step 1 - Select the Queue bound to the target sObject. Enumerate ALL bound queues (no LIMIT 1) so
252
+ # selection is deliberate; mutating the wrong queue's QueueRoutingConfigId could hijack another queue.
253
+ QSO_DISCOVERY=$(sf data query --target-org "$ORG" \
254
+ --query "SELECT QueueId, Queue.DeveloperName, Queue.Name FROM QueueSobject WHERE SobjectType='$SOBJECT_TYPE' ORDER BY Queue.DeveloperName" \
255
+ --json 2>/dev/null)
256
+
257
+ QSO_COUNT=$(echo "$QSO_DISCOVERY" | jq -r '.result.totalSize // 0' 2>/dev/null || echo "0")
258
+
259
+ if [ "$QSO_COUNT" = "0" ] || [ -z "$QSO_COUNT" ]; then
260
+ # Distinguish a genuine "zero bound queues" from a failed/inconclusive query.
261
+ if ! echo "$QSO_DISCOVERY" | jq -e '.result.totalSize != null' >/dev/null 2>&1; then
262
+ emit_blocked \
263
+ "Could not determine bound queues for '$SOBJECT_TYPE' - the QueueSobject query did not return a parseable result: $(echo "$QSO_DISCOVERY" | head -c 200). Not proceeding on an inconclusive read." \
264
+ ""
265
+ fi
266
+
267
+ if [ "$CREATE_IF_MISSING" = "1" ]; then
268
+ # PLAN_ONLY must not write: report the create as an action, don't perform it.
269
+ if [ "${PLAN_ONLY:-}" = "1" ]; then
270
+ QUEUE_DN="${EXPLICIT_QUEUE_DN:-$SUGGESTED_QUEUE_DN}"
271
+ QUEUE_ID="null"
272
+ emit_plan "action_needed" "No queue bound to $SOBJECT_TYPE; would CREATE queue '$QUEUE_DN' via Metadata API and bind it."
273
+ fi
274
+ # Create the canonical (or explicitly-named) queue, then re-discover so the rest of the
275
+ # script treats it exactly like a pre-existing bound queue.
276
+ CREATE_DN="${EXPLICIT_QUEUE_DN:-$SUGGESTED_QUEUE_DN}"
277
+ create_queue "$CREATE_DN"
278
+ QSO_DISCOVERY=$(sf data query --target-org "$ORG" \
279
+ --query "SELECT QueueId, Queue.DeveloperName, Queue.Name FROM QueueSobject WHERE SobjectType='$SOBJECT_TYPE' ORDER BY Queue.DeveloperName" \
280
+ --json 2>/dev/null)
281
+ QSO_COUNT=$(echo "$QSO_DISCOVERY" | jq -r '.result.totalSize // 0' 2>/dev/null || echo "0")
282
+ if [ "$QSO_COUNT" = "0" ] || [ -z "$QSO_COUNT" ]; then
283
+ emit_blocked \
284
+ "Created queue '$CREATE_DN' but no QueueSobject binding for '$SOBJECT_TYPE' is visible on re-query - creation did not converge." \
285
+ "" "null" "$CREATE_DN"
286
+ fi
287
+ else
288
+ emit_blocked \
289
+ "No Queue is bound to sObject '$SOBJECT_TYPE' on this org. Re-run with --create-if-missing to create one automatically, or create it manually." \
290
+ "Setup → Users → Queues → New → Label: '$SUGGESTED_QUEUE_DN', DeveloperName: '$SUGGESTED_QUEUE_DN'${ROUTING_CONFIG_DN:+, Routing Configuration: '$ROUTING_CONFIG_DN'}, Supported Objects: '$SOBJECT_TYPE' → Save"
291
+ fi
292
+ fi
293
+
294
+ # Distinct DeveloperNames bound to this sObject (a queue may have multiple QueueSobject rows).
295
+ CANDIDATE_DNS=$(echo "$QSO_DISCOVERY" | jq -c '[.result.records[].Queue.DeveloperName] | unique')
296
+
297
+ # --- Selection policy ---
298
+ QUEUE_DN=""
299
+ QUEUE_SOURCE=""
300
+
301
+ if [ -n "$EXPLICIT_QUEUE_DN" ]; then
302
+ # (a) Operator supplied an explicit queue - it MUST be bound to this sObject (or be created).
303
+ if [ "$(echo "$CANDIDATE_DNS" | jq --arg q "$EXPLICIT_QUEUE_DN" 'index($q) != null')" != "true" ]; then
304
+ if [ "$CREATE_IF_MISSING" = "1" ]; then
305
+ create_queue "$EXPLICIT_QUEUE_DN"
306
+ QSO_DISCOVERY=$(sf data query --target-org "$ORG" \
307
+ --query "SELECT QueueId, Queue.DeveloperName, Queue.Name FROM QueueSobject WHERE SobjectType='$SOBJECT_TYPE' ORDER BY Queue.DeveloperName" \
308
+ --json 2>/dev/null)
309
+ CANDIDATE_DNS=$(echo "$QSO_DISCOVERY" | jq -c '[.result.records[].Queue.DeveloperName] | unique')
310
+ if [ "$(echo "$CANDIDATE_DNS" | jq --arg q "$EXPLICIT_QUEUE_DN" 'index($q) != null')" != "true" ]; then
311
+ emit_blocked "Created queue '$EXPLICIT_QUEUE_DN' but its '$SOBJECT_TYPE' binding is not visible on re-query." "" "null" "$EXPLICIT_QUEUE_DN"
312
+ fi
313
+ else
314
+ emit_blocked \
315
+ "Requested queue '$EXPLICIT_QUEUE_DN' is not bound to sObject '$SOBJECT_TYPE'. Bound queues: $(echo "$CANDIDATE_DNS" | jq -c .). Re-run with --create-if-missing, bind it (Setup → Queues → '$EXPLICIT_QUEUE_DN' → Supported Objects add '$SOBJECT_TYPE'), or pass one of the bound names." \
316
+ ""
317
+ fi
318
+ fi
319
+ QUEUE_DN="$EXPLICIT_QUEUE_DN"
320
+ QUEUE_SOURCE="explicit"
321
+ elif [ "$(echo "$CANDIDATE_DNS" | jq --arg q "$SUGGESTED_QUEUE_DN" 'index($q) != null')" = "true" ]; then
322
+ # (b) The canonical queue for this sObject exists among the bound queues - prefer it.
323
+ QUEUE_DN="$SUGGESTED_QUEUE_DN"
324
+ QUEUE_SOURCE="canonical"
325
+ elif [ "$(echo "$CANDIDATE_DNS" | jq 'length')" = "1" ]; then
326
+ # (c) Exactly one bound queue - adopt it, and report that this was an adoption decision.
327
+ QUEUE_DN=$(echo "$CANDIDATE_DNS" | jq -r '.[0]')
328
+ QUEUE_SOURCE="sole_bound_queue"
329
+ else
330
+ # (d) Multiple bound queues and no explicit/canonical pick - refuse to guess.
331
+ emit_blocked \
332
+ "Multiple queues are bound to '$SOBJECT_TYPE' ($(echo "$CANDIDATE_DNS" | jq -c .)) and no canonical '$SUGGESTED_QUEUE_DN' exists. Refusing to guess which queue to modify. Re-run passing an explicit queue DeveloperName as the 4th argument." \
333
+ ""
334
+ fi
335
+
336
+ # Resolve the selected DeveloperName back to its QueueId.
337
+ QUEUE_ID=$(echo "$QSO_DISCOVERY" | jq -r --arg q "$QUEUE_DN" '[.result.records[] | select(.Queue.DeveloperName == $q)][0].QueueId')
338
+ if [ -z "$QUEUE_ID" ] || [ "$QUEUE_ID" = "null" ]; then
339
+ emit_blocked "Internal error: selected queue '$QUEUE_DN' but could not resolve its QueueId." ""
340
+ fi
341
+
342
+ # Step 2 - Query current queue QRC binding + full sObject list
343
+ Q_JSON=$(sf data query --target-org "$ORG" \
344
+ --query "SELECT Id, DeveloperName, QueueRoutingConfigId FROM Group WHERE Id='$QUEUE_ID' AND Type='Queue'" \
345
+ --json 2>/dev/null)
346
+
347
+ BEFORE_RC_ID=$(echo "$Q_JSON" | jq -r '.result.records[0].QueueRoutingConfigId // "null"')
348
+
349
+ # Full sObject list for the queue (for reporting)
350
+ QSO_ALL_JSON=$(sf data query --target-org "$ORG" \
351
+ --query "SELECT SobjectType FROM QueueSobject WHERE QueueId='$QUEUE_ID' ORDER BY SobjectType" \
352
+ --json 2>/dev/null)
353
+ BEFORE_QSOS=$(echo "$QSO_ALL_JSON" | jq -c '[.result.records[].SobjectType]')
354
+
355
+ # PLAN_ONLY: Steps 0-2 are read-only (queue selection is a query). Report whether a --run would
356
+ # PATCH the queue's routing-config binding, and stop before Step 3's write.
357
+ if [ "${PLAN_ONLY:-}" = "1" ]; then
358
+ if [ -z "$ROUTING_CONFIG_DN" ]; then
359
+ emit_plan "reused" "Queue '$QUEUE_DN' bound to $SOBJECT_TYPE; report-only (no routing_config to align)."
360
+ fi
361
+ RC_PLAN_JSON=$(sf data query --target-org "$ORG" \
362
+ --query "SELECT Id FROM QueueRoutingConfig WHERE DeveloperName='$ROUTING_CONFIG_DN'" --json 2>/dev/null || echo '{}')
363
+ RC_PLAN_COUNT=$(echo "$RC_PLAN_JSON" | jq -r '.result.totalSize // 0' 2>/dev/null || echo 0)
364
+ if [ "$RC_PLAN_COUNT" -eq 0 ]; then
365
+ emit_blocked \
366
+ "QueueRoutingConfig '$ROUTING_CONFIG_DN' does not exist on org. Run service-omni-queue-routing-config-deploy first." \
367
+ "" "$QUEUE_ID" "$QUEUE_DN"
368
+ fi
369
+ RC_PLAN_ID=$(echo "$RC_PLAN_JSON" | jq -r '.result.records[0].Id')
370
+ if [ "$BEFORE_RC_ID" = "$RC_PLAN_ID" ]; then
371
+ emit_plan "reused" "Queue '$QUEUE_DN' already aligned to QueueRoutingConfig '$ROUTING_CONFIG_DN'."
372
+ else
373
+ emit_plan "action_needed" "Would align queue '$QUEUE_DN' QueueRoutingConfigId → '$ROUTING_CONFIG_DN' (currently: $BEFORE_RC_ID)."
374
+ fi
375
+ fi
376
+
377
+ # Step 3 - Optional QRC alignment (only if routing_config_dn provided).
378
+ if [ "$QUEUE_CREATED" = "true" ]; then STATUS="created"; else STATUS="reused"; fi
379
+ TARGET_RC_ID=""
380
+ CHANGES=()
381
+
382
+ if [ -n "$ROUTING_CONFIG_DN" ]; then
383
+ RC_JSON=$(sf data query --target-org "$ORG" \
384
+ --query "SELECT Id FROM QueueRoutingConfig WHERE DeveloperName='$ROUTING_CONFIG_DN'" \
385
+ --json 2>/dev/null)
386
+
387
+ RC_COUNT=$(echo "$RC_JSON" | jq -r '.result.totalSize // 0' 2>/dev/null || echo "0")
388
+ if [ "$RC_COUNT" -eq 0 ]; then
389
+ emit_blocked \
390
+ "QueueRoutingConfig '$ROUTING_CONFIG_DN' does not exist on org. Run service-omni-queue-routing-config-deploy first." \
391
+ "" \
392
+ "$QUEUE_ID" \
393
+ "$QUEUE_DN"
394
+ fi
395
+ TARGET_RC_ID=$(echo "$RC_JSON" | jq -r '.result.records[0].Id')
396
+
397
+ if [ "$BEFORE_RC_ID" != "$TARGET_RC_ID" ]; then
398
+ # New queue stays "created"; a pre-existing queue whose binding we change is "updated".
399
+ [ "$QUEUE_CREATED" = "true" ] || STATUS="updated"
400
+ CHANGES+=("routing_config")
401
+
402
+ # Drift #16: `sf api request rest` emits `Warning:` to STDERR; redirect away for clean parsing.
403
+ PATCH_STDERR="$(mktemp)"
404
+ PATCH_RESULT=$(sf api request rest --target-org "$ORG" \
405
+ "/services/data/$API_VERSION/sobjects/Group/$QUEUE_ID" \
406
+ --method PATCH \
407
+ --body "{\"QueueRoutingConfigId\":\"$TARGET_RC_ID\"}" 2>"$PATCH_STDERR" || true)
408
+ rm -f "$PATCH_STDERR"
409
+
410
+ if echo "$PATCH_RESULT" | jq -e 'if type == "array" then .[0].errorCode else .errorCode end' >/dev/null 2>&1; then
411
+ ERR=$(echo "$PATCH_RESULT" | jq -r 'if type == "array" then .[0].message else .message end')
412
+ emit_blocked "PATCH Group.QueueRoutingConfigId failed: $ERR" "" "$QUEUE_ID" "$QUEUE_DN"
413
+ fi
414
+ fi
415
+ fi
416
+
417
+ # Step 4 - Re-query for post-check
418
+ AFTER_Q_JSON=$(sf data query --target-org "$ORG" \
419
+ --query "SELECT Id, QueueRoutingConfigId FROM Group WHERE Id='$QUEUE_ID'" --json)
420
+ AFTER_RC_ID=$(echo "$AFTER_Q_JSON" | jq -r '.result.records[0].QueueRoutingConfigId // "null"')
421
+
422
+ # Resolve AFTER_RC_ID → DeveloperName for reporting (may differ from ROUTING_CONFIG_DN if we didn't force)
423
+ AFTER_RC_DN="null"
424
+ if [ "$AFTER_RC_ID" != "null" ] && [ -n "$AFTER_RC_ID" ]; then
425
+ AFTER_RC_DN=$(sf data query --target-org "$ORG" \
426
+ --query "SELECT DeveloperName FROM QueueRoutingConfig WHERE Id='$AFTER_RC_ID'" --json 2>/dev/null | \
427
+ jq -r '.result.records[0].DeveloperName // "null"')
428
+ fi
429
+
430
+ # If we forced a QRC and post-check doesn't match, escalate
431
+ if [ -n "$ROUTING_CONFIG_DN" ] && [ "$AFTER_RC_ID" != "$TARGET_RC_ID" ]; then
432
+ emit_blocked "Post-check re-query shows QRC did not converge. After=$AFTER_RC_ID (target=$TARGET_RC_ID)." "" "$QUEUE_ID" "$QUEUE_DN"
433
+ fi
434
+
435
+ # Step 5 - Emit report
436
+ jq -n \
437
+ --arg status "$STATUS" \
438
+ --arg sobject "$SOBJECT_TYPE" \
439
+ --arg dn "$QUEUE_DN" \
440
+ --arg qid "$QUEUE_ID" \
441
+ --arg after_rc "$AFTER_RC_ID" \
442
+ --arg after_rc_dn "$AFTER_RC_DN" \
443
+ --argjson after_qsos "$BEFORE_QSOS" \
444
+ --arg before_rc "$BEFORE_RC_ID" \
445
+ --argjson before_qsos "$BEFORE_QSOS" \
446
+ --arg forced_rc "$ROUTING_CONFIG_DN" \
447
+ --arg queue_source "$QUEUE_SOURCE" \
448
+ --arg queue_created "$QUEUE_CREATED" \
449
+ --argjson candidate_dns "$CANDIDATE_DNS" \
450
+ '{
451
+ skill: "service-omni-queue-deploy",
452
+ status: $status,
453
+ sobject_type: $sobject,
454
+ developer_name: $dn,
455
+ queue_created: ($queue_created == "true"),
456
+ queue_selection_source: $queue_source,
457
+ candidate_queue_developer_names: $candidate_dns,
458
+ id: $qid,
459
+ queue_routing_config_id: (if $after_rc == "null" or $after_rc == "" then null else $after_rc end),
460
+ queue_routing_config_developer_name: (if $after_rc_dn == "null" or $after_rc_dn == "" then null else $after_rc_dn end),
461
+ queue_routing_config_source: (if $forced_rc == "" then "discovered" else "forced" end),
462
+ queue_sobjects: $after_qsos,
463
+ before: {
464
+ existed: true,
465
+ queue_routing_config_id: (if $before_rc == "null" then null else $before_rc end),
466
+ queue_sobjects: $before_qsos
467
+ },
468
+ manual_actions: [],
469
+ blocking_issue: null
470
+ }'
@@ -0,0 +1,93 @@
1
+ ---
2
+ name: service-omni-queue-members-assign
3
+ description: "Use to bind agent users into a Salesforce Queue via GroupMember Data API POSTs, with SOQL-based idempotency (safe re-run — only inserts missing bindings). Binds either the generated demo agents (agent{1..N}.<suffix>@example.com) or an explicit list of real agents passed as usernames/User Ids. Triggers: add agents to a Case/Voice queue, assign agent users to a queue, populate queue membership for Omni routing. Do not use to create the queue (service-omni-queue-deploy), to create the agent users (service-omni-agent-users-create), or to assign permission sets (service-omni-permission-set-assign)."
4
+ allowed-tools: Bash Read Write Edit Glob Grep
5
+ metadata:
6
+ version: "1.0"
7
+ domains: ["Service"]
8
+ minApiVersion: "66.0"
9
+ relatedSkills:
10
+ - "service-omni-agent-users-create"
11
+ - "service-omni-permission-set-assign"
12
+ - "service-omni-queue-deploy"
13
+ - "service-omni-queue-routing-config-deploy"
14
+ accessCheck:
15
+ - type: license
16
+ value: ServiceCloud
17
+ cliTools:
18
+ - tool: ["jq"]
19
+ semver: ">=1.6"
20
+ - tool: ["python3"]
21
+ semver: ">=3.8"
22
+ - tool: ["sf"]
23
+ semver: ">=2.139.6"
24
+ ---
25
+
26
+ # service-omni-queue-members-assign
27
+
28
+ Bind N agent users into a Salesforce Queue via `GroupMember` Data API POSTs. Without queue membership, Omni-Channel routing sees an empty pool for the queue and never assigns work. Detection is SOQL-based (the same detect-then-POST shape as `service-omni-queue-routing-config-deploy`), so only missing memberships are created and the skill is safe to re-run. Agent users come from `service-omni-agent-users-create`, the queue from `service-omni-queue-deploy`, and the users' Omni permissions from `service-omni-permission-set-assign`.
29
+
30
+ ## Inputs
31
+
32
+ ```bash
33
+ bash scripts/verify-and-bind.sh <org-alias> [queue-developer-name=CaseQueue] [count=3] [explicit-members-csv]
34
+ # demo agents (default):
35
+ bash scripts/verify-and-bind.sh myorg CaseQueue 3
36
+ # real agents by username/id:
37
+ bash scripts/verify-and-bind.sh myorg VoiceQueue "" "jdoe@acme.com,005XX000001abcd"
38
+ ```
39
+
40
+ - `org-alias` (required).
41
+ - `queue-developer-name` (optional, default `CaseQueue`).
42
+ - `count` (optional, default `3`, range `1..10`) — must match the agent user count; ignored in explicit mode.
43
+ - `explicit-members-csv` (optional 4th positional, or `MEMBER_USERNAMES_CSV` / `MEMBER_USER_IDS_CSV`) — each token is a Username or a 15/18-char User Id; the count is derived from the list.
44
+
45
+ ## Preconditions and safety
46
+
47
+ - Target org authenticated via `sf` CLI (My Domain URL), Service Cloud license, `sf` CLI ≥ 2.139.6.
48
+ - The executing user has `PermissionsModifyAllData` (standard on System Administrator).
49
+ - The agent users exist (`service-omni-agent-users-create`) and the queue exists (`service-omni-queue-deploy`); a missing user set or queue blocks with a pointer to the right skill (queues cannot be created via the Data API).
50
+ - The three-way `safe_to_write` production guard applies — adding users to a production queue mis-routes real customer work, so it blocks with no override.
51
+
52
+ ## Run
53
+
54
+ `verify-and-bind.sh` runs the whole cycle:
55
+
56
+ 1. Compute `safe_to_write`; derive the 8-char org suffix.
57
+ 2. Resolve the queue `Group.Id` by DeveloperName + `Type='Queue'`; block if missing.
58
+ 3. Resolve the member set — generated mode queries `User` for `agent{1..N}.<suffix>@example.com` (block if any expected user is missing); explicit mode resolves each supplied token and requires every one to be an ACTIVE user.
59
+ 4. Query existing `GroupMember` for the queue; compute the users not yet bound.
60
+ 5. POST one `GroupMember` per missing user (individual POSTs, no `allOrNone`).
61
+ 6. Re-query to confirm final membership and emit the report.
62
+
63
+ ## Behavior
64
+
65
+ **Two member sources, never mixed.** The default generated pattern keeps the user-create → member-assign handoff deterministic; explicit mode is opt-in for real agents and takes over count derivation. In explicit mode, a token that does not resolve to an active user blocks the run — binding a typo'd or inactive user would silently under-populate the queue.
66
+
67
+ **Idempotency and honesty.** POSTs are individual so one failed binding (e.g. a race with another admin) never rolls back its successful siblings, and the skill re-queries after all POSTs — a 201 only means the write was accepted; a subsequent SOQL confirms the row is visible to the routing engine. The `before` snapshot includes members already present, even non-demo users added out-of-band, so the coordinator can see full membership state.
68
+
69
+ **Non-destructive.** Create-only; it never removes or reassigns existing members.
70
+
71
+ ## Output contract
72
+
73
+ A single JSON object with `status` ∈ `bound` | `reused` | `partial` | `blocked`, the resolved `queue`, `org_suffix`, `member_source` (`generated_pattern` | `explicit`), `requested_count`, a `before` snapshot, `bound_this_run`/`bound_count`, `reused_count`, an `after` snapshot, `manual_actions`, and `blocking_issue`.
74
+
75
+ - `bound` — at least one new member created; final state matches the expected count.
76
+ - `reused` — all expected users were already members; nothing POSTed.
77
+ - `partial` — some POSTs failed; the re-query shows fewer members than requested (details in `blocking_issue`).
78
+ - `blocked` — precondition failed (production org, missing queue, missing users, missing permissions).
79
+
80
+ `bound_count + reused_count == requested_count` unless `partial`; `blocking_issue` is non-null only for `blocked`/`partial`.
81
+
82
+ ## Limitations
83
+
84
+ - Generated and explicit modes are mutually exclusive per run.
85
+ - Create-only; removing or reassigning members is out of scope.
86
+ - Does not create the queue or the users, and does not assign permission sets or presence configs.
87
+
88
+ ## References
89
+
90
+ | File | When to read |
91
+ |---|---|
92
+ | `references/api-notes.md` | Before the detect/bind cycle — GroupMember schema, the queue-vs-public-group distinction, why `UserOrGroupId` is polymorphic, and common POST failures |
93
+ | `scripts/tests/test_queue_members_contracts.py` | When validating changes — run `python3 scripts/tests/test_queue_members_contracts.py` from this skill directory |