--- name: routing-inbound-conversations-to-asa description: Build or fix an inbound Omni-Channel RoutingFlow that hands a live VoiceCall or MessagingSession to an Agentforce Service Agent (ASA / copilot). Covers the routingType=Copilot pattern, the MANDATORY fallback queue, dual-channel Contact linking, and the voice-vs-messaging differences. Use whenever a call/chat needs to be routed TO an Agentforce agent (not escalated to a human — that is the replicating-omnichannel-routing-flows skill). --- # Routing inbound conversations to an Agentforce Service Agent (ASA) ## What this skill is for An **inbound RoutingFlow** is what Omni-Channel runs when a new VoiceCall or MessagingSession arrives, to decide **who** picks it up. To route the conversation to an **Agentforce Service Agent** (ASA — the bot/copilot answers first, before any human), the flow's `routeWork` action uses **`routingType=Copilot`** and points at the agent's `BotDefinition` — **plus a REQUIRED fallback queue**. This is the mirror image of the `replicating-omnichannel-routing-flows` skill: - **That skill** = *escalation* (agent/bot → human **queue**), `routingType=QueueBased`, invoked via `@utils.escalate`. - **This skill** = *inbound* (channel → **ASA agent**), `routingType=Copilot`, invoked by the channel's inbound-flow assignment. Two flows, one per channel: a **Voice** variant (VoiceCall, `sfdc_phone`, links `VoiceCall.Contact__c`) and a **Messaging** variant (MessagingSession, `sfdc_livemessage`, links `MessagingEndUser.ContactId`). Ready-to-edit, verified templates ship in `assets/`: - `assets/inbound-asa-voice.flow-meta.xml` - `assets/inbound-asa-messaging.flow-meta.xml` Both are ground-truthed from the live, Active CitiBank flows `CitiBank_ASA_Inbound_Voice` / `CitiBank_ASA_Inbound_Messaging`. --- ## THE headline lesson: `routeWork` to a copilot still REQUIRES a fallback queue The single most common failure: authoring a `routingType=Copilot` `routeWork` with only the copilot target and **no queue**. It looks logically complete — you're routing to the agent, why name a queue? — but the platform **requires a fallback `queueId` + `queueLabel` anyway**. Omni needs a queue to fall back to when the agent can't take the work (capacity, offline, decline, timeout). Without it, routing fails. So a copilot `routeWork` carries **both**: | Purpose | Params | |---|---| | Primary target (the ASA agent) | `routingType=Copilot`, `copilotId`, `copilotLabel` | | **Required** fallback | `queueId` (a real `00G…` Group.Id), `queueLabel` | | Channel | `serviceChannelDevName`, `serviceChannelId`, `serviceChannelLabel` | | Work record | `recordId` | If you copy an escalation flow and just "swap QueueBased for Copilot," you'll drop the queue and it breaks. Keep the queue. --- ## The `routeWork` action — copilot routing, per channel ### Voice (`sfdc_phone`) ```xml recordIdrecordId serviceChannelLabelPhone serviceChannelDevNamesfdc_phone routingTypeCopilot copilotLabelCitiBank ASA serviceChannelId sfdc_phoneServiceChannel copilotId CitiBank_ASABotDefinition queueLabelGeneral Voice queueId00GKc000000wPROMA2 ``` Notable: on the **voice** flow the agent is expressed as a **`setupReference` to `BotDefinition`** (the agent's DeveloperName, e.g. `CitiBank_ASA`) and the channel as a `setupReference` to `ServiceChannel`. `versionString` = `2.0.0` on this action. ### Messaging (`sfdc_livemessage`) ```xml recordIdrecordId serviceChannelId0N9Kc000000h7oJKAQ serviceChannelLabelLiveMessage serviceChannelDevNamesfdc_livemessage routingTypeCopilot queueId00GKc000000wPRNMA2 queueLabelMessaging copilotId0XxKc000000UW41KAG copilotLabelCitiBank ASA ``` Notable difference: on the **messaging** flow `copilotId` and `serviceChannelId` are **raw Id strings** (`0Xx…` copilot config Id, `0N9…` channel Id), NOT `setupReference`s. (`0Xx` is the messaging agent-config record, not the `BotDefinition` `0Xx`→ query it, see below.) No `versionString` on this action. > Both patterns deploy and run. The voice `setupReference` form is more portable (resolves by dev name); the messaging raw-Id form is what Flow Builder writes for messaging. Either works as long as the Ids/dev names resolve in the target org. --- ## Full flow structure (both channels) ``` Start → resolve the phone to search on Voice: Read_Voice_Call → decide (phone input? else VoiceCall.FromPhoneNumber) Messaging: Load_Messaging_User (MessagingEndUser by input_record.MessagingEndUserId) → Load_Contact_by_Phone_Number actionType=findMatchingIndividuals, searchObject=Contact, searchFields=Phone → Loop_Search_Results over .contactIds → Load_Found_Contact (Contact by Id) → contact → Check_Found_Contact (contact != null?) Found: link the Contact Voice: recordUpdate VoiceCall.Contact__c = contact.Id (+ optional Customer_Demo__c stamp) Messaging: assign MessagingEndUser.ContactId = contact.Id → Update_Messaging_User → stamp session No: just stamp the conversation record (no contact link) → Route_to_ASA routeWork, routingType=Copilot, copilot target + REQUIRED fallback queue ``` - **Contact resolution is the dual-channel crux** (same as everywhere): voice links the found Contact onto the custom **`VoiceCall.Contact__c`** lookup; messaging writes **`MessagingEndUser.ContactId`**, which surfaces downstream as **`MessagingSession.EndUserContactId`**. (See the `messaging-inbound-omniflow-pattern` and `linking-individuals-by-phone-in-flows` memories/skills.) - **`findMatchingIndividuals`** is the standard phone→Contact matcher; loop its `.contactIds` and load the first. - The **messaging** flow's input variable is **`input_record` (SObject MessagingSession)** in addition to `recordId` (String) — it reads `input_record.MessagingEndUserId` to find the MessagingEndUser. The **voice** flow only needs `recordId` + a `phone` input. --- ## Build / fix procedure 1. **Find the ASA agent + its ids.** ```bash # BotDefinition dev name (voice setupReference form): sf data query --target-org --json -q "SELECT Id, DeveloperName, MasterLabel FROM BotDefinition WHERE DeveloperName=''" ``` For the **messaging** `copilotId` raw string, the value is the agent's messaging routing-config Id (`0Xx…`). The reliable way to get the exact ids/labels is to **read an already-working inbound flow in the org** (retrieve it) — or copy them from the voice flow's `BotDefinition` and let Flow Builder resolve. If unsure, author voice with the `setupReference`/`BotDefinition` form (portable) and mirror it for messaging. 2. **Find a fallback queue (REQUIRED).** ```bash sf data query --target-org --json -q "SELECT Id, DeveloperName, Name FROM Group WHERE Type='Queue'" ``` Pick the voice queue for the voice flow, the messaging queue for the messaging flow. Put its `00G…` Id in `queueId` and its label in `queueLabel`. **Do not omit this.** 3. **Confirm the messaging ServiceChannel Id** (messaging flow only): ```bash sf data query --target-org --json -q "SELECT Id, DeveloperName FROM ServiceChannel WHERE DeveloperName='sfdc_livemessage'" ``` 4. **Copy the matching asset** into `flows/`, rename to your flow's API name, and swap the `<<...>>` tokens. Keep `routingType=Copilot`, `serviceChannelDevName`, and BOTH the copilot target and the fallback queue. 5. **Deploy Active:** ```bash sf project deploy start --metadata Flow: --target-org --json ``` 6. **Wire the flow to the channel (Setup, not metadata for the assignment itself).** - **Voice:** the SCV/Omni voice channel's **inbound flow** is set in Setup (the voice channel → flow assignment). This is the flow that runs when a call lands. - **Messaging:** the messaging channel (Embedded / MIAW / etc.) names its **routing/inbound flow** in the channel config. For metadata-managed channels the flow is referenced in the channel definition; otherwise set it in Setup. In practice on this stack, the inbound-flow assignment per channel is **Setup-side** — deploying the flow Active does not by itself bind it; point the channel at it. 7. **Verify:** ```bash sf data query --target-org --json -q "SELECT ApiName, ProcessType, IsActive FROM FlowDefinitionView WHERE ApiName=''" ``` `ProcessType=RoutingFlow`, `IsActive=true`. Then place a test call / start a test chat and confirm it lands on the ASA agent, and that `Contact__c` / `EndUserContactId` got linked. --- ## Gotchas - **Fallback queue is mandatory for `routingType=Copilot`.** #1 mistake. The agent is the primary target; the queue is the required fallback. Keep both. - **Voice vs. messaging express the agent differently.** Voice = `copilotId` as `setupReference`/`BotDefinition`; messaging = `copilotId` as a raw `0Xx…` string. Don't force one form onto the other channel unless you've confirmed the id resolves. - **`serviceChannelId`**: voice uses a `setupReference` to `ServiceChannel` (`sfdc_phone`); messaging uses a raw `0N9…` channel Id string. Both alongside `serviceChannelDevName`. - **Messaging needs `input_record` (MessagingSession SObject) input**, not just `recordId` — it reads `MessagingEndUserId` from it. - **Contact link target differs**: `VoiceCall.Contact__c` (custom lookup) vs `MessagingEndUser.ContactId` (→ surfaces as `MessagingSession.EndUserContactId`). - **No `triggerType` on Start** — an inbound RoutingFlow has a bare Start with just a connector, same as escalation flows. - **`FlowDefinitionView` is a regular object** — query it WITHOUT `--use-tooling-api` (tooling API rejects it with `INVALID_TYPE`). ## Relationship to other skills - **Escalation (agent → human queue):** `replicating-omnichannel-routing-flows` (`routingType=QueueBased`, `@utils.escalate`). - **Phone→Contact matching detail:** `linking-individuals-by-phone-in-flows`. - **Messaging EndUser→Contact link + channel wiring:** the `messaging-inbound-omniflow-pattern` memory.