@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.
- package/package.json +1 -1
- package/skills/education-cloud-domain-configure/SKILL.md +234 -0
- package/skills/education-cloud-domain-configure/references/dependent-features.md +198 -0
- package/skills/education-cloud-domain-configure/references/domain-features/academic-operations.md +386 -0
- package/skills/education-cloud-domain-configure/references/domain-features/alumni-relations.md +155 -0
- package/skills/education-cloud-domain-configure/references/domain-features/cross-domain-dependencies.md +18 -0
- package/skills/education-cloud-domain-configure/references/domain-features/industriessettings-toggle-reference.md +43 -0
- package/skills/education-cloud-domain-configure/references/domain-features/mentoring.md +198 -0
- package/skills/education-cloud-domain-configure/references/domain-features/recruitment-admissions.md +315 -0
- package/skills/education-cloud-domain-configure/references/domain-features/student-management.md +155 -0
- package/skills/education-cloud-domain-configure/references/domain-features/student-success.md +249 -0
- package/skills/education-cloud-domain-configure/references/domain-features.md +27 -0
- package/skills/education-cloud-domain-configure/references/gotchas-extended.md +23 -0
- package/skills/education-cloud-domain-configure/references/toggle-write-path.md +67 -0
- package/skills/education-cloud-domain-configure/scripts/check_license_provisioned.py +62 -0
- package/skills/education-cloud-domain-configure/scripts/check_org_edition.py +44 -0
- package/skills/education-cloud-domain-configure/scripts/check_permission_assigned.py +39 -0
- package/skills/education-cloud-multi-campus-configure/SKILL.md +2 -0
- package/skills/education-cloud-multi-campus-configure/references/foundation_prerequisites.md +4 -3
- package/skills/service-agentforce-channel-configure/SKILL.md +33 -28
- package/skills/service-agentforce-human-escalation-configure/SKILL.md +205 -0
- package/skills/service-agentforce-human-escalation-configure/assets/escalation-thresholds.instructions.md +37 -0
- package/skills/service-agentforce-human-escalation-configure/assets/force-app/main/default/flows/Human_Escalation_Outbound_Flow.flow-meta.xml +173 -0
- package/skills/service-agentforce-human-escalation-configure/assets/package.xml +8 -0
- package/skills/service-agentforce-human-escalation-configure/assets/sfdx-project.json +5 -0
- package/skills/service-agentforce-human-escalation-configure/references/classifier-contracts.md +103 -0
- package/skills/service-agentforce-human-escalation-configure/references/escalation-mechanism.md +63 -0
- package/skills/service-agentforce-human-escalation-configure/references/failure-policy.md +44 -0
- package/skills/service-agentforce-human-escalation-configure/references/nga-escalation.md +57 -0
- package/skills/service-agentforce-human-escalation-configure/references/runtime-verification.md +72 -0
- package/skills/service-agentforce-human-escalation-configure/scripts/classify-agent-active.mjs +49 -0
- package/skills/service-agentforce-human-escalation-configure/scripts/classify-nga-escalation.mjs +166 -0
- package/skills/service-agentforce-human-escalation-configure/scripts/extract-outbound-route.mjs +67 -0
- package/skills/service-agentforce-human-escalation-configure/scripts/patch-escalation-surfaces.mjs +131 -0
- package/skills/service-agentforce-human-escalation-configure/scripts/tests/__init__.py +0 -0
- package/skills/service-agentforce-human-escalation-configure/scripts/tests/_bootstrap.py +249 -0
- package/skills/service-agentforce-human-escalation-configure/scripts/tests/test_escalation_contracts.py +899 -0
- package/skills/service-agentforce-human-escalation-configure/scripts/verify-and-configure.sh +705 -0
- package/skills/service-agentforce-human-escalation-configure/scripts/verify-escalation-config.mjs +89 -0
- package/skills/service-de-channel-routing-configure/SKILL.md +13 -5
- package/skills/service-de-channel-routing-configure/scripts/tests/test_skill_contract.py +25 -0
- package/skills/service-itsm-agentic-setup-agentforce-coordinate/SKILL.md +23 -10
- package/skills/service-itsm-agentic-setup-agentforce-coordinate/examples/output-templates.md +63 -48
- package/skills/service-itsm-agentic-setup-agentforce-coordinate/scripts/verify-child-verdict.mjs +6 -2
- package/skills/service-omni-agent-users-create/SKILL.md +96 -0
- package/skills/service-omni-agent-users-create/assets/create-users.apex.template +80 -0
- package/skills/service-omni-agent-users-create/references/apex-patterns.md +135 -0
- package/skills/service-omni-agent-users-create/references/apex-template-notes.md +40 -0
- package/skills/service-omni-agent-users-create/scripts/detect-and-create.sh +206 -0
- package/skills/service-omni-agent-users-create/scripts/detect-existing.sh +122 -0
- package/skills/service-omni-agent-users-create/scripts/run-create.sh +298 -0
- package/skills/service-omni-agent-users-create/scripts/tests/test_user_create_security.py +171 -0
- package/skills/service-omni-agent-work-sharing-configure/SKILL.md +78 -0
- package/skills/service-omni-agent-work-sharing-configure/references/api-notes.md +31 -0
- package/skills/service-omni-agent-work-sharing-configure/scripts/configure-and-report.sh +186 -0
- package/skills/service-omni-agent-work-sharing-configure/scripts/tests/_bootstrap.py +120 -0
- package/skills/service-omni-agent-work-sharing-configure/scripts/tests/test_agent_work_sharing_contracts.py +93 -0
- package/skills/service-omni-base-settings-configure/SKILL.md +77 -0
- package/skills/service-omni-base-settings-configure/assets/force-app/main/default/settings/OmniChannel.settings-meta.xml +8 -0
- package/skills/service-omni-base-settings-configure/assets/package.xml +8 -0
- package/skills/service-omni-base-settings-configure/assets/sfdx-project.json +12 -0
- package/skills/service-omni-base-settings-configure/references/api-notes.md +121 -0
- package/skills/service-omni-base-settings-configure/scripts/configure-and-report.sh +164 -0
- package/skills/service-omni-base-settings-configure/scripts/detect-existing.sh +69 -0
- package/skills/service-omni-channel-setup-coordinate/SKILL.md +168 -0
- package/skills/service-omni-channel-setup-coordinate/examples/output-templates.md +161 -0
- package/skills/service-omni-channel-setup-coordinate/references/gap-catalog.md +99 -0
- package/skills/service-omni-channel-setup-coordinate/scripts/integration-driver.sh +1542 -0
- package/skills/service-omni-channel-setup-coordinate/scripts/tests/__init__.py +0 -0
- package/skills/service-omni-channel-setup-coordinate/scripts/tests/_bootstrap.py +223 -0
- package/skills/service-omni-channel-setup-coordinate/scripts/tests/test_omni_contracts.py +804 -0
- package/skills/service-omni-command-center-analyze/SKILL.md +84 -0
- package/skills/service-omni-command-center-analyze/references/api-notes.md +54 -0
- package/skills/service-omni-command-center-analyze/scripts/analyze.sh +198 -0
- package/skills/service-omni-command-center-analyze/scripts/tests/_bootstrap.py +115 -0
- package/skills/service-omni-command-center-analyze/scripts/tests/test_command_center_analyze_contracts.py +165 -0
- package/skills/service-omni-permission-set-assign/SKILL.md +88 -0
- package/skills/service-omni-permission-set-assign/assets/package.xml +8 -0
- package/skills/service-omni-permission-set-assign/references/api-notes.md +143 -0
- package/skills/service-omni-permission-set-assign/scripts/verify-and-assign.sh +397 -0
- package/skills/service-omni-presence-status-deploy/SKILL.md +87 -0
- package/skills/service-omni-presence-status-deploy/assets/package.xml +12 -0
- package/skills/service-omni-presence-status-deploy/references/api-notes.md +115 -0
- package/skills/service-omni-presence-status-deploy/scripts/deploy-and-report.sh +422 -0
- package/skills/service-omni-presence-user-config-deploy/SKILL.md +80 -0
- package/skills/service-omni-presence-user-config-deploy/references/api-notes.md +60 -0
- package/skills/service-omni-presence-user-config-deploy/scripts/deploy-and-report.sh +261 -0
- package/skills/service-omni-presence-user-config-deploy/scripts/tests/_bootstrap.py +106 -0
- package/skills/service-omni-presence-user-config-deploy/scripts/tests/test_presence_user_config_contracts.py +142 -0
- package/skills/service-omni-queue-deploy/SKILL.md +91 -0
- package/skills/service-omni-queue-deploy/references/api-notes.md +140 -0
- package/skills/service-omni-queue-deploy/scripts/verify-and-align.sh +470 -0
- package/skills/service-omni-queue-members-assign/SKILL.md +93 -0
- package/skills/service-omni-queue-members-assign/references/api-notes.md +144 -0
- package/skills/service-omni-queue-members-assign/scripts/tests/_bootstrap.py +67 -0
- package/skills/service-omni-queue-members-assign/scripts/tests/test_queue_members_contracts.py +73 -0
- package/skills/service-omni-queue-members-assign/scripts/verify-and-bind.sh +362 -0
- package/skills/service-omni-queue-routing-config-deploy/SKILL.md +101 -0
- package/skills/service-omni-queue-routing-config-deploy/references/api-notes.md +127 -0
- package/skills/service-omni-queue-routing-config-deploy/scripts/tests/_bootstrap.py +90 -0
- package/skills/service-omni-queue-routing-config-deploy/scripts/tests/test_queue_routing_config_contracts.py +70 -0
- package/skills/service-omni-queue-routing-config-deploy/scripts/upsert-and-report.sh +538 -0
- package/skills/service-omni-routing-flow-deploy/SKILL.md +111 -0
- package/skills/service-omni-routing-flow-deploy/assets/force-app/main/default/flows/Omni_Route_Case_Trigger.SkillsBased.flow-meta.xml +94 -0
- package/skills/service-omni-routing-flow-deploy/assets/force-app/main/default/flows/Omni_Route_Case_Trigger.flow-meta.xml +88 -0
- package/skills/service-omni-routing-flow-deploy/assets/force-app/main/default/flows/Omni_Route_Cases.flow-meta.xml +144 -0
- package/skills/service-omni-routing-flow-deploy/assets/force-app/main/default/flows/Omni_Route_VoiceCall_Trigger.SkillsBased.flow-meta.xml +94 -0
- package/skills/service-omni-routing-flow-deploy/assets/force-app/main/default/flows/Omni_Route_VoiceCall_Trigger.flow-meta.xml +88 -0
- package/skills/service-omni-routing-flow-deploy/assets/force-app/main/default/flows/Omni_Route_VoiceCalls.flow-meta.xml +144 -0
- package/skills/service-omni-routing-flow-deploy/assets/package.xml +11 -0
- package/skills/service-omni-routing-flow-deploy/assets/sfdx-project.json +11 -0
- package/skills/service-omni-routing-flow-deploy/references/api-notes.md +265 -0
- package/skills/service-omni-routing-flow-deploy/scripts/deploy-and-report.sh +684 -0
- package/skills/service-omni-service-channel-configure/SKILL.md +83 -0
- package/skills/service-omni-service-channel-configure/assets/force-app/main/default/serviceChannels/Cases.serviceChannel-meta.xml +9 -0
- package/skills/service-omni-service-channel-configure/assets/force-app/main/default/serviceChannels/Incidents.serviceChannel-meta.xml +9 -0
- package/skills/service-omni-service-channel-configure/assets/force-app/main/default/serviceChannels/MessagingSessions.serviceChannel-meta.xml +9 -0
- package/skills/service-omni-service-channel-configure/assets/force-app/main/default/serviceChannels/VoiceCalls.serviceChannel-meta.xml +9 -0
- package/skills/service-omni-service-channel-configure/assets/package.xml +11 -0
- package/skills/service-omni-service-channel-configure/assets/sfdx-project.json +12 -0
- package/skills/service-omni-service-channel-configure/references/api-notes.md +84 -0
- package/skills/service-omni-service-channel-configure/scripts/deploy-and-report.sh +383 -0
- package/skills/service-omni-sidebar-configure/SKILL.md +77 -0
- package/skills/service-omni-sidebar-configure/references/api-notes.md +51 -0
- package/skills/service-omni-sidebar-configure/scripts/enable-and-report.sh +315 -0
- package/skills/service-omni-sidebar-configure/scripts/tests/_bootstrap.py +142 -0
- package/skills/service-omni-sidebar-configure/scripts/tests/test_sidebar_contracts.py +139 -0
- package/skills/service-omni-skills-based-routing-configure/SKILL.md +81 -0
- package/skills/service-omni-skills-based-routing-configure/references/api-notes.md +50 -0
- package/skills/service-omni-skills-based-routing-configure/scripts/configure-and-report.sh +270 -0
- package/skills/service-omni-skills-based-routing-configure/scripts/tests/_bootstrap.py +120 -0
- package/skills/service-omni-skills-based-routing-configure/scripts/tests/test_sbr_contracts.py +129 -0
- package/skills/service-omni-supervisor-config-deploy/SKILL.md +97 -0
- package/skills/service-omni-supervisor-config-deploy/assets/force-app/main/default/omniSupervisorConfigs/Omni_Supervisor.omniSupervisorConfig-meta.xml +9 -0
- package/skills/service-omni-supervisor-config-deploy/assets/package.xml +8 -0
- package/skills/service-omni-supervisor-config-deploy/assets/sfdx-project.json +12 -0
- package/skills/service-omni-supervisor-config-deploy/references/api-notes.md +205 -0
- package/skills/service-omni-supervisor-config-deploy/scripts/deploy-and-report.sh +586 -0
- package/skills/service-omni-supervisor-config-deploy/scripts/tests/_bootstrap.py +153 -0
- package/skills/service-omni-supervisor-config-deploy/scripts/tests/test_supervisor_config_contracts.py +116 -0
- package/skills/service-omni-supervisor-permset-assign/SKILL.md +88 -0
- package/skills/service-omni-supervisor-permset-assign/references/api-notes.md +48 -0
- package/skills/service-omni-supervisor-permset-assign/scripts/verify-and-assign.sh +316 -0
- package/skills/service-omni-supervisor-surface-deploy/SKILL.md +85 -0
- package/skills/service-omni-supervisor-surface-deploy/references/api-notes.md +47 -0
- package/skills/service-omni-supervisor-surface-deploy/scripts/deploy-and-report.sh +216 -0
- package/skills/service-omni-supervisor-surface-deploy/scripts/tests/_bootstrap.py +95 -0
- package/skills/service-omni-supervisor-surface-deploy/scripts/tests/test_supervisor_surface_contracts.py +154 -0
- package/skills/service-omni-supervisor-users-create/SKILL.md +92 -0
- package/skills/service-omni-supervisor-users-create/assets/create-supervisors.apex.template +72 -0
- package/skills/service-omni-supervisor-users-create/references/apex-patterns.md +73 -0
- package/skills/service-omni-supervisor-users-create/references/apex-template-notes.md +41 -0
- package/skills/service-omni-supervisor-users-create/scripts/detect-and-create.sh +197 -0
- package/skills/service-omni-supervisor-users-create/scripts/detect-existing.sh +112 -0
- package/skills/service-omni-supervisor-users-create/scripts/run-create.sh +296 -0
- package/skills/service-omni-work-skill-routing-configure/SKILL.md +80 -0
- package/skills/service-omni-work-skill-routing-configure/references/api-notes.md +67 -0
- package/skills/service-omni-work-skill-routing-configure/scripts/configure-and-report.sh +262 -0
- package/skills/service-omni-work-skill-routing-configure/scripts/tests/_bootstrap.py +121 -0
- 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//&/&}"; s="${s//</<}"; s="${s//>/>}"; s="${s//\"/"}"; s="${s//\'/'}"
|
|
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 |
|