Agent handoff targets
A multi-agent setup typically has one routing / triage agent and several specialised agents (billing, support, sales, returns). When the routing agent decides “this is a billing question, hand off”, you want the destination to be a small explicit set — not “any agent in this org”.handoff_targets is that allowlist.
What it gates
Settinghandoff_targets on agent A declares: “Agent A may hand off ONLY to the agents listed here.”
The allowlist is enforced in two places:
- Runtime — the
transfer_to_agenttool is always registered, so the LLM always sees it in its tool list. The allowlist is checked inside the tool handler after the model calls it: iftarget_agent_idis not on the source agent’s allowlist (or the allowlist is empty), the call returns an error result and no handoff occurs. The model receives that error and continues the turn — it does not crash the conversation. - Internal handoff API (
POST /api/v1/agents/internal/handoffs) — even with a valid internal token, a handoff is rejected with403 FORBIDDENif the target is not on the source agent’s allowlist. This protects the audit ledger and theagent.handoff_occurredwebhook from forged entries.
transfer_to_agent call the model attempts is rejected by the runtime. Set the allowlist explicitly to enable inter-agent routing.
Configure on agent create
Configure on agent update
handoff_targets is persisted under the agent’s config.handoff_targets so the runtime can read it on every executor rebuild.
Update semantics
What a runtime handoff looks like
The router agent’s LLM always hastransfer_to_agent in its tool list — the tool is registered unconditionally. Its target_agent_id parameter is a free-form string, so the model can name a target that isn’t on the allowlist; the runtime is what stops it. When the model calls transfer_to_agent, the handler validates target_agent_id against the source agent’s handoff_targets before resolving the target. If it isn’t on the list (or the list is empty), the call returns an error result and the handoff is dropped.
A successful handoff:
- Persists an
agent_handoffsrow. - Fans out a webhook event
agent.handoff_occurredto the tenant’s subscription endpoint withhandoff_id,source_agent_id,target_agent_id,conversation_id,reason, andsummary. - Re-enters the runtime with the target agent’s executor, carrying the conversation history forward.
What a refused handoff looks like
There are two refusal paths, depending on where the disallowed target originates. From the model (runtime). When the agent’s LLM callstransfer_to_agent with a target_agent_id that isn’t on the allowlist (or when the allowlist is empty), the tool handler returns an error result to the model rather than routing:
agent_handoffs row or agent.handoff_occurred webhook is produced.
From the internal API. If a caller (most commonly an internal automation invoking POST /api/v1/agents/internal/handoffs directly) tries to record a handoff to an agent that isn’t on the source agent’s list:
agents.handoff_allowlist_violation with the source / target / allowlist size, so you can monitor for abuse.
Patterns
One-way fan-out (router → specialists)
Two-way (specialists can return to router)
Mesh
Common pitfalls
- Forgetting to set
handoff_targets— the model still sees thetransfer_to_agenttool (it is always registered) and may try to use it, but every call is rejected by the runtime because the allowlist is empty. Symptom: the agent keeps trying to hand off and getsHANDOFF_TARGET_NOT_ALLOWEDback, then answers itself anyway. Set the allowlist to make the handoff actually route. - Listing an agent that doesn’t exist —
PUT /api/v1/agents/:idvalidates every supplied id against the tenant’s existing agents and rejects the whole request with422 INVALID_HANDOFF_TARGETSif any id is missing (the response message lists the offending ids). The only case in which an unknown target is a harmless runtime no-op is one that was already persisted before the agent it pointed at was deleted —transfer_to_agentthen simply can’t route to it. Note the create/update asymmetry:POST /api/v1/agentsdoes not run this existence check, so a create call can persist an unknown id that a later PUT would reject. - Listing the agent’s own id — allowed but pointless. The runtime handles
transfer_to_agentwith the same agent as a no-op. - Cross-tenant ids — every id is tenant-scoped. You cannot hand off to an agent in a different organisation.
See also
- Creating Agents — full agent-create reference.
- Cost controls —
max_cost_per_run_cents/max_cost_per_conversation_cents. - Webhooks → Events —
agent.handoff_occurredpayload.