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:
MessagingChannelUsageFunctions.validateBeforeSaverunsvalidateDeploymentStatus→validateChannelReadinessOnProvisioning. For WhatsApp this confirms consent is configured. Rejected writes return HTTP 400FIELD_INTEGRITY_EXCEPTION.MessagingChannelUsageFunctions.saveHook_PostStmtExecuteOncefires unconditionally after the UPDATE statement. It callsMessagingChannelUsageFunctionsHelper.handlePostSavewhich registers a post-commitTransactionObserver.- At commit, the observer calls
ConversationChannelUsageDeploymentStatusService.handleProvisioning(inherited fromAbstractChannelUsageDeploymentStatusService), which:- Calls
runProvisioning—switch-dispatches byMessageTypefor 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 →defaultbranch, no external callout, no network wait.
- On success: writes
DeploymentStatus = 'Active'via PLSQL. - On failure: writes
DeploymentStatus = 'Error'plusErrorReason/ErrorDetails.
- Calls
- Inside the same observer, a second pass syncs
MessagingChannel.IsActive = trueonce MCU reachesActive(theisTransitioningStatusflag skips the flip while status is stillProvisioning).
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
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 → Provisioningis not ingetValidAPIStatusTransitions()) — the PATCH would return 400. The Stage 1 precondition check catches this and emitsnoop:true. - Routing isn't configured.
activateChannelUsageused to fail withLiveMessageSetupException/nullQueueIdat the Apex entry point. With the REST path the same guard lives invalidateChannelReadinessOnProvisioning— write withSessionHandlerId=null && FallbackQueueId=null→ 400FIELD_INTEGRITY_EXCEPTION. Runservice-de-channel-routing-configurefirst. 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-charMessagingChannel.Id(prefix0Mj). The channel must already exist with a non-nullSessionHandlerIdorFallbackQueueId.{ORG_ALIAS}— optional; thesfCLI target-org alias. Default: whateversf config get target-orgreturns. Used for OAuth and SOQL reads.{API_VERSION}— optional; REST API version. Default:68.0. Any version whereMessagingChannelUsageis 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]:
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:
Known 400 FIELD_INTEGRITY_EXCEPTION messages:
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:
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 succeeded but before the PLSQL terminal-write ran; unusual instance config with async observer execution; or a future MessageType whose dispatch behavior we haven't accounted for. If Stage 3 returned Provisioning, loop:
Max wait: 30s. If still Provisioning after the loop: emit {ok:false, kind:"timeout", mcuId, lastStatus:"Provisioning", elapsedS:30}. Verified on WhatsApp/wadtesting 2026-04-30: this loop should never actually iterate.
Stage 4: Verify MessagingChannel.IsActive
The observer flips IsActive inside the same callback as the status terminal write. In principle this is set by the time the PATCH returns. Confirm:
If IsActive === true: compute durationMs = Date.now() - T0 and emit success. If IsActive !== true despite MCU Active: the IsActive sync pass skipped (e.g. isTransitioningStatus was true when the observer ran, which shouldn't happen post-terminal). Emit:
Stage 5: Report to caller
Build the success envelope:
If this skill is the leaf (user invoked it directly), render:
Success — Activated — MessagingChannel {CHANNEL_ID} ({messageType}) is now live. MCU DeploymentStatus=Active, IsActive=true. (~{durationMs/1000}s)Info: Already active — MessagingChannel {CHANNEL_ID} is IsActive=true. No changes.(no-op path)Error: Routing not configured — run 'service-de-channel-routing-configure' skill first.(no-routing)Error: No MessagingChannelUsage row — run the insertion skill first.(no-mcu)Error: Readiness check failed: {message} — if it mentions consent/keywords, run 'service-de-channel-settings-configure' first.(readiness-failed)Error: Activation failed: {errorReason} — {errorDetails}(provisioning-error)Error: WhatsApp phone number verification failed: {hint}(verification-failed)Error: Could not request verification code: {message}(verification-request-failed)Timeout: Activation timed out after {elapsedS}s with MCU.DeploymentStatus={lastStatus}. Defensive-poll limit hit; this is unusual. Check MCU {mcuId} in Setup.(timeout)
Worked examples
For end-to-end activation traces (WhatsApp happy-path, Apple activation, a readiness-validator failure, and the already-active no-op), see references/worked-examples.md.
Gotchas
Eleven known gotchas — synchronous PATCH timing for WhatsApp, valid API status transitions, the IsActive sync pass, relationship-name variance across orgs, consent preconditions, DeploymentStatus picklist casing, the REST-vs-Apex equivalence, ErrorReason values, VERIFICATION_REQUIRED handling, OAuth token extraction, and the Status-code-409 Admin API conflict. Before troubleshooting an unexpected result or modifying this skill, load references/gotchas.md and follow it.


