@salesforce/afv-skills 1.47.0 → 1.48.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 +7 -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,684 @@
1
+ #!/usr/bin/env bash
2
+ # deploy-and-report.sh - deploy + verify an Omni routing Flow: autolaunched dryRun smoke flow (default, verified via Actions REST) or record-triggered production flow (--trigger, token-resolved, optional --runtime-proof). Args, token resolution, exit codes: SKILL.md.
3
+
4
+ set -euo pipefail
5
+
6
+ # Escape the five XML predefined entities so a ServiceChannel MasterLabel with & < > " ' cannot
7
+ # corrupt the flow XML when substituted into the asset below.
8
+ xml_escape() {
9
+ local s="$1"
10
+ s="${s//&/&amp;}"; s="${s//</&lt;}"; s="${s//>/&gt;}"; s="${s//\"/&quot;}"; s="${s//\'/&apos;}"
11
+ printf '%s' "$s"
12
+ }
13
+ # Escape sed replacement metacharacters (\ & and the | delimiter) so an escaped value is inserted
14
+ # literally by the sed substitution.
15
+ sed_repl_escape() { printf '%s' "$1" | sed -e 's/[\\&|]/\\&/g'; }
16
+
17
+ if [ $# -lt 1 ]; then
18
+ echo '{"error":"Missing required argument: org-alias","usage":"bash deploy-and-report.sh <org-alias> [flow_developer_name] [--skip-invoke] [--target Case|VoiceCall] [--trigger] [--runtime-proof]"}' >&2
19
+ exit 1
20
+ fi
21
+
22
+ # --- Parse flags out of the arg list, leaving positional args intact ---
23
+ SKIP_INVOKE="false"
24
+ TRIGGER_MODE="false"
25
+ RUNTIME_PROOF="false"
26
+ # --require-proof (or OMNI_RUNTIME_PROOF_REQUIRED=1): make runtime routing proof mandatory (a missing
27
+ # PendingServiceRouting|AgentWork blocks instead of fail-soft); the Voice release gate. Implies --runtime-proof.
28
+ REQUIRE_PROOF="${OMNI_RUNTIME_PROOF_REQUIRED:-0}"
29
+ [ "$REQUIRE_PROOF" = "1" ] && { REQUIRE_PROOF="true"; RUNTIME_PROOF="true"; TRIGGER_MODE="true"; } || REQUIRE_PROOF="false"
30
+ TARGET="Case"
31
+ # Routing type for the record-triggered production flow: QueueBased (default, backward-compatible) or
32
+ # SkillsBased. SkillsBased selects the skills-based flow variant (${FLOW_DN}.SkillsBased.flow-meta.xml)
33
+ # and REQUIRES a non-null skillOption - a null skillOption makes the platform routeWork action NPE and
34
+ # roll back the triggering insert (W-24069761).
35
+ ROUTING_TYPE="${OMNI_ROUTING_TYPE:-QueueBased}"
36
+ # skillOption for SkillsBased routing (ignored for QueueBased). RunSBRRules (default) makes the platform
37
+ # evaluate the org's WorkSkillRouting rules server-side and attach matching SkillRequirement rows;
38
+ # DefineSkillRequirements uses only skills the flow passes; Both does both. Closed set - mirrors
39
+ # OmniFlowConstants.SkillOption in core.
40
+ SKILL_OPTION="${OMNI_SKILL_OPTION:-RunSBRRules}"
41
+ SKILL_OPTION_EXPLICIT="false"
42
+ POSITIONAL=()
43
+ while [ $# -gt 0 ]; do
44
+ case "$1" in
45
+ --skip-invoke) SKIP_INVOKE="true" ;;
46
+ --trigger) TRIGGER_MODE="true" ;;
47
+ --runtime-proof) RUNTIME_PROOF="true"; TRIGGER_MODE="true" ;;
48
+ --require-proof) REQUIRE_PROOF="true"; RUNTIME_PROOF="true"; TRIGGER_MODE="true" ;;
49
+ --target) shift; TARGET="${1:-Case}" ;;
50
+ --target=*) TARGET="${1#*=}" ;;
51
+ --routing-type) shift; ROUTING_TYPE="${1:-}" ;;
52
+ --routing-type=*) ROUTING_TYPE="${1#*=}" ;;
53
+ --skill-option) shift; SKILL_OPTION="${1:-}"; SKILL_OPTION_EXPLICIT="true" ;;
54
+ --skill-option=*) SKILL_OPTION="${1#*=}"; SKILL_OPTION_EXPLICIT="true" ;;
55
+ *) POSITIONAL+=("$1") ;;
56
+ esac
57
+ shift
58
+ done
59
+ set -- "${POSITIONAL[@]}"
60
+
61
+ ORG="$1"
62
+
63
+ case "$TARGET" in
64
+ Case) DEFAULT_SMOKE_FLOW="Omni_Route_Cases"; DEFAULT_TRIGGER_FLOW="Omni_Route_Case_Trigger"; DEFAULT_SC_DEVNAME="Cases" ;;
65
+ VoiceCall) DEFAULT_SMOKE_FLOW="Omni_Route_VoiceCalls"; DEFAULT_TRIGGER_FLOW="Omni_Route_VoiceCall_Trigger"; DEFAULT_SC_DEVNAME="sfdc_phone" ;;
66
+ *) echo "{\"error\":\"Invalid --target '$TARGET'. Supported: Case, VoiceCall\"}" >&2; exit 2 ;;
67
+ esac
68
+
69
+ case "$ROUTING_TYPE" in
70
+ QueueBased|SkillsBased) ;;
71
+ *) echo "{\"error\":\"Invalid --routing-type '$ROUTING_TYPE'. Supported: QueueBased, SkillsBased\"}" >&2; exit 2 ;;
72
+ esac
73
+ # 761 guard: a SkillsBased routeWork with a null/invalid skillOption NPEs in the platform action and
74
+ # rolls back the triggering insert. Refuse to emit one - the skill never ships a null skillOption.
75
+ if [ "$ROUTING_TYPE" = "SkillsBased" ]; then
76
+ case "$SKILL_OPTION" in
77
+ RunSBRRules|DefineSkillRequirements|Both) ;;
78
+ "") echo "{\"error\":\"routing-type SkillsBased requires a non-empty --skill-option (RunSBRRules|DefineSkillRequirements|Both). Refusing to deploy a SkillsBased routeWork flow with a null skillOption: it would NPE in the platform action and roll back the triggering insert (W-24069761).\"}" >&2; exit 2 ;;
79
+ *) echo "{\"error\":\"Invalid --skill-option '$SKILL_OPTION'. Supported: RunSBRRules, DefineSkillRequirements, Both\"}" >&2; exit 2 ;;
80
+ esac
81
+ fi
82
+
83
+ if [ "$ROUTING_TYPE" = "QueueBased" ] && [ "$SKILL_OPTION_EXPLICIT" = "true" ]; then
84
+ echo '{"error":"--skill-option is valid only with --routing-type SkillsBased."}' >&2
85
+ exit 2
86
+ fi
87
+
88
+ # Flow to operate on: explicit positional wins; else target+mode default.
89
+ if [ "$TRIGGER_MODE" = "true" ]; then
90
+ FLOW_DN="${2:-$DEFAULT_TRIGGER_FLOW}"
91
+ else
92
+ FLOW_DN="${2:-$DEFAULT_SMOKE_FLOW}"
93
+ fi
94
+
95
+ if ! [[ "$FLOW_DN" =~ ^[A-Za-z][A-Za-z0-9_]{0,79}$ ]]; then
96
+ echo "{\"error\":\"Invalid flow_developer_name '$FLOW_DN'. Must start with a letter, only A-Z a-z 0-9 _ (max 80).\"}" >&2
97
+ exit 1
98
+ fi
99
+
100
+ if ! [[ "$ORG" =~ ^[A-Za-z0-9._@-]{1,128}$ ]]; then
101
+ echo "{\"error\":\"Invalid org alias '$ORG'.\"}" >&2
102
+ exit 1
103
+ fi
104
+
105
+ if ! sf org display --target-org "$ORG" --json >/dev/null 2>&1; then
106
+ echo "{\"error\":\"Org alias '$ORG' is not authenticated.\"}" >&2
107
+ exit 1
108
+ fi
109
+
110
+ # Write guard: deploys Flow metadata, so self-enforce the production-org guard (directly invokable).
111
+ ORG_GUARD_JSON=$(sf data query --target-org "$ORG" \
112
+ --query "SELECT IsSandbox, TrialExpirationDate, OrganizationType FROM Organization LIMIT 1" \
113
+ --json 2>/dev/null) || {
114
+ echo "{\"error\":\"Failed to query Organization for safe_to_write guard: $(printf '%s' "$ORG_GUARD_JSON" | head -c 300)\"}" >&2
115
+ exit 1
116
+ }
117
+ IS_SANDBOX=$(echo "$ORG_GUARD_JSON" | jq -r '.result.records[0].IsSandbox')
118
+ TRIAL_EXP=$(echo "$ORG_GUARD_JSON" | jq -r '.result.records[0].TrialExpirationDate // "null"')
119
+ ORG_TYPE=$(echo "$ORG_GUARD_JSON" | jq -r '.result.records[0].OrganizationType')
120
+
121
+ SAFE_TO_WRITE=false
122
+ if [ "$IS_SANDBOX" = "true" ] \
123
+ || [ "$TRIAL_EXP" != "null" ] \
124
+ || [ "$ORG_TYPE" = "Developer Edition" ] \
125
+ || [ "$ORG_TYPE" = "Base Edition" ]; then
126
+ SAFE_TO_WRITE=true
127
+ fi
128
+
129
+ if [ "$SAFE_TO_WRITE" != "true" ]; then
130
+ echo "{\"skill\":\"service-omni-routing-flow-deploy\",\"status\":\"blocked\",\"blocking_issue\":\"Refusing to deploy Flow metadata to 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
131
+ exit 1
132
+ fi
133
+
134
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
135
+ ASSETS_DIR="$(cd "$SCRIPT_DIR/../assets" && pwd)"
136
+ FLOW_XML="$ASSETS_DIR/force-app/main/default/flows/${FLOW_DN}.flow-meta.xml"
137
+ # Template the deploy reads from. The SkillsBased trigger variant is a sibling asset carrying
138
+ # routingType=SkillsBased + a non-null skillOption; QueueBased and the smoke/dryRun flows use the base
139
+ # asset. Either way the temp copy is written as ${FLOW_DN}.flow-meta.xml, so the deployed DeveloperName
140
+ # stays $FLOW_DN (the SkillsBased flow REPLACES the QueueBased one - never two triggers on one object).
141
+ TEMPLATE_XML="$FLOW_XML"
142
+ if [ "$TRIGGER_MODE" = "true" ] && [ "$ROUTING_TYPE" = "SkillsBased" ]; then
143
+ TEMPLATE_XML="$ASSETS_DIR/force-app/main/default/flows/${FLOW_DN}.SkillsBased.flow-meta.xml"
144
+ fi
145
+
146
+ if [ ! -f "$ASSETS_DIR/sfdx-project.json" ]; then
147
+ echo "{\"error\":\"Assets DX project not found at $ASSETS_DIR\"}" >&2
148
+ exit 1
149
+ fi
150
+
151
+ if [ ! -f "$TEMPLATE_XML" ]; then
152
+ echo "{\"error\":\"Flow asset not found at $TEMPLATE_XML. Add the .flow-meta.xml file, or pick a different flow_developer_name / --routing-type.\"}" >&2
153
+ exit 1
154
+ fi
155
+
156
+ # Resolve the org's API version so we hit the right Actions REST endpoint. The Flow metadata
157
+ # is written at 66.0 but the org may serve a newer API - the endpoint takes any recent version.
158
+ API_VERSION=$(sf org display --target-org "$ORG" --json 2>/dev/null \
159
+ | jq -r '.result.apiVersion // "66.0"')
160
+ if ! [[ "$API_VERSION" =~ ^[0-9]+\.[0-9]+$ ]]; then
161
+ API_VERSION="66.0"
162
+ fi
163
+
164
+ # TRIGGER MODE - deploy the record-triggered production flow (token-resolved), then optionally prove
165
+ # routing at runtime. Self-contained: emits its own report and exits.
166
+ if [ "$TRIGGER_MODE" = "true" ]; then
167
+ # Cleanup on EVERY exit path: remove the temp DX project + delete any throwaway proof record.
168
+ TWORK=""
169
+ CLEANUP_RECORD_ID=""
170
+ _rf_cleanup() {
171
+ [ -n "$TWORK" ] && rm -rf "$TWORK" 2>/dev/null || true
172
+ if [ -n "$CLEANUP_RECORD_ID" ]; then
173
+ sf data delete record --target-org "$ORG" --sobject "$TARGET" --record-id "$CLEANUP_RECORD_ID" >/dev/null 2>&1 || true
174
+ fi
175
+ }
176
+ trap _rf_cleanup EXIT
177
+
178
+ trigger_blocked() {
179
+ jq -n --arg flow "$FLOW_DN" --arg target "$TARGET" --arg msg "$1" \
180
+ '{skill:"service-omni-routing-flow-deploy", status:"blocked", mode:"record_triggered",
181
+ target:$target, flow_developer_name:$flow, flow_active:false,
182
+ runtime_proof:{attempted:false,success:false,reason:"blocked_before_proof"},
183
+ manual_actions:[], blocking_issue:$msg}'
184
+ exit 1
185
+ }
186
+
187
+ # --- Resolve the Queue bound to the target sObject ---
188
+ QDN="${QUEUE_DEVELOPER_NAME:-}"
189
+ if [ -n "$QDN" ] && ! [[ "$QDN" =~ ^[A-Za-z][A-Za-z0-9_]{0,79}$ ]]; then
190
+ trigger_blocked "Invalid QUEUE_DEVELOPER_NAME '$QDN'."
191
+ fi
192
+ if [ -z "$QDN" ]; then
193
+ QSO=$(sf data query --target-org "$ORG" \
194
+ --query "SELECT QueueId, Queue.DeveloperName FROM QueueSobject WHERE SobjectType='$TARGET' ORDER BY Queue.DeveloperName" --json 2>/dev/null)
195
+ QSO_DNS=$(echo "$QSO" | jq -c '[.result.records[].Queue.DeveloperName] | unique' 2>/dev/null || echo '[]')
196
+ QSO_N=$(echo "$QSO_DNS" | jq 'length' 2>/dev/null || echo 0)
197
+ if [ "$QSO_N" = "0" ]; then
198
+ trigger_blocked "No queue is bound to '$TARGET'. Run service-omni-queue-deploy --create-if-missing first, or set QUEUE_DEVELOPER_NAME."
199
+ elif [ "$QSO_N" != "1" ]; then
200
+ trigger_blocked "Multiple queues bound to '$TARGET' ($QSO_DNS). Set QUEUE_DEVELOPER_NAME to disambiguate."
201
+ fi
202
+ QDN=$(echo "$QSO_DNS" | jq -r '.[0]')
203
+ fi
204
+ Q_JSON=$(sf data query --target-org "$ORG" \
205
+ --query "SELECT Id, QueueRoutingConfigId FROM Group WHERE DeveloperName='$QDN' AND Type='Queue'" --json 2>/dev/null)
206
+ QUEUE_ID=$(echo "$Q_JSON" | jq -r '.result.records[0].Id // empty')
207
+ [ -z "$QUEUE_ID" ] && trigger_blocked "Queue '$QDN' not found on org."
208
+ QUEUE_RC_ID=$(echo "$Q_JSON" | jq -r '.result.records[0].QueueRoutingConfigId // empty')
209
+
210
+ # --- Resolve the QueueRoutingConfig ---
211
+ RC_DN="${ROUTING_CONFIG_DEVELOPER_NAME:-}"
212
+ if [ -n "$RC_DN" ] && ! [[ "$RC_DN" =~ ^[A-Za-z][A-Za-z0-9_]{0,79}$ ]]; then
213
+ trigger_blocked "Invalid ROUTING_CONFIG_DEVELOPER_NAME '$RC_DN'."
214
+ fi
215
+ if [ -z "$RC_DN" ] && [ -n "$QUEUE_RC_ID" ]; then
216
+ RC_DN=$(sf data query --target-org "$ORG" \
217
+ --query "SELECT DeveloperName FROM QueueRoutingConfig WHERE Id='$QUEUE_RC_ID'" --json 2>/dev/null \
218
+ | jq -r '.result.records[0].DeveloperName // empty')
219
+ fi
220
+ if [ -z "$RC_DN" ]; then
221
+ case "$TARGET" in
222
+ VoiceCall) RC_DN="Voice_Routing_Config" ;;
223
+ *) RC_DN="Case_Routing_Config" ;;
224
+ esac
225
+ fi
226
+ RC_ID=$(sf data query --target-org "$ORG" \
227
+ --query "SELECT Id FROM QueueRoutingConfig WHERE DeveloperName='$RC_DN'" --json 2>/dev/null \
228
+ | jq -r '.result.records[0].Id // empty')
229
+ [ -z "$RC_ID" ] && trigger_blocked "QueueRoutingConfig '$RC_DN' does not exist. Run service-omni-queue-routing-config-deploy (QRC_ROUTING_TARGET=$TARGET) first, or set ROUTING_CONFIG_DEVELOPER_NAME."
230
+
231
+ # --- Resolve the ServiceChannel (Tooling API), FAIL CLOSED if absent ---
232
+ # The flow embeds the ServiceChannel Id in routeWork; an empty id would deploy a flow routing
233
+ # nothing. The coordinator passes the discovered channel via SERVICE_CHANNEL_DEVELOPER_NAME.
234
+ SC_DN="${SERVICE_CHANNEL_DEVELOPER_NAME:-$DEFAULT_SC_DEVNAME}"
235
+ if [ -n "$SC_DN" ] && ! [[ "$SC_DN" =~ ^[A-Za-z0-9_]{1,80}$ ]]; then
236
+ trigger_blocked "Invalid SERVICE_CHANNEL_DEVELOPER_NAME '$SC_DN'."
237
+ fi
238
+ SC_TOOLING=$(sf data query --target-org "$ORG" --use-tooling-api \
239
+ --query "SELECT Id, MasterLabel FROM ServiceChannel WHERE DeveloperName='$SC_DN'" --json 2>/dev/null || echo '{}')
240
+ SC_ID=$(echo "$SC_TOOLING" | jq -r '.result.records[0].Id // empty')
241
+ SC_LABEL=$(echo "$SC_TOOLING" | jq -r '.result.records[0].MasterLabel // empty')
242
+ if [ -z "$SC_ID" ]; then
243
+ trigger_blocked "ServiceChannel '$SC_DN' not found on org (Tooling API returned no row). For $TARGET run service-omni-service-channel-configure (sobject_type=$TARGET) first - the standard Voice channel is 'sfdc_phone' - or set SERVICE_CHANNEL_DEVELOPER_NAME to an existing channel. Refusing to deploy a flow with an unresolved ServiceChannel."
244
+ fi
245
+ [ -z "$SC_LABEL" ] && SC_LABEL="$SC_DN"
246
+
247
+ # --- PLAN_ONLY: report the resolution, do not write ---
248
+ if [ "${PLAN_ONLY:-}" = "1" ]; then
249
+ jq -n --arg flow "$FLOW_DN" --arg target "$TARGET" --arg q "$QDN" --arg rc "$RC_DN" --arg sc "$SC_DN" \
250
+ --arg routing_type "$ROUTING_TYPE" --arg skill_option "$SKILL_OPTION" \
251
+ '{skill:"service-omni-routing-flow-deploy", status:"action_needed", plan_mode:true, mode:"record_triggered",
252
+ target:$target, flow_developer_name:$flow, routing_type:$routing_type,
253
+ skill_option:(if $routing_type=="SkillsBased" then $skill_option else null end),
254
+ resolved:{queue:$q, routing_config:$rc, service_channel:$sc},
255
+ plan_detail:("Would deploy record-triggered \($routing_type) flow \($flow) binding queue \($q), QRC \($rc), channel \($sc)" + (if $routing_type=="SkillsBased" then " with skillOption \($skill_option)." else "." end)),
256
+ manual_actions:[], blocking_issue:null}'
257
+ exit 0
258
+ fi
259
+
260
+ # --- Idempotency guard with stale-binding detection ---
261
+ # A Flow deploy always mints a new version, so re-runs would grow the version count. If an active
262
+ # version exists AND its embedded queue/QRC/channel Ids still match, report "reused" and skip;
263
+ # if the bindings have drifted, redeploy. OMNI_FLOW_FORCE=1 always redeploys.
264
+ EXISTING_ACTIVE=$(sf data query --target-org "$ORG" --use-tooling-api \
265
+ --query "SELECT Id FROM Flow WHERE Definition.DeveloperName='$FLOW_DN' AND Status='Active' LIMIT 1" --json 2>/dev/null \
266
+ | jq -r '.result.records[0].Id // empty')
267
+ BINDINGS_MATCH="false"
268
+ if [ -n "$EXISTING_ACTIVE" ]; then
269
+ RWORK="$(mktemp -d)"
270
+ cp "$ASSETS_DIR/sfdx-project.json" "$RWORK/sfdx-project.json"
271
+ mkdir -p "$RWORK/force-app/main/default"
272
+ ( cd "$RWORK" && sf project retrieve start --target-org "$ORG" --metadata "Flow:${FLOW_DN}" --json >/dev/null 2>&1 || true )
273
+ ACTIVE_XML=$(find "$RWORK" -name "${FLOW_DN}.flow-meta.xml" 2>/dev/null | head -1)
274
+ if [ -n "$ACTIVE_XML" ] && [ -f "$ACTIVE_XML" ]; then
275
+ # Resource bindings and routing semantics must all match. Without the routing-mode checks, a
276
+ # QueueBased flow with current IDs could be incorrectly reused for a SkillsBased request.
277
+ if grep -qF "$QUEUE_ID" "$ACTIVE_XML" 2>/dev/null \
278
+ && grep -qF "$RC_ID" "$ACTIVE_XML" 2>/dev/null \
279
+ && grep -qF "$SC_ID" "$ACTIVE_XML" 2>/dev/null \
280
+ && grep -qF "<stringValue>${ROUTING_TYPE}</stringValue>" "$ACTIVE_XML" 2>/dev/null \
281
+ && { [ "$ROUTING_TYPE" != "SkillsBased" ] \
282
+ || grep -qF "<stringValue>${SKILL_OPTION}</stringValue>" "$ACTIVE_XML" 2>/dev/null; }; then
283
+ BINDINGS_MATCH="true"
284
+ fi
285
+ fi
286
+ rm -rf "$RWORK"
287
+ fi
288
+ if [ -n "$EXISTING_ACTIVE" ] && [ "$BINDINGS_MATCH" = "true" ] && [ "${OMNI_FLOW_FORCE:-0}" != "1" ]; then
289
+ TRIG_STATUS="reused"
290
+ TSTATE="Unchanged"
291
+ else
292
+ # --- Substitute tokens into a temp copy of the asset and deploy ---
293
+ TWORK="$(mktemp -d)"
294
+ mkdir -p "$TWORK/force-app/main/default/flows"
295
+ cp "$ASSETS_DIR/sfdx-project.json" "$TWORK/sfdx-project.json"
296
+ SC_LABEL_SED="$(sed_repl_escape "$(xml_escape "$SC_LABEL")")"
297
+ sed \
298
+ -e "s|__SERVICE_CHANNEL_ID__|${SC_ID}|g" \
299
+ -e "s|__SERVICE_CHANNEL_DEVNAME__|${SC_DN}|g" \
300
+ -e "s|__SERVICE_CHANNEL_LABEL__|${SC_LABEL_SED}|g" \
301
+ -e "s|__ROUTING_CONFIG_ID__|${RC_ID}|g" \
302
+ -e "s|__QUEUE_ID__|${QUEUE_ID}|g" \
303
+ -e "s|__SKILL_OPTION__|${SKILL_OPTION}|g" \
304
+ "$TEMPLATE_XML" > "$TWORK/force-app/main/default/flows/${FLOW_DN}.flow-meta.xml"
305
+
306
+ TDEPLOY=$(cd "$TWORK" && sf project deploy start --target-org "$ORG" --metadata "Flow:${FLOW_DN}" --json 2>/dev/null || true)
307
+ TDEPLOY_OK=$(echo "$TDEPLOY" | jq -r '.result.success // false' 2>/dev/null)
308
+ if [ "$TDEPLOY_OK" != "true" ]; then
309
+ TERR=$(echo "$TDEPLOY" | jq -r '.result.details.componentFailures // [] | map(.problem) | join("; ")' 2>/dev/null)
310
+ [ -z "$TERR" ] || [ "$TERR" = "null" ] && TERR=$(echo "$TDEPLOY" | jq -r '.message // "Unknown deploy error"' 2>/dev/null)
311
+ trigger_blocked "Record-triggered flow deploy failed: $TERR"
312
+ fi
313
+ TSTATE=$(echo "$TDEPLOY" | jq -r --arg fn "$FLOW_DN" '.result.files[] | select(.fullName==$fn and .type=="Flow") | .state' 2>/dev/null | head -1)
314
+ TSTATE="${TSTATE:-unknown}"
315
+ TRIG_STATUS="reused"
316
+ case "$TSTATE" in Created) TRIG_STATUS="created";; Changed) TRIG_STATUS="updated";; Unchanged) TRIG_STATUS="reused";; *) trigger_blocked "Deploy ok but Flow state unrecognized ('$TSTATE').";; esac
317
+ fi
318
+
319
+ # --- Round-trip active-version proof ---
320
+ # Component state only proves the deploy transaction; a flow with no active version never fires.
321
+ # Prefer FlowDefinitionView, then fall back to the Tooling Flow row because an unrelated malformed
322
+ # managed-package definition can make FlowDefinitionView fail for the entire org.
323
+ TRIG_ACTIVE_VER=$(sf data query --target-org "$ORG" \
324
+ --query "SELECT ApiName, ActiveVersionId FROM FlowDefinitionView WHERE ApiName='$FLOW_DN'" --json 2>/dev/null \
325
+ | jq -r '.result.records[0].ActiveVersionId // ""' 2>/dev/null || echo "")
326
+ if [ -z "$TRIG_ACTIVE_VER" ] || [ "$TRIG_ACTIVE_VER" = "null" ]; then
327
+ TRIG_ACTIVE_VER=$(sf data query --target-org "$ORG" --use-tooling-api \
328
+ --query "SELECT Id FROM Flow WHERE Definition.DeveloperName='$FLOW_DN' AND Status='Active' LIMIT 1" --json 2>/dev/null \
329
+ | jq -r '.result.records[0].Id // ""' 2>/dev/null || echo "")
330
+ fi
331
+ if [ -z "$TRIG_ACTIVE_VER" ] || [ "$TRIG_ACTIVE_VER" = "null" ]; then
332
+ trigger_blocked "Record-triggered flow '$FLOW_DN' deployed (state=$TSTATE), but neither FlowDefinitionView nor the Tooling Flow query found an active version, so routing would not fire. Re-run, or activate the flow in Setup → Flows."
333
+ fi
334
+
335
+ # --- Runtime proof (optional): fire the trigger and look for a PendingServiceRouting/AgentWork ---
336
+ RP_ATTEMPTED="false"; RP_SUCCESS="false"; RP_REASON=""; RP_RECORD_ID=""; RP_PSR="null"; RP_AW="null"
337
+ # SkillsBased acceptance signal: SkillRequirement rows the platform attached to the PSR (the exact
338
+ # thing that was missing when routeWork hardcoded QueueBased - W-24069467).
339
+ RP_SKILLREQS="null"; RP_SKILLREQ_COUNT="0"
340
+ if [ "$RUNTIME_PROOF" = "true" ]; then
341
+ RP_ATTEMPTED="true"
342
+ case "$TARGET" in
343
+ Case) REC=$(sf data create record --target-org "$ORG" --sobject Case --values "Subject='Omni routing proof' Status='New' Origin='Phone'" --json 2>/dev/null || true) ;;
344
+ VoiceCall) REC=$(sf data create record --target-org "$ORG" --sobject VoiceCall --values "CallType='Inbound'" --json 2>/dev/null || true) ;;
345
+ esac
346
+ RP_RECORD_ID=$(echo "$REC" | jq -r '.result.id // empty' 2>/dev/null)
347
+ # Register the record for deletion on exit (trap) so the proof never leaves test data behind.
348
+ [ -n "$RP_RECORD_ID" ] && CLEANUP_RECORD_ID="$RP_RECORD_ID"
349
+ if [ -z "$RP_RECORD_ID" ]; then
350
+ RP_REASON="Could not create a $TARGET record to fire the trigger: $(echo "$REC" | jq -r '.message // (.result.errors[0].message) // "unknown"' 2>/dev/null | head -c 240). On orgs without Service Cloud Voice provisioned, VoiceCall is not directly insertable - deploy is proven, runtime proof is not."
351
+ else
352
+ # Poll for PendingServiceRouting then AgentWork (async). Cadence env-tunable
353
+ # (OMNI_PROOF_POLL_TRIES / OMNI_PROOF_POLL_SLEEP); defaults ~30s.
354
+ RP_TRIES="${OMNI_PROOF_POLL_TRIES:-6}"
355
+ RP_SLEEP="${OMNI_PROOF_POLL_SLEEP:-5}"
356
+ for _ in $(seq 1 "$RP_TRIES"); do
357
+ PSR=$(sf data query --target-org "$ORG" \
358
+ --query "SELECT Id, IsReadyForRouting, RoutingModel FROM PendingServiceRouting WHERE WorkItemId='$RP_RECORD_ID' ORDER BY CreatedDate DESC LIMIT 1" --json 2>/dev/null || echo '{}')
359
+ if [ "$(echo "$PSR" | jq -r '.result.totalSize // 0')" != "0" ]; then
360
+ RP_PSR=$(echo "$PSR" | jq -c '.result.records[0]')
361
+ if [ "$ROUTING_TYPE" = "SkillsBased" ]; then
362
+ _psr_id=$(echo "$PSR" | jq -r '.result.records[0].Id')
363
+ # SkillRequirement has no SkillLevel column; its skill-strength field is SkillNumber.
364
+ SREQ=$(sf data query --target-org "$ORG" \
365
+ --query "SELECT Id, SkillId, SkillNumber, IsAdditionalSkill, SkillPriority FROM SkillRequirement WHERE RelatedRecordId='$_psr_id'" --json 2>/dev/null || true)
366
+ # A failed query prints error JSON to stdout THEN exits non-zero, so a `|| echo '{}'`
367
+ # fallback would concatenate two JSON values and break the downstream --argjson. Validate
368
+ # that we captured a single well-formed value and fall back to empty otherwise.
369
+ echo "$SREQ" | jq -e . >/dev/null 2>&1 || SREQ='{}'
370
+ RP_SKILLREQ_COUNT=$(echo "$SREQ" | jq -r '.result.totalSize // 0' 2>/dev/null || echo 0)
371
+ RP_SKILLREQS=$(echo "$SREQ" | jq -c '.result.records // []' 2>/dev/null || echo '[]')
372
+ if [ "$RP_SKILLREQ_COUNT" -gt 0 ]; then
373
+ RP_SUCCESS="true"
374
+ break
375
+ fi
376
+ else
377
+ RP_SUCCESS="true"
378
+ break
379
+ fi
380
+ fi
381
+ AW=$(sf data query --target-org "$ORG" \
382
+ --query "SELECT Id, Status FROM AgentWork WHERE WorkItemId='$RP_RECORD_ID' ORDER BY CreatedDate DESC LIMIT 1" --json 2>/dev/null || echo '{}')
383
+ if [ "$(echo "$AW" | jq -r '.result.totalSize // 0')" != "0" ]; then
384
+ RP_AW=$(echo "$AW" | jq -c '.result.records[0]')
385
+ if [ "$ROUTING_TYPE" != "SkillsBased" ]; then
386
+ RP_SUCCESS="true"
387
+ break
388
+ fi
389
+ fi
390
+ sleep "$RP_SLEEP"
391
+ done
392
+ if [ "$RP_SUCCESS" != "true" ] && [ "$ROUTING_TYPE" = "SkillsBased" ] \
393
+ && [ "$RP_PSR" != "null" ] && [ "$RP_SKILLREQ_COUNT" = "0" ]; then
394
+ RP_REASON="A PendingServiceRouting row appeared for record $RP_RECORD_ID, but it has no SkillRequirement rows. SkillsBased runtime proof requires at least one SkillRequirement produced by the active WorkSkillRouting rule."
395
+ elif [ "$RP_SUCCESS" != "true" ]; then
396
+ RP_REASON="Record $RP_RECORD_ID created but no PendingServiceRouting/AgentWork appeared within the proof window. The flow deployed and is Active; routing may require an online agent, a configured Contact Center, or more time."
397
+ fi
398
+ fi
399
+ fi
400
+
401
+ # Mandatory-proof gate: when proof is required, a missing PendingServiceRouting|AgentWork blocks.
402
+ if [ "$REQUIRE_PROOF" = "true" ] && [ "$RP_SUCCESS" != "true" ]; then
403
+ jq -n \
404
+ --arg flow "$FLOW_DN" --arg target "$TARGET" --arg state "$TSTATE" --arg active_ver "$TRIG_ACTIVE_VER" \
405
+ --arg routing_type "$ROUTING_TYPE" --arg skill_option "$SKILL_OPTION" \
406
+ --arg q "$QDN" --arg qid "$QUEUE_ID" --arg rc "$RC_DN" --arg rcid "$RC_ID" --arg sc "$SC_DN" --arg scid "$SC_ID" \
407
+ --arg rp_rec "$RP_RECORD_ID" --argjson rp_psr "$RP_PSR" --argjson rp_aw "$RP_AW" --arg rp_reason "$RP_REASON" \
408
+ --argjson rp_skillreqs "$RP_SKILLREQS" --arg rp_skillreq_count "$RP_SKILLREQ_COUNT" \
409
+ '{skill:"service-omni-routing-flow-deploy", status:"blocked", mode:"record_triggered", target:$target,
410
+ flow_developer_name:$flow, flow_state:$state, flow_active:true, flow_active_version_id:$active_ver,
411
+ routing_type:$routing_type, skill_option:(if $routing_type=="SkillsBased" then $skill_option else null end),
412
+ resolved:{queue:{developer_name:$q,id:$qid}, routing_config:{developer_name:$rc,id:$rcid}, service_channel:{developer_name:$sc,id:(if $scid=="" then null else $scid end)}},
413
+ runtime_proof:{attempted:true, required:true, success:false, record_id:(if $rp_rec=="" then null else $rp_rec end), pending_service_routing:$rp_psr, agent_work:$rp_aw, skill_requirement_count:($rp_skillreq_count|tonumber), skill_requirements:(if $routing_type=="SkillsBased" then $rp_skillreqs else null end), reason:(if $rp_reason=="" then null else $rp_reason end)},
414
+ manual_actions:[], blocking_issue:("Runtime routing proof REQUIRED but not observed: " + (if $rp_reason=="" then "no PendingServiceRouting/AgentWork appeared" else $rp_reason end) + " (VoiceCall runtime proof needs a provisioned Contact Center + an online agent).")}'
415
+ exit 1
416
+ fi
417
+
418
+ jq -n \
419
+ --arg status "$TRIG_STATUS" --arg flow "$FLOW_DN" --arg target "$TARGET" --arg state "$TSTATE" --arg active_ver "$TRIG_ACTIVE_VER" \
420
+ --arg routing_type "$ROUTING_TYPE" --arg skill_option "$SKILL_OPTION" \
421
+ --arg q "$QDN" --arg qid "$QUEUE_ID" --arg rc "$RC_DN" --arg rcid "$RC_ID" --arg sc "$SC_DN" --arg scid "$SC_ID" \
422
+ --arg rp_attempted "$RP_ATTEMPTED" --arg rp_success "$RP_SUCCESS" --arg rp_reason "$RP_REASON" \
423
+ --arg rp_required "$REQUIRE_PROOF" \
424
+ --arg rp_rec "$RP_RECORD_ID" --argjson rp_psr "$RP_PSR" --argjson rp_aw "$RP_AW" \
425
+ --argjson rp_skillreqs "$RP_SKILLREQS" --arg rp_skillreq_count "$RP_SKILLREQ_COUNT" \
426
+ '{
427
+ skill:"service-omni-routing-flow-deploy",
428
+ status:$status,
429
+ mode:"record_triggered",
430
+ target:$target,
431
+ flow_developer_name:$flow,
432
+ flow_state:$state,
433
+ flow_active:true,
434
+ flow_active_version_id:$active_ver,
435
+ routing_type:$routing_type,
436
+ skill_option:(if $routing_type=="SkillsBased" then $skill_option else null end),
437
+ resolved:{queue:{developer_name:$q, id:$qid}, routing_config:{developer_name:$rc, id:$rcid}, service_channel:{developer_name:$sc, id:(if $scid=="" then null else $scid end)}},
438
+ runtime_proof:{
439
+ attempted:($rp_attempted=="true"),
440
+ required:($rp_required=="true"),
441
+ success:($rp_success=="true"),
442
+ record_id:(if $rp_rec=="" then null else $rp_rec end),
443
+ pending_service_routing:$rp_psr,
444
+ agent_work:$rp_aw,
445
+ skill_requirement_count:($rp_skillreq_count|tonumber),
446
+ skill_requirements:(if $routing_type=="SkillsBased" then $rp_skillreqs else null end),
447
+ reason:(if $rp_reason=="" then null else $rp_reason end)
448
+ },
449
+ manual_actions:[],
450
+ blocking_issue:null
451
+ }'
452
+ exit 0
453
+ fi
454
+
455
+ # Deploy - explicit --metadata (not --source-dir) so a sibling metadata file can't piggyback.
456
+ DEPLOY_JSON=$(cd "$ASSETS_DIR" && sf project deploy start --target-org "$ORG" \
457
+ --metadata "Flow:${FLOW_DN}" \
458
+ --json 2>/dev/null || true)
459
+
460
+ DEPLOY_SUCCESS=$(echo "$DEPLOY_JSON" | jq -r '.result.success // false' 2>/dev/null)
461
+ DEPLOY_ID=$(echo "$DEPLOY_JSON" | jq -r '.result.id // null' 2>/dev/null)
462
+
463
+ translate_deploy_error() {
464
+ local raw="$1"
465
+ case "$raw" in
466
+ *INVALID_TYPE*"Flow"*)
467
+ echo "Flow metadata type not enabled on this org (unusual - usually indicates the org edition doesn't include Flow Builder)."
468
+ ;;
469
+ *"Flow.*is not valid for this API version"*|*"API version"*)
470
+ echo "Flow XML apiVersion or asset version mismatch. Confirm assets/force-app/main/default/flows/${FLOW_DN}.flow-meta.xml <apiVersion> is <= the org's current API version ($API_VERSION)."
471
+ ;;
472
+ *"Invalid element name"*|*"was not expected"*)
473
+ echo "Flow XML schema mismatch. The Flow metadata schema is strict; check element ordering and spelling against v66 Metadata API docs."
474
+ ;;
475
+ *INSUFFICIENT_ACCESS*)
476
+ echo "Executing user lacks Metadata API deploy permissions. Re-authenticate as a System Administrator or grant Author Apex + Manage Flows."
477
+ ;;
478
+ *)
479
+ echo "$raw"
480
+ ;;
481
+ esac
482
+ }
483
+
484
+ if [ "$DEPLOY_SUCCESS" != "true" ]; then
485
+ FAILURES=$(echo "$DEPLOY_JSON" | jq -r '.result.details.componentFailures // [] | map(.problem) | join("; ")' 2>/dev/null)
486
+ if [ -z "$FAILURES" ] || [ "$FAILURES" = "null" ]; then
487
+ FAILURES=$(echo "$DEPLOY_JSON" | jq -r '.message // .result.errorMessage // "Unknown deploy error"' 2>/dev/null)
488
+ fi
489
+ FRIENDLY=$(translate_deploy_error "$FAILURES")
490
+
491
+ jq -n \
492
+ --arg deploy_id "$DEPLOY_ID" \
493
+ --arg flow "$FLOW_DN" \
494
+ --arg blocking "$FRIENDLY" \
495
+ --arg raw "$FAILURES" \
496
+ '{
497
+ skill: "service-omni-routing-flow-deploy",
498
+ status: "blocked",
499
+ flow_developer_name: $flow,
500
+ flow_state: "not_deployed",
501
+ flow_active: false,
502
+ deploy_id: (if $deploy_id == "null" or $deploy_id == "" then null else $deploy_id end),
503
+ invocation_smoke_test: {attempted: false, success: false, reason: "deploy_failed"},
504
+ manual_actions: [],
505
+ blocking_issue: $blocking,
506
+ raw_error: $raw
507
+ }'
508
+ exit 1
509
+ fi
510
+
511
+ FLOW_STATE=$(echo "$DEPLOY_JSON" | jq -r --arg fn "$FLOW_DN" \
512
+ '.result.files[] | select(.fullName == $fn and .type == "Flow") | .state' 2>/dev/null | head -1)
513
+ FLOW_STATE="${FLOW_STATE:-unknown}"
514
+
515
+ case "$FLOW_STATE" in
516
+ Created|Changed|Unchanged) ;;
517
+ *)
518
+ jq -n \
519
+ --arg flow "$FLOW_DN" \
520
+ --arg st "$FLOW_STATE" \
521
+ --arg deploy_id "$DEPLOY_ID" \
522
+ '{
523
+ skill: "service-omni-routing-flow-deploy",
524
+ status: "blocked",
525
+ flow_developer_name: $flow,
526
+ flow_state: $st,
527
+ flow_active: false,
528
+ deploy_id: (if $deploy_id == "null" or $deploy_id == "" then null else $deploy_id end),
529
+ invocation_smoke_test: {attempted: false, success: false, reason: "unrecognized_component_state"},
530
+ manual_actions: [],
531
+ blocking_issue: ("Deploy reported success overall but Flow component has an unrecognized/absent state \"" + $st + "\" (expected Created, Changed, or Unchanged).")
532
+ }'
533
+ exit 1
534
+ ;;
535
+ esac
536
+
537
+ # Post-verify #1: retrieve the deployed Flow and confirm <status>Active</status> (a flow can deploy
538
+ # in Draft/Obsolete - a silent failure, since autolaunched flows must be Active to be CLI-invocable).
539
+ VERIFY_WORK="$(mktemp -d)"
540
+ trap 'rm -rf "$VERIFY_WORK"' EXIT
541
+ cp "$ASSETS_DIR/sfdx-project.json" "$VERIFY_WORK/sfdx-project.json"
542
+ mkdir -p "$VERIFY_WORK/force-app/main/default"
543
+ # Retrieve for its side effect (writes the flow XML); verified by inspecting the file below.
544
+ ( cd "$VERIFY_WORK" && sf project retrieve start --target-org "$ORG" \
545
+ --metadata "Flow:${FLOW_DN}" --json >/dev/null 2>&1 || true )
546
+ RETRIEVED_XML=$(find "$VERIFY_WORK" -name "${FLOW_DN}.flow-meta.xml" 2>/dev/null | head -1)
547
+
548
+ FLOW_ACTIVE="unverified"
549
+ if [ -n "$RETRIEVED_XML" ] && [ -f "$RETRIEVED_XML" ]; then
550
+ if grep -qE "<status>Active</status>" "$RETRIEVED_XML"; then
551
+ FLOW_ACTIVE="true"
552
+ else
553
+ FLOW_ACTIVE="false"
554
+ fi
555
+ fi
556
+
557
+ if [ "$FLOW_ACTIVE" != "true" ]; then
558
+ if [ "$FLOW_ACTIVE" = "false" ]; then
559
+ BLOCK_MSG="Flow deployed but not Active on post-verify (the retrieved metadata does not carry <status>Active</status>). The flow XML must declare <status>Active</status> for autolaunched CLI invocation to work."
560
+ else
561
+ BLOCK_MSG="Flow deployed but the Active-status verification could not be performed (retrieve returned no Flow XML - inconclusive)."
562
+ fi
563
+ jq -n \
564
+ --arg flow "$FLOW_DN" \
565
+ --arg st "$FLOW_STATE" \
566
+ --arg deploy_id "$DEPLOY_ID" \
567
+ --arg active "$FLOW_ACTIVE" \
568
+ --arg msg "$BLOCK_MSG" \
569
+ '{
570
+ skill: "service-omni-routing-flow-deploy",
571
+ status: "blocked",
572
+ flow_developer_name: $flow,
573
+ flow_state: $st,
574
+ flow_active: ($active == "true"),
575
+ deploy_id: (if $deploy_id == "null" or $deploy_id == "" then null else $deploy_id end),
576
+ invocation_smoke_test: {attempted: false, success: false, reason: "flow_not_active"},
577
+ manual_actions: [],
578
+ blocking_issue: $msg
579
+ }'
580
+ exit 1
581
+ fi
582
+
583
+ # Post-verify #2: invoke the flow via Actions REST with dryRun=true (proves CLI-executability).
584
+ # POST /services/data/vXX.X/actions/custom/flow/<FlowDN>, body {"inputs":[{"dryRun":true}]};
585
+ # success = [0].isSuccess && [0].outputValues.dryRunOk.
586
+ INVOKE_ATTEMPTED="false"
587
+ INVOKE_SUCCESS="false"
588
+ INVOKE_REASON=""
589
+ INVOKE_RAW="{}"
590
+ INVOKE_DRY_RUN_OK="false"
591
+
592
+ if [ "$SKIP_INVOKE" != "true" ]; then
593
+ INVOKE_ATTEMPTED="true"
594
+ INVOKE_ENDPOINT="/services/data/v${API_VERSION}/actions/custom/flow/${FLOW_DN}"
595
+ INVOKE_BODY='{"inputs":[{"dryRun":true}]}'
596
+
597
+ INVOKE_RAW=$(sf api request rest \
598
+ --method POST \
599
+ --target-org "$ORG" \
600
+ --body "$INVOKE_BODY" \
601
+ "$INVOKE_ENDPOINT" \
602
+ 2>&1 || true)
603
+
604
+ # sf api request rest (beta) prints a warning line before the JSON body; strip everything up to
605
+ # the first line starting with [ or { - that's the actual body.
606
+ INVOKE_JSON=$(printf '%s' "$INVOKE_RAW" | awk '/^[[{]/,0')
607
+
608
+ # Parse the stripped payload (raw array for Actions API, or an error-wrapping object).
609
+ if echo "$INVOKE_JSON" | jq -e '.' >/dev/null 2>&1; then
610
+ IS_SUCCESS=$(echo "$INVOKE_JSON" | jq -r 'if type == "array" then .[0].isSuccess else .isSuccess // false end' 2>/dev/null)
611
+ INVOKE_DRY_RUN_OK=$(echo "$INVOKE_JSON" | jq -r 'if type == "array" then (.[0].outputValues.dryRunOk // false) else (.outputValues.dryRunOk // false) end' 2>/dev/null)
612
+ if [ "$IS_SUCCESS" = "true" ] && [ "$INVOKE_DRY_RUN_OK" = "true" ]; then
613
+ INVOKE_SUCCESS="true"
614
+ else
615
+ INVOKE_REASON=$(echo "$INVOKE_JSON" | jq -r '
616
+ if type == "array" then
617
+ (.[0].errors // []) | map(.message) | join("; ")
618
+ else
619
+ (.errors // []) | map(.message) | join("; ")
620
+ end' 2>/dev/null)
621
+ if [ -z "$INVOKE_REASON" ] || [ "$INVOKE_REASON" = "null" ]; then
622
+ INVOKE_REASON="Invocation returned isSuccess=$IS_SUCCESS, dryRunOk=$INVOKE_DRY_RUN_OK (expected true/true)."
623
+ fi
624
+ fi
625
+ else
626
+ INVOKE_REASON="Actions API returned non-JSON after stripping the sf beta-warning preamble. Raw first 200 chars: $(printf '%s' "$INVOKE_RAW" | head -c 200)"
627
+ fi
628
+
629
+ if [ "$INVOKE_SUCCESS" != "true" ]; then
630
+ jq -n \
631
+ --arg flow "$FLOW_DN" \
632
+ --arg st "$FLOW_STATE" \
633
+ --arg deploy_id "$DEPLOY_ID" \
634
+ --arg reason "$INVOKE_REASON" \
635
+ --argjson raw "$(echo "$INVOKE_JSON" | jq -R -s '.' 2>/dev/null || echo '""')" \
636
+ '{
637
+ skill: "service-omni-routing-flow-deploy",
638
+ status: "blocked",
639
+ flow_developer_name: $flow,
640
+ flow_state: $st,
641
+ flow_active: true,
642
+ deploy_id: (if $deploy_id == "null" or $deploy_id == "" then null else $deploy_id end),
643
+ invocation_smoke_test: {attempted: true, success: false, reason: $reason, raw_response: $raw},
644
+ manual_actions: [{id: "INVOCATION_FAILED", title: "Flow deployed & Active but Actions REST invocation failed. Rerun with --skip-invoke to accept deploy-only, or investigate the API response.", endpoint: ("/services/data/v" + "'"$API_VERSION"'" + "/actions/custom/flow/" + $flow)}],
645
+ blocking_issue: ("Flow deployed and Active, but invocation via Actions REST failed: " + $reason)
646
+ }'
647
+ exit 1
648
+ fi
649
+ fi
650
+
651
+ # Skill-level status.
652
+ STATUS="reused"
653
+ case "$FLOW_STATE" in
654
+ Created) STATUS="created" ;;
655
+ Changed) STATUS="updated" ;;
656
+ Unchanged) STATUS="reused" ;;
657
+ esac
658
+
659
+ jq -n \
660
+ --arg status "$STATUS" \
661
+ --arg flow "$FLOW_DN" \
662
+ --arg flow_state "$FLOW_STATE" \
663
+ --arg deploy_id "$DEPLOY_ID" \
664
+ --arg api_ver "$API_VERSION" \
665
+ --arg invoke_attempted "$INVOKE_ATTEMPTED" \
666
+ --arg invoke_success "$INVOKE_SUCCESS" \
667
+ --arg dry_run_ok "$INVOKE_DRY_RUN_OK" \
668
+ '{
669
+ skill: "service-omni-routing-flow-deploy",
670
+ status: $status,
671
+ flow_developer_name: $flow,
672
+ flow_state: $flow_state,
673
+ flow_active: true,
674
+ deploy_id: $deploy_id,
675
+ invocation_smoke_test: {
676
+ attempted: ($invoke_attempted == "true"),
677
+ success: ($invoke_success == "true"),
678
+ cli_path: "actions_rest",
679
+ endpoint: ("/services/data/v" + $api_ver + "/actions/custom/flow/" + $flow),
680
+ dry_run_ok: ($dry_run_ok == "true")
681
+ },
682
+ manual_actions: [],
683
+ blocking_issue: null
684
+ }'