service-de-channel-activate
Activate an Enhanced `MessagingChannel` (WhatsApp / Apple / Facebook / SMS / RCS) by PATCHing `MessagingChannelUsage.DeploymentStatus` from `Disabled` to `Provisioning` via the standard REST sobject endpoint. The UDD save-hook fires on the REST write, dispatches by `MessageType` for the external cal
By forcedotcom · 596 installs
npx skills add forcedotcom/sf-skills --skill service-de-channel-activate
Source repository · Upstream listing
Activating a Messaging Channel
What this skill does
Given a {CHANNEL ID} , reads the channel's MessagingChannelUsage.Id , then fires PATCH /services/data/v{V}/sobjects/MessagingChannelUsage/{MCU ID} with body {"DeploymentStatus":"Provisioning"} . The server side chain:
1. MessagingChannelUsageFunctions.validateBeforeSave runs validateDeploymentStatus → validateChannelReadinessOnProvisioning . For WhatsApp this confirms consent is configured. Rejected writes return HTTP 400 FIELD INTEGRITY EXCEPTION .
2. MessagingChannelUsageFunctions.saveHook PostStmtExecuteOnce fires unconditionally after the UPDATE statement. It calls MessagingChannelUsageFunctionsHelper.handlePostSave which registers a post commit TransactionObserver .
3. At commit, the observer calls ConversationChannelUsageDeploymentStatusService.handleProvisioning (inherited from AbstractChannelUsageDeploymentStatusService ), which:
Calls runProvisioning — switch dispatches by MessageType for the external callout:
WHATS APP → registerCsotWhatsAppNumber → LiveMessageSetupApi.registerWhatsAppNumber → Meta /register + status verification. 15 21s wall clock.
FACEBOOK → metaGraphApiService.subscribeFacebookPage .
TEXT → registerCsotSms .
AppleBusinessChat , Line , everything else → default branch, no external callout, no network wait.
On success: writes DeploymentStatus = 'Active' via PLSQL.
On failure: writes DeploymentStatus = 'Error' plus ErrorReason / ErrorDetails .
4. Inside the same observer, a second pass syncs MessagingChannel.IsActive = true once MCU reaches Active (the isTransitioningStatus flag skips the flip while status is still Provisioning ).
All synchronous within the PATCH request — the 204 response only comes back after the full chain completes. WhatsApp: ~15 21s (Meta /register round trip). Apple / Line: ~1s (no external call; just the local save hook + observer + PLSQL write). Verified on wadtesting 2026 04 30.
Reference File Index
Reference file Load when
references/phone verification.md Stage 3 comes back with ErrorReason === "VERIFICATION REQUIRED" (WhatsApp only) — the phone number OTP verification sub flow.
references/worked examples.md You want a reference run of the WhatsApp happy path, Apple activation, a readiness failure, or the already active no op.
references/gotchas.md Troubleshooting an unexpected result, or before modifying this skill — the eleven known gotchas.
Why REST PATCH instead of Apex?
A direct REST PATCH produces the identical save hook chain as the old activateChannelUsage Apex method, with substantially less machinery — no CSRF cookie acquisition, no bootstrap fetch, no Aura response parsing, no double wrapped returnValue . REST semantics are honest: 204 means the transition succeeded; 4xx means it didn't.
Code proof: MessagingChannelUsageFunctions.saveHook PostStmtExecuteOnce fires on any DML path (REST, SOAP, Apex, Metadata API) — there is no Apex specific gate. The entity XML ( MessagingChannelUsage.entity.xml ) marks DeploymentStatus as editAccess="always" with no <readonly attribute. The transition validator ( getValidAPIStatusTransitions ) allows Disabled → Provisioning (and New → Provisioning , and Error → Provisioning Deprovisioning , and Active → Deprovisioning ). The DB only transitions Provisioning → Active Error are reserved for the observer's PLSQL call — that's why we write Provisioning and let the server pick the terminal state.
When NOT to use this skill
The channel is already IsActive=true . Re firing is blocked by the API transition validator ( Active → Provisioning is not in getValidAPIStatusTransitions() ) — the PATCH would return 400. The Stage 1 precondition check catches this and emits noop:true .
Routing isn't configured. activateChannelUsage used to fail with LiveMessageSetupException / nullQueueId at the Apex entry point. With the REST path the same guard lives in validateChannelReadinessOnProvisioning — write with SessionHandlerId=null && FallbackQueueId=null → 400 FIELD INTEGRITY EXCEPTION . Run service de channel routing configure first. The Stage 1 check still runs defensively.
The MCU doesn't exist. Can't PATCH a row that's missing. Run the insertion skill first — it always creates the MCU as a side effect of addChannel .
Inputs (from caller)
{CHANNEL ID} — a 15 or 18 char MessagingChannel.Id (prefix 0Mj ). The channel must already exist with a non null SessionHandlerId or FallbackQueueId .
{ORG ALIAS} — optional; the sf CLI target org alias. Default: whatever sf config get target org returns. Used for OAuth and SOQL reads.
{API VERSION} — optional; REST API version. Default: 68.0 . Any version where MessagingChannelUsage is addressable as a standard sobject is fine (v50+ should work; not exhaustively tested).
Unlike the old Aura based version of this skill, there are no {POLL TIMEOUT S} / {POLL INTERVAL S} inputs — the PATCH is synchronous end to end.
Output (to caller)
Success — channel is live:
Success — no op (already active):
Failure — precondition not met:
Failure — validator or server side provisioning error:
Failure — auth / transport:
Stage 1: Precondition checks
Read the channel, then its MCU, as two separate SOQL calls. A combined subquery would be cheaper but fails on orgs where the child relationship is unnameable (see gotcha 4).
Let channel = /tmp/amc precheck channel.json records[0] and mcu = /tmp/amc precheck mcu.json records[0] :
Condition Envelope
Channel query returned 0 records {ok:false, kind:"channel missing", hint:"MessagingChannel id not found"}
channel.IsActive === true {ok:true, noop:true, channelId, message:"Channel already active"} — return
channel.SessionHandlerId == null && channel.FallbackQueueId == null {ok:false, kind:"no routing", hint:"run service de channel routing configure first"}
MCU query returned 0 records {ok:false, kind:"no mcu", hint:"no MessagingChannelUsage row for this channel — run the insertion skill first"}
Otherwise Record {MCU ID} = mcu.Id , {MESSAGE TYPE} = channel.MessageType , {MESSAGING PLATFORM KEY} = channel.MessagingPlatformKey , {INITIAL MCU STATUS} = mcu.DeploymentStatus and continue to Stage 2.
Also record {T0} (epoch ms at start of Stage 2) so the final envelope can report durationMs .
Stage 1.1: Fast path for already provisioning MCU
If {INITIAL MCU STATUS} === "Provisioning" — the MCU is already mid flight from a prior call in this transaction window. Skip Stage 2 (firing the PATCH) entirely and jump to Stage 3 (verification). This is a rare race guard: the observer is synchronous with the PATCH, so by the time the caller sees the 204 the status is already terminal ( Active or Error ) — Provisioning should be invisible from outside. If we do see it in the precheck, something wrote Provisioning in a separate DML and the observer is still mid flight — don't fire a second PATCH.
Stage 2: PATCH MessagingChannelUsage.DeploymentStatus = "Provisioning"
Use sf api request rest so authentication stays inside the CLI's transport — no OAuth token is ever extracted into shell state.
Fire the PATCH. This call can take 15 30 seconds for WhatsApp — the observer runs synchronously, including Meta's /register round trip. sf api request rest has no separate client side timeout to raise; it waits on the underlying HTTP call.
include prints the HTTP status/headers block ahead of the (typically empty, on 204) body — read the status line from that block rather than a w style trailing marker.
Classify by HTTP status:
Status Body Handling
204 empty Success. The observer ran to completion; MCU is Active or Error . Continue to Stage 3 to read the terminal state.
400 [{errorCode:"FIELD INTEGRITY EXCEPTION", message:"..."}] Validator rejection. See table below.
400 [{errorCode:"INVALID FIELD FOR INSERT UPDATE" or "MALFORMED ID", ...}] Skill bug — the MCU ID from Stage 1 was wrong, or the body shape is off. Emit {ok:false, kind:"transport", status:400, message} .
401 (usually empty) Bearer token invalid. Emit {ok:false, kind:"auth", hint:"OAuth token invalid / expired — run 'sf org login web'"} .
403 [{errorCode:"INSUFFICIENT ACCESS"}] User lacks perm to write DeploymentStatus . Emit {ok:false, kind:"business", errorCode:"INSUFFICIENT ACCESS", message} .
5xx varies Transport. The observer may have partially committed — Stage 3's SOQL is the source of truth. Read MCU state; if it's Active , report success with a warning; if Error or still Disabled , classify as {ok:false, kind:"transport", status, message} .
Known 400 FIELD INTEGRITY EXCEPTION messages:
Message fragment Meaning Envelope
"invalid deployment status transition" The current status doesn't allow → Provisioning (e.g. MCU is already Active — Stage 1 should have caught this, but there's a race window). Re read MCU; if now Active , emit success noop. If Provisioning , Stage 3 poll. Otherwise emit {ok:false, kind:"readiness failed", ...} .
"consent" / "keyword" / mentions of STOP/HELP validateChannelReadinessOnProvisioning rejected — channel doesn't have required consent configured. {ok:false, kind:"readiness failed", errorCode:"FIELD INTEGRITY EXCEPTION", message, hint:"channel requires a ConsentType and a matching MsgChannelLanguageKeyword record (opt out keyword + confirmation) before activation — run service de channel consent configure"} .
"routing" / "queue" / "SessionHandler" Routing precondition (Stage 1 should have caught, but the validator re checks). {ok:false, kind:"no routing", message, hint:"run service de channel routing configure first"} .
other Unrecognized validator error. {ok:false, kind:"readiness failed", errorCode, message} .
Stage 3: Read the terminal MCU state
The PATCH is synchronous, so by the time we're here the MCU is Active or Error — no polling. Read once:
Status Handling
Active Continue to Stage 4.
Error with ErrorReason === "VERIFICATION REQUIRED" For WhatsApp channels only: phone number needs OTP verification. Continue to Stage 3.2 (WhatsApp verification flow).
Error (other) Emit {ok:false, kind:"provisioning error", mcuId, errorReason, errorDetails} .
Provisioning Unexpected — the observer was supposed to terminate before the PATCH returned 204. Fall through to a defensive poll (see Stage 3.1).
Disabled The PATCH returned 204 but the write didn't take? Emit {ok:false, kind:"transport", message:"PATCH returned 204 but MCU is still Disabled — observer didn't commit"} .
Stage 3.2: WhatsApp phone number verification (only if ErrorReason === "VERIFICATION REQUIRED")
This sub flow only runs for WhatsApp channels when activation fails with ErrorReason === "VERIFICATION REQUIRED" — the phone number needs OTP verification with Meta before it can be registered. If the MCU comes back with ErrorReason === "VERIFICATION REQUIRED" (WhatsApp only), load references/phone verification.md and follow it to drive the phone number verification sub flow (request code → prompt user → validate code → retry activation once).
Stage 3.1: Defensive poll (only if Stage 3 saw Provisioning )
The observer's external callout block (WhatsApp/Facebook/SMS) runs synchronously inside the PATCH request — there's no async queue indirection in runProvisioning for any message type (verified against the switch/case in ConversationChannelUsageDeploymentStatusService ). So a Provisioning status at Stage 3 should not happen in steady state. Possible causes if it does: an exception thrown after the external call s