config.handoffs is where an agent’s outgoing edges live. Each entry becomes
one tool the model can call, and the model chooses between them from the
descriptions you write. A handoff moves the conversation to another agent;
handing the caller to a person is a transfer, which is a
different operation with different rules.
The fields
handoffs is available on every channel — voice, video and text.
description is the router
The compiler turns each entry into a tool named handoff_to_<name>, and the
entry’s description is that tool’s description. It is the only thing the model
has to go on.
Write it as what belongs there, not as an instruction:
The generated tool takes no arguments — except under
context: "summary",
where it takes exactly one, the summary the model writes as it hands over.
The tool name
The name is the destination’sname, lowercased, with every run of
non-alphanumeric characters replaced by an underscore: Billing becomes
handoff_to_billing, Tier 2 support becomes handoff_to_tier_2_support.
It is derived from the destination’s name and not from the target agent’s,
deliberately: renaming an agent in the dashboard must not change the tool name a
published prompt was written against. The dashboard shows the resulting tool
name under each destination as you type.
Three name rules are enforced at save or publish:
- Two destinations on one agent cannot share a
name. - A
namewith no letters or digits in it has nothing to slug into a tool name and is refused. - The generated name shares one namespace with attached tools, hook tools and the model’s own built-in tools. A collision is an error, not a coin flip:
Naming the target
Withagent_id, the target is a stored agent and the call enters it at
whatever version is published at that moment, with the tools that version
pinned. This is different from config.tools, which pins at the source agent’s
publish: republish the target and the very next handoff lands on the new
version, with nothing to republish on the source. That is usually what you want,
and it means a target you break is broken for every agent pointing at it.
The target must have a published version. Publish refuses the edge otherwise:
agent_id, name resolves against the team defined on the
call. A stored agent’s team is unknowable at
publish — it depends on which call the agent ends up running on — so this
publishes with a warning rather than an error:
message — what the caller hears at the boundary
The line 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 and
voice, and the caller should hear “connecting you to billing” during that rather
than after it. It always finishes playing before the target’s on_enter hook
runs.
- Voice and video, cascade pipeline: spoken verbatim.
- Voice and video, realtime pipeline: a speech-to-speech model has no text-to-speech to script, so it is asked to say the line and may reword, reorder or trim it. Do not put an exact form of words there on a realtime agent.
- Text: sent as a message from the agent before the target replies.
message, description and summary_prompt are all personalized at build time
with {{userdata.…}}, {{system_vars.…}} and {{vars.…}}, exactly as the
prompt and greeting are. A token whose root is none of those — a bare
{{name}}, a typo like {{userdate.name}} — is a save error, not text.
What the target does on arrival
In order:- Its own
on_enterhook runs, to completion. The target’s hook, not the source’s — see lifecycle hooks. Its latency lands before the target’s first word, so a hook that fetches something slow should open with asay. - It speaks its own greeting, if it has one, with
{{userdata.…}}resolved against the live session bag the hook just wrote to. - If it has no greeting, it opens with a generated line instead — the
caller has just asked for something and is waiting on an answer. What the
target is told depends on what it actually received: under
transcriptit is told it can see everything and to address the most recent message; undersummary, that it has a summary plus the last few messages; undernonewith no tail, that it has not been told what came before and should introduce itself briefly and find out what the caller needs.
conversation.context is not consulted. That field decides
what a new call starts from (conversation memory);
what a handoff target starts from is the edge’s context.
What validation enforces across a team
Every edge is checked at publish, and again at runtime before the switch, because a stored target can be republished in between. These are errors:
The expressive rule is conditioned on the source, which makes it transitive: a
legal target is itself expressive, so its own outgoing edges get the same check.
One expressive agent puts its whole downstream graph on one voice.
One check is a warning, not an error:
validate before publishing to see all of this at once:
POST /v1/agents/{agent_id}/validate.
Handing back, and loops
A target hands back the same way it hands anywhere: with its ownhandoffs
entry pointing at the agent it came from, by agent_id or — on a team — by
name. There is nothing special about the return edge. Give the specialist a
destination named for the front desk with a description like “anything that is
not a billing question”, and set its context to none with a small
recent_turns so the front desk does not re-read the whole billing detour on
every turn.
Nothing prevents a routing cycle, and no single edge can detect one. What stops
it is a per-call cap: 25 handoffs on one call. The next attempt fails and
the model is handed a sentence to relay to the caller:
handoffs edges and
handoff operations. If you are anywhere near it,
the routing descriptions are overlapping and two agents are passing the caller
back and forth.
On text, the agent that is speaking belongs to the live conversation window. A
window closes after about a minute idle, on
end_call, or when you close it —
and the next message starts again from the agent that opened the thread, not the
one that was last handed the conversation. See text
conversations.A worked example
A front desk that routes to two specialists. The write takes the whole config, so read the current one, add the edges, then validate and publish.Next
Context policies
transcript, summary and none — what the target starts from.Patterns
Front desk to specialists, handing back, and what a hop costs.
Conditional handoffs
Routing decided by a tool tree rather than by the model.