Skip to content
Forward Deployed

Forward Deployed
Lessons and contents
Multi-Agent ArchitecturesLesson 6 of 7
7 lessons
Learn

Handoffs and swarms

Source
From the RAG & Agents Engineering course
By
Sourav Banerjee
Last updated
9 Nov 2026

Report an issue

Handoffs

What you'll learn

  • Distinguish a handoff from a router by who holds control after an agent finishes.
  • Explain the handoff tool: a tool call that returns a command for the parent graph.
  • Trace one Larkspur thread across turns with the active agent saved by a checkpointer.
  • Compare the calls a swarm and a supervisor spend as follow-ups pile up.

Lesson 4 of T11.1's supervisor decides every step and takes control back each time. On a thread where the customer keeps talking to billing, it still pays a routing call before and after every answer.

A Larkspur customer asks about an outage and credits, then follows up with Which invoice will the credit be on? That second message belongs to billing, and billing already has the context. A supervisor routes it again from scratch. A handoff lets the agent holding the conversation pass it on itself, and lets the next message go straight to whoever holds it now.

A router and a handoff both move a request between agents. They differ in who holds control afterwards:

Router or supervisor Handoff
Who picks the next agent the router, after every step the agent holding the conversation, by calling a tool
Where control goes after an agent back to the router, on a static edge nowhere: the agent finishes the turn or hands off
The next user message starts at the router the active agent, read from active_agent in state
What stops a loop the router's own cap nothing built in; only recursion_limit

Table 1: Router against handoff.

A structured-output classifier that sends each request to one specialist is still a router. A handoff needs three things: the agent itself chooses (a tool call), the parent graph moves control, and the choice is remembered.

The handoff tool

A handoff tool is an ordinary tool that returns a Command instead of a string. It runs inside the calling agent's own graph, but the decision belongs to the parent graph that holds all the agents. The Command is addressed there. The handoff notebook calls one directly, with no model, for a customer charged twice. We ran that cell:

Field Value returned What it does
goto billing names the parent node to run next
graph __parent__ (Command.PARENT) applies the command in the closest parent graph, which ends the calling agent's run
update.messages the conversation plus a ToolMessage the ToolMessage answers call call_demo and carries the note "Customer was charged twice for this month's subscription."
update.active_agent billing who answers the next message on this thread

Table 2: The Command a hand-built handoff tool returns (the notebook's direct-call cell).

InjectedState gives the tool the agent's messages and InjectedToolCallId gives it the id of the call it answers. The model fills in one argument only: context, a note for the next agent so the customer is not asked again. The parent's state is MessagesState plus active_agent. Its only routing of its own is at START: read active_agent and send the new message there, or to triage on a new thread.

Following the active agent across turns

With a checkpointer, active_agent outlives the turn. Here is one Larkspur thread through triage and the specialists:

One Larkspur thread with handoffs Turn 1 reaches triage, which hands off to operations, which hands off to billing. Turn 2 goes straight to billing, the active agent, and triage does not run. Turn 3 also starts at billing, which hands off sideways to operations. Customer Triage Operations Billing Turn 1: credits? to operations to billing Turn 2: which invoice? Turn 3: root cause? to operations
  1. A new thread has no active agent, so “My server went down last night. Do I get service credits?” goes to triage. The marker shows who holds the conversation.
  2. Triage calls transfer_to_operations. The tool’s Command runs operations next and sets active_agent.
  3. Operations answers the outage part and calls transfer_to_billing. Billing answers the credits part and keeps the conversation.
  4. “Which invoice will the credit be on?” goes straight to billing, read from the saved thread. Triage does not run.
  5. “When is the root-cause report out?” also starts at billing, which has no incident section.
  6. Billing hands off to operations, which answers. No coordinator was involved, and active_agent is now operations.

Figure 1: One thread, three turns. Each handoff is one tool call that rewrites active_agent.

Turn 2 is the point of the checkpointer: triage does not run, because START read active_agent = billing from the saved thread. Turn 3 moves sideways, billing to operations, with no coordinator involved. Billing has no incident section and has to recognise that and hand off.

The notebook proves the mechanism with a scripted model, LangChain's GenericFakeChatModel replaying fixed replies, tool calls included. We ran it:

Turn Customer Agents that acted active_agent after
1 "I was charged twice." triage, then billing (via transfer_to_billing) billing
2 "Will I get a confirmation?" billing only billing

Table 3: Two turns on one thread with a scripted model and no API call.

Planted failure: the handoff ping-pong

Nothing in this graph forces a stop. If agents keep deciding someone else should answer, they pass the request around until recursion_limit raises GraphRecursionError. Each agent run is one super-step of the parent graph. The notebook scripts that handoff ping-pong (triage to support to billing to triage, forever) and caps it:

Setting Result
recursion_limit = 10 (the demo cap) 10 handoffs, then GraphRecursionError
recursion_limit = 25 (every real run in the notebook) would end the same way, after 25 super-steps
No limit passed LangGraph's default of 10,007 super-steps, each one a model call in a live run

Table 4: The scripted handoff loop under each limit.

The fix for a real ping-pong is ownership: one agent's prompt and tool descriptions must claim the topic. The limit only makes the failure fast and visible.

Swarm

A swarm is the same mechanism packaged by the langgraph-swarm library. create_handoff_tool builds the tool, and create_swarm builds the parent graph with one node per agent and the START router. SwarmState is MessagesState plus active_agent, and default_active_agent names the agent a new thread starts at. Compile with a checkpointer and the swarm remembers, per thread_id, which agent holds each conversation.

Diagram: START; active_agent saved?; default agent (general QA); that agent; peer agentDiagram: START; active_agent saved?; default agent (general QA); that agent; peer agent

Figure 2: The swarm's only routing of its own. Every other move is a handoff tool call.

The swarm notebook builds three agents for a music store: a general QA agent that does RAG over company documents, an NL2SQL agent over the Chinook catalogue, and an invoice agent. Each gets its own tools plus handoff tools to the other two. Two details carry over to any swarm.

The library tool carries no note. It takes no arguments. Called directly, it returns the same goto, graph and update keys as the hand-built tool, and its ToolMessage says only "Successfully transferred to billing". Whatever the next agent needs must already be in the conversation. The hand-built tool's context argument exists to fix that.

Tools that run model-written queries must be safe whatever the model writes. The NL2SQL connection reads only, three times over:

Planted query Result Guard that refuses it
DELETE FROM Invoice not authorised PRAGMA query_only = ON and the authorizer
ATTACH DATABASE ... not authorised the authorizer (read-only flags alone allow ATTACH)
PRAGMA query_only = OFF not authorised the authorizer

Table 5: Writes and escapes sent through the query tool. The notebook checks all three on Chinook, and we reproduced the same authorizer on an in-memory database.

The file itself is opened read-only too. The invoice tools go further: they run fixed queries with the customer ID bound as a parameter, never pasted into the SQL.

What a swarm saves

A swarm spends routing once per thread. On the outage request, triage hands off to operations, operations answers its part and hands off to billing: 3 model calls, one of them routing, against 5 for a supervisor. After that, each follow-up goes straight to the active agent for 1 call. The routing cost lab runs the same three Larkspur requests through all four architectures, with the follow-up turns on a slider.

Interactive lab: Who routes, and what it costs. Open full screen (opens in a new tab)

The lab's totals for the outage request as follow-ups pile up:

Architecture Request only Plus 1 follow-up Plus 3 follow-ups Reaches billing
Router (T9.3) 2 4 8 ✗
Supervisor 5 8 14 ✓
Hierarchy 7 12 22 ✓
Swarm 3 4 6 ✓

Table 6: Model calls on the outage thread, from the routing cost lab. Each follow-up stays with billing.

The swarm's saving rests on the conversation staying with one specialist. When a follow-up changes topic, the active agent has to recognise that and hand off, and routing quality now lives in every agent's prompt instead of one supervisor's. Choose the swarm when threads stay on one specialist's topic, and the supervisor when you need one decision log.

Run the notebooks

The code for this lesson is in these notebooks. Colab runs one in your browser: install the packages its first cell imports and set the API keys it reads. To run every notebook as written, clone the repository and use its uv environment.

Notebook GitHub Colab
Agent handoffs Open on GitHub Open in Colab
Multi agent swarm Open on GitHub Open in Colab

Key Takeaways

  • A handoff tool returns Command(goto=..., graph=Command.PARENT) and sets active_agent. With a checkpointer, the next user message goes straight to the agent holding the conversation, with no router in between.
  • Nothing in a swarm forces a stop, so agents can bounce a request until recursion_limit raises. Pass a note with each handoff, because the library tool carries none.
  • A swarm spends routing once per thread: 6 calls for the outage request and 3 follow-ups, against 14 for a supervisor. The saving holds only while follow-ups stay on one specialist's topic.

Check yourself

1. A structured-output classifier sends each Larkspur request to one specialist. Is that a handoff, and what would make it one?

No, it is still a router. A handoff needs the agent holding the conversation to choose the next agent by calling a tool, and the graph to remember the choice in active_agent. The handoff tool returns a Command with graph=Command.PARENT, so the parent graph runs the named agent next.

2. In the routing cost lab, how many model calls do the swarm, the supervisor and the hierarchy spend on the outage request plus 3 follow-ups?

6 for the swarm, 14 for the supervisor and 22 for the hierarchy. The swarm spends 3 calls on the first request and 1 per follow-up, because each follow-up goes straight to the active agent. The supervisor pays 3 per follow-up and the hierarchy 5.

3. In a langgraph-swarm build, the billing agent asks the customer again for details they already gave triage. Why?

The library's handoff tool takes no arguments and its ToolMessage says only "Successfully transferred", so it carries no note. Whatever the next agent needs must already be in the conversation. The hand-built tool's context argument exists to fix that.

Rate your confidence and keep notes on the module page.