You shipped one Agentforce agent. It started with six topics, it now carries twenty-two, two of them answer the same question differently depending on what the customer said first, and every instruction you add to fix one flow quietly degrades another. The decision in front of you is whether to split it into a Super Agent with Connected Subagents — and if you do, how routing actually behaves once those pieces are separate agents in the org rather than subagent blocks in one script. It does not behave the way single-agent tutorials imply.
The ceiling is instruction interference, not topic count
The Salesforce Admins blog names the failure mode plainly: asking one agent to do more makes it harder to "write clean instructions, test predictable behavior, and maintain it over time." The mechanism shows up in the flow documentation. When Agentforce transitions to a subagent, it discards the previous subagent's prompt instructions and processes the new subagent's instructions from the top, so the final prompt contains only the second subagent's instructions. Isolation is the whole benefit. A single agent with twenty-two topics has none — every instruction sits in every prompt, competing for attention on every turn.
Agentforce subagents come in two forms, and they are not interchangeable:
- Several
subagentblocks in one script. You get instruction isolation immediately, with one deployable artifact and one test suite. - Connected Subagents. Separate agents in the org, referenced by URI, each with its own builder, owner and release. You also get a routing layer that behaves unlike anything in a single-agent script.
I would only pay for the second when independent ownership, independent release cadence, or reuse across more than one Super Agent is a real requirement rather than an aspiration. If one team owns everything and nothing is reused, keep it in one script and split into subagents. The multi-agent version puts a distribution problem on top of a prompt problem.
start_agent is the only front door, and it is not a supervisor
Every customer utterance enters at start_agent. Exactly one is required per script, and it is what the Admins blog calls the Agent Router: the entry point for every utterance, responsible for subagent classification, filtering, routing and variable initialization. It shares all of its fields with a subagent block, and @start_agent aliases @subagent, so anything legal in a subagent is legal in the router.
What it is not is a live supervisor. Execution comes back to start_agent only after the current subagent completes and Agentforce is waiting for the next utterance. Nothing in the router observes a subagent mid-run, so any rule you need enforced during a subagent's turn has to be written inside that subagent.
That settles what belongs in the router: compliance and guardrails, user verification and context priming, disambiguation, and the decision to escalate to a human — the rules that have to hold on every interaction. Here is the router from a facilities org with a local verification subagent and two Connected Subagents, warranty claims and on-site engineer scheduling.
config:
name: "Facilities Router"
description: "Front door for asset service requests from site managers."
variables:
asset_tag: mutable string = ""
asset_id: mutable string = ""
service_state: mutable string = ""
caller_verified: mutable boolean = False
visit_window: mutable string = ""
system:
You coordinate facilities service requests. You never quote warranty
outcomes or engineer availability yourself.
start_agent Facilities_Router:
label: "Facilities Router"
description: "Classifies a facilities request and hands it to the right specialist."
reasoning:
instructions:
- Confirm the caller against the asset record before discussing service history.
- If the request covers both a warranty question and a site visit, ask which to handle first.
actions:
verify_caller: @utils.transition to @subagent.Caller_Verification
available when @variables.caller_verified == False
open_warranty_claim: @actions.go_to_Warranty_Claims_Agent
available when @variables.caller_verified == True
with [email protected]_id
set @variables.service_state = @outputs.claim_state
available when gates an action's visibility to the reasoning engine using a boolean expression, and it is valid only inside reasoning.actions. Watch the transition form: in reasoning.actions a transition is the action @utils.transition to @subagent.X; in a directive block such as after_reasoning it is the bare statement transition to @subagent.X. I have reviewed scripts that mixed the two and lost an afternoon to it, because the validation error names the block rather than the offending line.
Pick the seam: domain, stage, or capability
The primary source offers three decompositions, and they are not equally good defaults.
By functional domain mirrors how the business is organised — warranty, scheduling, procurement. This is the split I reach for first, because it lines up with who gets paged when a subagent misbehaves. Ownership boundaries that match org-chart boundaries survive contact with a release calendar.
By workflow stage structures around process phases, where each stage has distinct actions you can reuse. Use it when the same record moves through phases with genuinely different action sets — intake, triage, dispatch, closeout — and when two stages would otherwise duplicate half of each other's instructions.
By capability means one agent retrieves, one analyses, one acts. It reads well, and I would not start here. It creates the most agent-to-agent chatter per user turn, and whether Connected Subagents can run concurrently is not something any current documentation confirms — so design as if every call is sequential and budget the latency. If retrieval genuinely serves three domains, carve out the retriever alone and leave the rest split by domain.
Whichever seam you pick, apply the clarity test from the source: if you cannot state a subagent's job in one sentence, the scope is wrong.
The transition-to gotcha: only one construct reaches a Connected Subagent
This is the part that breaks working single-agent patterns. The Admins blog is direct about it: with a single agent you can use transition to in an If/Else statement to move to the subagent, and in a multi-agent orchestration "that doesn't work." The documented fix is to route by invoking the action that calls the Connected Subagent.
There are exactly two control-movement constructs, and only one of them can see a Connected Subagent.
| Construct | Reaches a Connected Subagent? | Outputs to the caller | Caller's instructions | Where it is written |
|---|---|---|---|---|
transition to @subagent.X (bare in directive blocks; @utils.transition to in reasoning.actions) |
No | None — transitions are one-way and control does not return to the previous subagent | Discarded; the final prompt holds only the target subagent's instructions | Reasoning actions, reasoning instructions, before/after reasoning blocks |
run @actions.go_to_X, the action fronting a Connected Subagent |
Yes — this is the documented route | Yes — set @variables.x = @outputs.y captures them |
Retained; the Super Agent stays the point of contact and consolidates the response | Anywhere an action call is legal, including inside an if in a directive block |
run is the keyword. The spec describes it as a statement that "invokes a named action", first-class, traced, auditable and callback-capable. There is no invoke keyword, and no construct in the spec returns control to a caller. The action route works not because it returns, but because it is an ordinary action call: its outputs land in the calling agent's variables the way every other action's do. A transition hands the conversation away and throws your instructions out with it.
The Connected Subagent block is where the contract lives:
connected_subagent Engineer_Scheduling:
target: "<filled in by Agentforce Builder when you connect the agent>"
label: "Engineer Scheduling"
description: "Books, moves and cancels on-site engineer visits for one known asset. Call when an asset needs a physical technician visit."
loading_text: "Checking engineer availability for this site."
inputs:
asset_reference: string = @variables.asset_id
requested_state: string = @variables.service_state
after_response:
if @variables.visit_window == "":
transition to @subagent.Caller_Verification
delegate_escalation: True
Two details bite here. target is a URI that Builder fills in when you connect the agent as a subagent — do not hand-author it. And inputs binds in the direction people get backwards: the left side is the name the Connected Subagent expects, the right side references the calling agent's variable. Above, asset_reference is the callee's parameter and @variables.asset_id is mine.
Closing the last 20% with deterministic logic
Description-based routing got the source's author to the right subagent about 80% of the time. That is a fine number for a demo and a bad one for a dispatch flow. The remedy given is negative guardrails that stop requests, plus deterministic logic to enforce conditions, rather than trusting reasoning alone.
In practice: when an action has already resolved the fact that decides the route, do not let a description decide it.
after_reasoning:
run @actions.Resolve_Asset_Service_State
with [email protected]_tag
set @variables.asset_id = @outputs.asset_id
set @variables.service_state = @outputs.service_state
if @variables.service_state == "Awaiting Site Visit":
if @variables.caller_verified == True:
run @actions.go_to_Engineer_Scheduling_Agent
with [email protected]_id
set @variables.visit_window = @outputs.scheduled_window
else:
transition to @subagent.Caller_Verification
Once service_state is known, scheduling is not a judgement call. Nested if blocks are how you write that chain — Agent Script has no elif or else if — and booleans must be capitalised True / False.
Sequencing matters as much as syntax. Build and test each Connected Subagent standalone until it is boring, stand up the Super Agent with the deterministic routes first, then add Connected Subagents one at a time and re-test routing after each. Adding three at once and measuring accuracy afterwards tells you the number is bad without telling you which description caused it.
What to watch for
descriptiononconnected_subagentis required by the blocks documentation even though the spec lists it as optional. Write it as a trigger condition ("call when...") rather than a capability blurb; the reasoning engine is matching intent, not reading a brochure.- Variables are global across the script and persist across transitions. A
service_stateleft over from the previous request will route the next one confidently and wrongly. Reset routing state in the router. mutablevariables require a default value.linkedvariables are read-only, supplied from external context through asource:annotation, take no default, and cannot use thelisttype.delegate_escalation: Truelets the Connected Subagent escalate to human support, and applies in handoff mode only. Decide deliberately whether escalation belongs to the specialist or the router — the primary source puts escalation decisions in the Super Agent's column.- Concurrency is unconfirmed. Design every route as sequential; if parallel invocation lands later, that assumption costs you nothing.
available whenexists only inreasoning.actions. Guarding an action in a directive block means anifon a variable, notavailable when.
Leave a Comment