LangGraph supervisor vs swarm: a local comparison
Supervisor vs Swarm: 6 Differences I Tested (LangGraph multi-agent, locally)
TL;DR: LangChain ships two "multi-agent" libraries whose names sound like synonyms. I tested both on the same task and the difference comes down to one thing: who decides who speaks next. In supervisor, the central node re-decides every time. In swarm, a state field called active_agent decides, and the checkpointer remembers it. That difference is invisible in a single turn and becomes "your follow-up question never got re-triaged" on the second turn. Two more differences are quieter: supervisor silently deletes your worker's intermediate steps by default, and neither topology actually parallelises, though they fail in completely different ways. Every number below comes from local runs on langgraph 1.2.18 + langgraph-supervisor 0.0.31 + langgraph-swarm 0.1.0 (Python 3.13, Windows 11). This page contains no affiliate links.
Start with the two official sentences
The whole argument is contained in two sentences of official documentation. From the Patterns table at docs.langchain.com/oss/python/langchain/multi-agent:
- Subagents (the supervisor semantics): "A main agent coordinates subagents as tools. All routing passes through the main agent, which decides when and how to invoke each subagent."
- Handoffs (the swarm semantics): "Agents transfer control to each other via tool calls. Each agent can hand off to others or respond directly to the user."
The langchain-ai/langgraph-swarm README puts it even more plainly: "The system remembers which agent was last active, ensuring that on subsequent interactions, the conversation resumes with that agent."
In one line: supervisor routes everything through a switchboard; swarm leaves the phone with whoever you last talked to. LangChain's own blog post *Benchmarking Multi-Agent Architectures* attributes supervisor's extra token cost to exactly this "translation" layer, because the worker cannot answer the user directly and the supervisor has to restate everything.
If you have read the LangGraph persistence article on this site, treat this as its sequel: the checkpointer remembers state, and swarm remembers who holds the microphone.
Test rig: no key, no model, no network
I did not want a billing account attached to a comparison article, so I replaced the model with an object that replays a fixed script. LangGraph's create_react_agent supports a dynamic model: any callable with signature (state, runtime) that returns a LangChain Runnable will do (see _resolve_model in langgraph/prebuilt/chat_agent_executor.py). Routing, tool execution and state writes all behave normally; only the "thinking" step is scripted.
Install into an isolated directory so your main environment stays untouched:
pip install --target D:/openclaw-blog/tmp/ma-env langgraph-supervisor langgraph-swarm
Versions measured (all confirmed against the GitHub Releases API):
- langgraph 1.2.18 (released 2026-10-26T20:04:45Z)
- langgraph-prebuilt 1.1.1 / langgraph-checkpoint 4.3.1 / langchain-core 1.7.0
- langgraph-supervisor 0.0.31 (released 2025-11-19T18:34:43Z)
- langgraph-swarm 0.1.0 (released 2025-12-04T19:04:25Z)
The scripted model itself. It needs a bind_tools that returns itself, because create_supervisor calls it internally:
from langchain_core.messages import AIMessage
from langchain_core.runnables import RunnableLambda
class ScriptedModel:
def __init__(self, script):
self.script = script # list of AIMessage; call n returns message n
self.n = 0
def bind_tools(self, tools, **kw):
return self
def __call__(self, state, runtime=None):
i = self.n
self.n += 1
msg = self.script[i] if i < len(self.script) else AIMessage(content="(exhausted)")
return RunnableLambda(lambda _: msg)
A worker is then one line: bob = create_react_agent(ScriptedModel([AIMessage(content="Bob worked")]), tools=[], name="bob").
Difference 1: who answers the second turn
Same task, "let bob do the work". Tracing nodes with stream_mode="updates":
- supervisor:
supervisor(agent) → supervisor → bob(agent) → bob → supervisor(agent) → supervisor— 6 steps - swarm:
alice(agent) → alice → bob(agent) → bob— 4 steps
The interesting part is not the step count. Add a checkpointer and ask a second question ("and now?") on the same thread_id:
- supervisor's next executed node is supervisor again, so the follow-up is re-triaged.
- swarm's next executed node is bob directly, and
get_state().values["active_agent"]reads"bob"— the value written last turn survived.
This is intentional and officially documented, and in the docs it is a feature: later interactions resume with the same specialist. The part that bites is that your second sentence may not belong to that specialist at all. Ask the finance agent to balance a ledger, then casually ask about the weather: swarm hands that sentence straight to finance, while supervisor at least lets the central node look at it again. If you pick swarm, do one of two things: tell every agent in its system prompt to hand back anything outside its domain, or reset active_agent at the start of every new task in your application layer.
Difference 2: the default setting deletes your worker's intermediate steps
create_supervisor takes an output_mode argument that defaults to last_message. It controls how much of the worker's internal message history is written back into the parent graph.
I gave bob a tool so its internals contained one tool-call round trip, then compared:
output_mode="full_history": parent history ends with 9 messages, 3 of them from bob (including the intermediate tool result)output_mode="last_message"(the default): 7 messages, only 2 from bob — the intermediate ToolMessage is gone
The logic lives in _make_call_agent in langgraph_supervisor/supervisor.py:
elif output_mode == "last_message":
if isinstance(messages[-1], ToolMessage):
messages = messages[-2:]
else:
messages = messages[-1:]
When does this hurt? When your supervisor needs to make its next decision based on which tools the worker actually called and with what arguments — auditing, billing, passing context to another worker. Under the default those artefacts vanish, with no exception and no warning. Either pass output_mode="full_history" explicitly, or have the worker restate the relevant intermediate results in its final message. If you only want the answer and care about context budget, the default is the right choice. The default is not a bug; it is a setting you need to know exists.
Difference 3: parallel dispatch — neither topology really does it
The official "Choosing a pattern" table has a Parallelization column where Handoffs (swarm) is marked --, explicitly unsupported, while Subagents (supervisor) gets stars. What I measured is subtler than the table.
First, supervisor: I had the supervisor emit two handoff tool calls in a single AIMessage (transfer_to_alice and transfer_to_bob). There is a dedicated parallel branch in langgraph_supervisor/handoff.py; when len(last_ai_message.tool_calls) > 1 it wraps the jump in Send, with this comment:
# NOTE: we are using Send here to allow the ToolNode in langgraph.prebuilt
# to handle parallel handoffs by combining all Send commands into a single command
goto=[Send(agent_name, {**state, "messages": handoff_messages})],
Following that hint, I expected both workers to run. What actually happened: across 6 independent runs, exactly one worker executed every single time (bob 4 times, alice 2 — which one survives is not deterministic), the other left no trace and raised nothing, and the final history held 5 messages. I monkey-patched ToolNode._combine_tool_outputs and found it called twice with n_input=1 each time: the two handoff tools each produced a parent command carrying a single Send, they never landed in the same batch, and the accumulation branch (replace(parent_command, goto=parent_command.goto + output.goto)) is never reached.
Now the same experiment on swarm. It raises immediately, with an error that looks unrelated to handoffs:
ValueError: Found AIMessages with tool_calls that do not have a corresponding ToolMessage.
Here are the first few of those tool calls:
[{'name': 'transfer_to_charlie', 'args': {}, 'id': 'w2', 'type': 'tool_call'}].
Every tool call (LLM requesting to call a tool) in the message history MUST have a
corresponding ToolMessage (result of a tool invocation to return to the LLM) ...
For troubleshooting, visit: https://docs.langchain.com/oss/python/langgraph/errors/INVALID_CHAT_HISTORY
The engineering conclusion is simple: if you want parallelism, do not expect either topology to provide it. Build a fan-out / fan-in subgraph with Send yourself, or force supervisor to emit one handoff per turn. That is far more reliable than depending on a code path whose comment promises parallelism that the trace does not show.
Troubleshooting: seven real errors I reproduced
All seven were reproduced locally, with the raising line and the workaround.
Error 1: ParentCommand — invoking an agent that has handoff tools, standalone
Symptom: you call bob.invoke(...) to debug one worker and it blows up with langgraph.errors.ParentCommand. str(e) prints a thousand-character repr of the entire Command object, with no explanation and no remediation hint.
Cause: the tool built by create_handoff_tool returns Command(goto=..., graph=Command.PARENT, ...). Line 1762 of langgraph/graph/state.py does a bare raise ParentCommand(command) with no friendly wrapper. ParentCommand subclasses GraphBubbleUp, an internal signal for a subgraph talking to its parent — with no parent to talk to, it surfaces as a message-less exception.
Fix: handoff tools only work inside a graph assembled by create_swarm or create_supervisor. To unit-test a single worker, wrap it in a one-agent swarm temporarily, or swap in a tool list that excludes the handoff tools.
Error 2: KeyError: 'charlie' — writing a nonexistent agent name into active_agent
Symptom: you pass it manually, write it via update_state, or a confused model produces it. All you get is KeyError: 'charlie', raised at langgraph/graph/_branch.py line 203.
Cause: create_swarm narrows active_agent: str | None into Literal['alice','bob'] | None (_update_state_schema_agent_names in langgraph_swarm/swarm.py), and add_active_agent_router attaches a conditional edge from START with path_map=route_to. A return value outside path_map yields a bare KeyError.
Fix: validate at the boundary. Assert active_agent in {a.name for a in agents} at your entry point and do not count on the graph to explain itself.
Error 3: Error: transfer_to_Bob is not a valid tool — case and whitespace
Symptom: the model emits transfer_to_Bob and the transcript grows a ToolMessage reading Error: transfer_to_Bob is not a valid tool, try one of [transfer_to_bob]. No exception. The handoff silently fails and the model usually retries or gives up.
Cause: create_handoff_tool normalises with _normalize_agent_name: WHITESPACE_RE.sub("_", agent_name.strip()).lower(). Consequences:
agent_name="Bob"produces tooltransfer_to_bob, whileCommand(goto=...)still uses"Bob"agent_name="code reviewer"producestransfer_to_code_reviewer, while the jump target stays"code reviewer"with the space
That is to say: the tool name and the node name are two different strings. Fix: name agents in lowercase snake_case, and spell the exact target node name inside the handoff tool's description instead of making the model guess.
Error 4: ValueError: Please specify a name when you create your agent
Raised at langgraph_supervisor/supervisor.py:400. Triggered whenever a graph passed to create_supervisor has name is None or name == "LangGraph", i.e. a compile() call with no name. Fix: create_react_agent(..., name="xxx") or graph.compile(name="xxx").
Error 5: ValueError: Agent with name 'dup' already exists. Agent names must be unique.
Same loop, line 406. Note the check is on the name, not object identity: two different workers that happen to share a name will trip it.
Error 6: ValueError: Invalid agent output mode: all_history. Needs to be one of ('full_history', 'last_message')
Raised at supervisor.py:73. Only two values are legal (per the source comments: full_history adds the entire history, last_message adds only the last message), and misspellings like all_history are rejected outright — which makes this one of the friendlier errors in the set.
Error 7: ValueError: Missing required key 'active_agent' in state_schema and Default active agent 'charlie' not found in agent names ['alice']
Both live in langgraph_swarm/swarm.py (lines 257 and 267). The first is usually caused by writing supervisor-shaped code: passing MessagesState straight into create_swarm. Swarm needs its own SwarmState (or a custom schema that does carry active_agent). The second means default_active_agent does not match any agent's name, and the comparison is case-sensitive. An empty list fails too: agents list cannot be empty at swarm.py:261.
One supervisor-side sibling worth indexing for search: once you supply custom handoff tools, you must supply them for every sub-agent, or supervisor.py:167 raises ValueError: When providing custom handoff tools, you must provide them for all subagents. Missing handoff tools for agents '{'bob'}'.
Which one to use
- You need a central fallback, auditing, billing, or guaranteed re-triage of follow-ups → supervisor
- Agents must talk to the user directly (support handover, expert takeover) and a conversation rarely crosses domains → swarm, plus a prompt that tells each agent to hand back anything outside its domain
- Neither gives you automatic parallelism; when you need it, build fan-out / fan-in with
Send - An under-reported fact: the
langchain-ai/langgraph-supervisorREADME now carries an official note at the top — "We now recommend using the supervisor pattern directly via tools rather than this library for most use cases... We're making this library compatible with LangChain 1.0 to help users upgrade their existing code." The recommended modern approach is wrapping sub-agents as tools;create_supervisoris closer to a compatibility layer now. Existing projects need not migrate in a hurry, but new ones can follow the official supervisor tutorial directly.
FAQ
Q: Without a checkpointer, does swarm still remember the last active agent?
No. active_agent is a field in graph state, and it only survives between turns via a checkpoint. With no checkpointer, every invoke starts from default_active_agent. The Difference 1 findings above assume a checkpointer is in place.
Q: Is supervisor always more expensive, given that every turn goes back to the centre?
The official blog concludes supervisor is consistently pricier, and blames the translation layer. But if you needed a central node for validation or auditing anyway, that spend bought you control rather than waste. Also remember the default output_mode strips the worker's intermediate steps, which helps if you are trying to cut context costs.
Q: Can I mix the two?
Yes, and the docs encourage it: use handoffs at the outer layer for expert switching, and have each expert call other agents as tools internally. The LangGraph streaming article on this site covers what the same runtime does under streaming, and the human-in-the-loop article covers the six hard rules that appear once you put a person into this loop. Reading them together makes the topology question easier to debug.
Q: Just tell me which library to install.
To maintain an existing project, keep using these two (versions above, all current on GitHub Releases at time of writing). For a new project, read the official supervisor tutorial's "wrap it as a tool" approach first. The two libraries do not conflict, but both pin against the same langgraph base version.
Sources
docs.langchain.com/oss/python/langchain/multi-agent— the Patterns table and the starred "Choosing a pattern" grid where Handoffs shows--for Parallelizationdocs.langchain.com/oss/python/langchain/supervisor— the official supervisor tutorial, including the "If you are migrating from the langgraph-supervisor package..." note- GitHub
langchain-ai/langgraph-swarmREADME — the original wording onactive_agentsurviving across turns - GitHub
langchain-ai/langgraph-supervisorREADME — the official note recommending against using the library directly - LangChain blog, Benchmarking Multi-Agent Architectures — the multi-agent benchmark and its cost attribution
- GitHub Releases API —
langgraph==1.2.18(2026-10-26),langgraph-supervisor==0.0.31(2025-11-19),langgraph-swarm==0.1.0(2025-12-04) - Local source lines —
langgraph_supervisor/supervisor.py(73 / 167 / 400 / 406),langgraph_supervisor/handoff.py(Send merge branch),langgraph_swarm/swarm.py(257 / 261 / 267),langgraph_swarm/handoff.py(_normalize_agent_name),langgraph/graph/state.py(1762),langgraph/graph/_branch.py(203),langgraph/prebuilt/tool_node.py(_combine_tool_outputs)
Limits of this test
Three caveats. First, there was no real LLM involved: the models are scripted, so the findings cover topology and runtime behaviour, not what a real model will emit. Whether your provider issues parallel tool calls is a provider question. The official supervisor tutorial says "many LLMs will issue multiple tool calls in a single response ... which the runtime executes in parallel", which does not match what I measured on the langgraph-supervisor 0.0.31 handoff path — that discrepancy is the single item most worth re-checking yourself. Second, I only tested InMemorySaver; SQLite and Postgres backends were not exercised for concurrency consistency. Third, everything ran on Windows 11 with Python 3.13, with no Linux control group. The topology conclusions are platform-independent, but the source line numbers are specific to langgraph 1.2.18. This page contains no affiliate links.
👉 Join Xiaomi MiMo Platform: Leading AI model platform with cost-effective inference
👉 Join Aliyun AI: Top AI products with exclusive coupons for business innovation
📌 This article was AI-assisted generated and human-reviewed | TechPassive — An AI-driven content testing site focused on real tool reviews
🔗 Recommended Tools
These are carefully selected tools. Using our affiliate links supports us to keep producing quality content: