LangGraph Multi-Agent Supervisor Tutorial: Handoffs with Command, Durable Threads, and the Loop That Eats Budget
The supervisor announces it will hand the task to the research agent, the run ends two seconds later with no research and no error, and in production the same graph picked one subagent four times in a row while the token bill quietly tripled.
💡 What You Will Learn
The supervisor announces it will hand the task to the research agent, the run ends two seconds later with no research and no error, and in production the same graph picked one subagent four times in a
📜 Table of Contents
- What the current releases actually give you
- Choose the wiring before you write code
- State: decide what crosses the boundary
- The supervisor is a routing function, not a chat partner
- Subagents as ordinary nodes
- Durable threads, and the config key everyone forgets
- The recursion limit is no longer 25
- What the loop costs, in tokens
- Test the routing without a model
- When a supervisor is the wrong answer
- FAQ
A two-agent LangGraph build that answers "I will hand this off to the research agent" and then stops is the most common failure we see in hand-rolled multi-agent systems. There is no traceback. The run takes two seconds. The final message looks like progress, so nobody files a bug. What happened is duller than it looks: the supervisor emitted a sentence instead of a routing decision, and a graph drawn as START to supervisor to END has no edge for a sentence to travel along.
The second failure costs real money: the same supervisor picks the same subagent four times, each handoff re-sends the message list, and a question that one prompt could answer eats tens of thousands of input tokens. This tutorial builds the pattern that avoids both, with every version number checked against PyPI and the wheels themselves on 2026-10-24.
What the current releases actually give you
- langgraph 1.2.16 (PyPI, requires Python 3.10+). The repository
langchain-ai/langgraphshowed 43,788 stars and a push on 2026-10-23 when we checked. - langchain 1.4.7, which is where
create_agentnow lives:from langchain.agents import create_agent. - langgraph-checkpoint-sqlite 3.1.2, exposing
SqliteSaverfor durable threads without standing up Postgres. - langgraph-supervisor 0.0.31, the convenience package. Read its README before you install it: it now states that the tools-based supervisor pattern is preferred and that the library mainly exists to keep LangChain 1.0 upgrades working.
That last line matters. The library is not broken, but building the handoff yourself costs about thirty lines and removes a dependency whose recommended status is moving the other way. Everything below is the by-hand version.
Choose the wiring before you write code
Three shapes work, and picking the wrong one is why teams spend a week on plumbing they did not need.
- One agent, many tools. A single
create_agentcall with search, calculator and database tools. Cheapest to run, easiest to debug, and usually enough. - Supervisor plus handoff tools. One agent whose tools are the other agents, so a tool call is the handoff. Good when every subagent shares one tool surface and you want the model to decide.
- StateGraph with Command. A supervisor node that returns
Command(goto=...). Explicit routing, typed state, and the only one of the three where you can unit-test the routing decision without a model in the loop.
This article uses the third. It is the shape that survives production, because when a call goes wrong you can point at the function that chose the next node.
State: decide what crosses the boundary
from typing import Annotated, Literal, TypedDict
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
class TeamState(TypedDict):
messages: Annotated[list, add_messages]
next: str
notes: list[str]
handoffs: int
add_messages is the reducer that appends instead of overwriting, so every node can return a partial update. handoffs is not decoration: it is the guard against the ping-pong loop, and the section on recursion limits below explains why you cannot rely on the framework to stop you.
The supervisor is a routing function, not a chat partner
The supervisor should return a decision, never prose. Structured output makes that enforceable instead of aspirational.
from langgraph.types import Command
class Route(TypedDict):
next: Literal["researcher", "writer", "finish"]
reason: str
def supervisor(state: TeamState) -> Command:
if state.get("handoffs", 0) >= 3:
return Command(goto=END, update={"next": "finish"})
router = model.with_structured_output(Route)
decision = router.invoke([
{"role": "system", "content": SUPERVISOR_PROMPT},
*state["messages"],
])
goto = END if decision["next"] == "finish" else decision["next"]
return Command(goto=goto, update={"next": decision["next"],
"handoffs": state["handoffs"] + 1})
Command in langgraph 1.2.16 carries four fields: graph, update, resume and goto, where goto accepts a node name, a list of node names, or a Send object. Returning it from a node both writes state and picks the next node in one step, which removes the classic bug of an edge that no branch declares.
Subagents as ordinary nodes
from langchain.agents import create_agent
researcher = create_agent(model=model, tools=[search_docs], name="researcher",
prompt="Search the internal docs. Return findings with source titles. Do no writing.")
writer = create_agent(model=writer_model, tools=[], name="writer",
prompt="Write from the notes in state. Never invent facts.")
builder = StateGraph(TeamState)
builder.add_node("supervisor", supervisor)
builder.add_node("researcher", researcher)
builder.add_node("writer", writer)
builder.add_edge(START, "supervisor")
for node in ("researcher", "writer"):
builder.add_edge(node, "supervisor")
graph = builder.compile()
Two details keep this from rotting. First, a compiled create_agent graph reads and writes messages, so your state must have that key with the add_messages reducer or the first handoff fails validation. Second, every worker returns to the supervisor rather than chaining to a sibling. A worker that jumps straight to another worker looks tidy in a diagram and makes the view of the conversation that the supervisor holds a lie.
Durable threads, and the config key everyone forgets
from langgraph.checkpoint.sqlite import SqliteSaver
with SqliteSaver.from_conn_string("checkpoints.sqlite") as saver:
graph = builder.compile(checkpointer=saver)
config = {"configurable": {"thread_id": "ticket-4821"}, "recursion_limit": 40}
graph.invoke({"messages": [{"role": "user", "content": TICKET}],
"notes": [], "handoffs": 0}, config)
snapshot = graph.get_state(config)
print(snapshot.next, len(snapshot.values["messages"]))
Re-invoking with the same thread_id resumes from the last checkpoint. If a worker dies halfway through, the retry continues instead of restarting the whole conversation and paying for the same tokens twice. The most common checkpointer complaint is a missing thread_id: without it the saver has no key to write under, and the run is correct but ephemeral.
The recursion limit is no longer 25
Most blog posts still say LangGraph stops a runaway graph at 25 supersteps. On 1.2.16 that number is stale. The config module inside the wheel defines:
DEFAULT_RECURSION_LIMIT = int(getenv("LANGGRAPH_DEFAULT_RECURSION_LIMIT", "10007"))
That is not a typo for 25, and it changes the failure mode. A supervisor that keeps handing work back and forth will happily spend a thousand model calls before the framework objects, by which point the invoice is the error message. Set recursion_limit explicitly to something your budget tolerates, and keep an application-level guard like the handoffs counter above. Two guards, because they fail differently: the counter ends the run cleanly with a note, the limit raises an exception you can page on.
What the loop costs, in tokens
Assume each supervisor turn re-sends a 1,200-token base transcript and each agent appends roughly 300 tokens. Eight turns of a ping-pong exchange means input tokens of 1,200 for the first call, 1,500 for the second, and so on, which sums to 18,000 tokens read across the run against 1,200 if one agent had answered directly. Twelve turns sums to 34,200. The growth is quadratic in turns, so a loop that doubles the turn count nearly doubles the bill again. The handoffs counter capped at three is the cheapest optimisation in this article.
Test the routing without a model
Do not mock the whole graph. Extract the decision into a pure function that takes a transcript and returns a route, then test that:
def test_supervisor_sends_pricing_to_writer():
state = {"messages": [{"role": "user", "content": "summarise the pricing doc"}],
"notes": ["pricing.md"], "handoffs": 0}
cmd = supervisor_stub(state, decide=lambda s: {"next": "writer", "reason": "draft"})
assert cmd.goto == "writer"
assert cmd.update["handoffs"] == 1
def test_budget_exhaustion_ends_the_run():
state = {"messages": [], "notes": [], "handoffs": 3}
cmd = supervisor_stub(state, decide=lambda s: {"next": "researcher", "reason": "loop"})
assert cmd.goto == END
Two assertions catch most routing regressions, and they run in milliseconds because no model is involved. Keep the live model for a nightly smoke test of the full graph.
When a supervisor is the wrong answer
If your subagents share one tool set, use one agent. If the split exists only because a diagram looked impressive, delete it. The supervisor earns its overhead in three concrete cases: different agents need different permissions or tool sets, a cheap model should handle extraction while an expensive one writes, or a human has to approve a step before the next one runs. Outside those, the extra node is pure cost, both in tokens and in the number of places a bug can hide.
FAQ
Does the langgraph-supervisor package still work? Yes, at 0.0.31 on PyPI. Its README now recommends the tools-based pattern and positions the library as a compatibility path for LangChain 1.0 upgrades. New builds are better off wiring the supervisor themselves.
Do I need Postgres for durability? No. langgraph-checkpoint-sqlite at 3.1.2 is enough for one host. Postgres enters when several workers must share checkpoints or you need replicas.
Why did my handoff return an empty answer? Usually the supervisor wrote text and nothing routed. Print snapshot.next after the run: an empty tuple means the graph reached END, which is what happens when a node returns a message and no Command.
Can subagents talk to each other directly? They can, using Command(graph=Command.PARENT) from a nested graph, but then the transcript the supervisor holds stops being the source of truth. For most teams that trade is not worth it.
If retrieval quality is the thing that keeps failing rather than routing, the harness in the Ragas evaluation walkthrough separates retrieval failures from generation failures before you touch the graph. For the surrounding stack, the LangChain 1.x rebuild notes and the PDF to Markdown harness cover the pieces underneath.
Written by our editorial team; tools listed here are tested or verified against public sources. Links point to official sites or GitHub repos for reference only — no paid placements.
