You will build four agents on one phone number: a front desk that greets the caller, works out what they need in a question or two and hands them on, plus three specialists that each hold one job, one prompt and one set of tools. You will also add one route the model does not decide — a lookup that sends a caller with an open ticket straight to technical support. The example is Northwind Tools. Every hop here is a handoff — agent to agent, on the same call. Handing the caller to a person is a transfer, and the two are never interchangeable.

Should you split at all?

Splitting is not free. Every hop pauses the call while the target is built, and the target then pays for whatever context crossed on every turn it takes, on your own provider key. One agent has neither cost. Three desks is about the point where it pays. Below that, the descriptions overlap and the caller gets passed around.

What you need

1

Build and publish the three specialists

Publish these first. A handoff edge whose target has no published version is refused when you publish the front desk, so the graph is built leaves-first.Each specialist gets a short prompt covering one desk, and — this is the part people leave out — a line telling it not to re-litigate which desk the caller wants. It has already been chosen. A specialist that re-routes sends the caller round the loop.

Billing

Its invoice lookup is defined inline. On an agent that is a shorthand: the write creates the tool, publishes it at v1, and stores its id in place, so an agent and its tools arrive in one request. It is not a second kind of tool — read the config back and you will find a tool_id.Save the prompt above as billing-prompt.txt, then:

Technical support

Same shape, with a knowledge base instead of a tool, and a prompt that says when to consult it and what to say while it fetches.

Sales

Sales does not close on the call — it qualifies and puts the caller through to a person, so its one tool is an argument-free transfer with the escalation policy in the description.
That transfer is warm: the caller waits on hold music while the agent briefs the account executive on a separate line, and only then puts the two together. It costs a second AI conversation on your own keys, and the person answering can decline — in which case the caller comes back to the sales agent. The say in front of it is worded for warm; before a cold transfer the same line would be a lie. See transfer.Publish all three before going on.
2

Build the front desk

The front desk carries one handoffs entry per desk. Each entry becomes one tool the model can call, named handoff_to_<name>, and the model routes on that entry’s description and nothing else.Write each description as what belongs there, never as an instruction to the model:Descriptions that do not overlap are most of the work. If billing says “payments” and sales says “orders”, a caller asking about a payment for an order they have not placed is a coin flip. Put the ambiguous words in exactly one description, deliberately.The front desk’s own prompt is short, and most of it is about what it does not answer. An agent with three handoff tools and a prompt inviting it to be helpful will answer the billing question badly instead of routing it.
Save it as front-desk-prompt.txt, then create it.
The tools entry below names route_by_open_ticket, which step 4 builds. Attaching an unpublished tool is rejected at save, so if you are working straight through, either build and publish that tool first or leave tools out of this write and add it when you get there.

Why each edge carries what it does

context decides what the target starts from, and it is the field that costs the most money on a team. Justify every edge rather than taking the default three times.Two rules the API enforces rather than ignores: recent_turns is refused under transcript, which already carries every turn, and summary_prompt is refused outside summary. Under summary, the source agent writes the summary itself as an argument on the handoff_to_* tool — there is no second model call, but the caller waits through 60 to 100 tokens before the handover happens. Full rules in context policies.
summary is not a privacy boundary. It narrows what the target model is shown and guarantees nothing: the tail always crosses, userdata crosses regardless, the full transcript is still recorded, and a later transcript hop shows that agent the whole call including the part summary scoped away. If a fact must not reach the next agent, do not let the first agent hold it.

The handover message

message is queued before the target is built, which is the whole point of it: building the target loads its definition and compiles its prompt, tools, knowledge and voice, and the caller should hear “connecting you to billing” during that pause rather than after it. It always finishes playing before the target’s own greeting.On a cascade agent it is spoken verbatim. Leave it out and the handover is silent, which is fine when the target has a greeting of its own — but then the caller hears two seconds of nothing followed by a new voice, so say something.
3

Let a specialist hand back

A specialist that discovers it has the wrong caller hands back. There is nothing special about a return edge — it is an ordinary handoffs entry on the specialist, pointing at the front desk:
A write takes the whole config, so read the current one, add the field and send it back — omitting tools here would silently detach billing’s invoice lookup.Give technical support the same edge, plus one to billing with summary and recent_turns: 3 for the long-call hop in the table above. Nothing prevents a routing cycle and no single edge can detect one; what stops it is a per-call cap of 25 handoffs, after which the model is handed a line to relay: “I’m sorry, I can’t pass you to another assistant again on this call.” If you are anywhere near that cap, two descriptions overlap.
4

Route on a lookup instead of on the model

Whether a caller has an open ticket is a fact about their account, not a judgement about what they said. Facts belong in a tool tree. The handoff operation is the same move as an edge on the agent, except the tree decides.This tool assumes your endpoint answers 200 with all three keys on every response — open_ticket_id and open_ticket_summary as empty strings when there is nothing open. That matters: publish_fields fails the operation when a path is missing or null, so an endpoint that omits a key on the quiet path breaks the tool on exactly the calls it is meant to pass through.
Four things that tree is doing on purpose:
  • on_error: "continue" on the lookup is what stops a flaky endpoint taking the front desk down with it. A failed operation publishes nothing, so {{tooldata.ticket_id}} resolves to empty, the if takes its else, and the caller gets an ordinary greeting instead of an apology.
  • context: "summary" here takes a value, not a request. The tree runs after the model’s tool call, so there is no argument being written at that moment to take a summary from. Point it at something real — a template over tooldata, as above, or fixed prose. A summary under any other context is an error.
  • The if is terminal and branches do not rejoin. Nothing may follow it in the same chain, so anything both paths need goes before it or into both branches.
  • The else speaks. No operation in this tree can return a response, so the tool is silent by derivation — without the generate_reply the agent would fall quiet after the lookup. Adding a generate_reply that states what to say is the right way to break that silence; there is no flag to untick.
POST it to /v1/tools, validate it — target_agent_id must already be published, which is why the specialists came first — and publish it before attaching it to the front desk. The examples in step 2 call the created tool route. Then publish the front desk.
5

Keep the team sounding like one company, then go live

Every agent above shares language, stt, tts and its voice. That is not laziness — it is the difference between a caller being passed to a colleague and a caller wondering whether the line dropped.Validate every agent, then publish, then put the front desk on the number. The agent assigned to a number is the one that answers — the specialists are reached only through handoffs and are never assigned to anything.
A handoff edge names a stored agent and enters it at whatever version is published at that moment — unlike config.tools, which pins at the source agent’s publish. So republishing a specialist reaches the very next handoff with nothing to republish on the front desk, and a specialist you break is broken for every agent pointing at it.
Post-call analysis runs on the agent that answered — the front desk — using its spec and its own language model, over the whole call including every desk. That is why the analysis block sits on the front desk and not on the specialists.

Test it

Test one hop at a time, and test the hop, not the desk. Run the routing tool on its own. POST /v1/tools/{tool_id}/run executes the draft for real against your endpoint.
That proves the URL, the secret and the publish paths. It will always take the else branch, because {{system_vars.human_phone_number}} is empty outside a phone call — the same is true on a web call and in the dashboard test panel. Prove the then branch by ringing the number from a phone that has an open ticket. Walk each hop in the browser. The agent editor’s test panel runs the latest published version, so republish after every edit. Say one sentence per desk and check it lands: “my card was charged twice”, “the drill won’t charge”, “I want to kit out a new site”. Then say something ambiguous on purpose — “I’ve got a problem with an order” — and see which way it goes. That sentence is the one your descriptions have to settle.
The sales desk’s transfer cannot be exercised from a web call: transfer is phone-only and fails on a web session with “transfer is only available on phone calls”, after which the agent apologises and carries on. Test it by ringing the number.
Ring the number and read the call back. GET /v1/calls/{session_id} returns the whole thing:

What to watch in the first week

Which agent said each line. Every transcript item stamps its own agent_id and agent_version as it was produced. The session names only the agent that answered and never moves, so on a call with a handoff these stamps are the only way to tell who said what. GET /v1/conversations/{conversation_id}/sessions is deliberately the same: it names the agent that picked up, not the one that did the work. See conversations. Where the handovers are, and what crossed. The transcript carries an agent_handoff item at each boundary. Beside it, events carries an agent.handoff entry with the policy, the resolved recent_turns, tail_items — how many messages the tail actually came to, usually more than recent_turns because a turn can hold several — the summary itself, summary_fallback, and via, which is handoffs for an edge on the agent and operation for the conditional route above. Telling a mis-route from a bad prompt. These look identical in a summary and are completely different problems, and the transcript separates them in one glance: The reroutes field. Any call above 1 is worth opening. Read those before you read the failures — a caller who reached the right desk on the third try counts as a success and is still a bad call.

Where this falls short

Routing quality is a description-writing problem, and it is iterative. There is no model, threshold or confidence score to turn up. The model reads three descriptions and picks one, so the only lever is the wording — and the loop is: read the transcripts where it chose wrong, find the caller’s exact phrase, and put that phrase in one description. Change one description at a time; changing three at once means you cannot tell which fixed it. A description that has grown into three sentences of instructions is a sign the split itself is wrong. Every hop costs latency, and it stacks. At the boundary the platform loads the target’s published definition, its pinned tools, its knowledge and its hooks, and builds its models — that pause is what the message exists to cover. Under summary there is a second pause first, while the source writes the summary. Then the target’s on_enter hook runs to completion before it says a word. Two hops on a short call are unnoticeable; three hops with summary on each is a call that feels slow, and the caller has no idea why. Watch snapshot.response_latency.p95_ms on multi-agent calls specifically. There is no way to test a routing change before it faces real callers. No staging environment, no simulated caller, no test suite that replays past calls against a new set of descriptions — none of that exists here. The closest thing is real and worth using: place a call against the draft rather than the published version by passing agent_version: "draft" on a call token or an outbound dial, so you can hear an edit without publishing it over what live callers are getting. Beyond that, change one description, publish, and read the next twenty calls. Two smaller limits. summary is not a privacy boundary — see the warning above — so a team is not a way to keep one desk from seeing another’s conversation. And on text the agent that is speaking belongs to the live conversation window: a window closes after about a minute idle, and the next message starts again from the agent that opened the thread rather than the one last handed the conversation, so a text triage team forgets where it routed the person. See text conversations.

Next

Team patterns

The same four designs as reference, with the constraints that shape them.

Context policies

What each hop carries, what it costs, and what crossed on a real call.

Conditional handoffs

The handoff operation in full, and when the tree should decide.

Inbound support line

The single-agent version, with knowledge, a lookup and a number.