teachyou.ai academy
← All posts
LangChainLangGraphmulti-agent systemsAI agentsorchestration

The LangGraph Swarm Pattern for Multi-Agent Systems

Pramod Dutta · Jun 29, 2026 · 13 min read

A langgraph swarm is a multi-agent architecture where specialized agents talk to each other directly by handing off control, instead of routing every decision through a central supervisor. In LangGraph terms, a swarm is a graph where each agent is a node, each agent carries a set of "handoff tools" that transfer control to a named peer, and the graph keeps track of whichever agent is currently active so that the next user message goes straight back to it. This article walks through why that matters, how to build one with the langgraph-swarm library, and where it breaks down compared to a supervisor.

What the swarm pattern actually is

Most people's first multi-agent design is a supervisor: one router agent reads the user's message, decides which specialist should handle it, calls that specialist as a tool or subgraph, gets a result back, and decides what to do next. The supervisor is the single source of truth for control flow. It works, but it has a tax: every handoff between specialists round-trips through the supervisor, the supervisor's prompt has to know about every specialist and when to use it, and the supervisor becomes the bottleneck as you add more agents.

A langgraph swarm removes the middleman for lateral handoffs. Each agent is still built the normal way (a ReAct-style agent with its own system prompt, its own tools, its own model), but in addition to its domain tools, each agent gets one or more handoff tools: a tool whose only job is "transfer this conversation to agent X." When agent A calls the handoff tool for agent B, the graph updates two things: the active agent pointer and the shared message history that both agents see. Control moves to B directly, no supervisor in the loop, and B picks up the conversation with full context of what A and the user already said.

The graph remembers who's "in the driver's seat" using a small piece of state, usually called active_agent. That's the mechanism that makes a swarm feel like a single stateful conversation instead of a series of disconnected tool calls: the next turn from the user is automatically routed to whichever specialist was last active, not back to a router that has to re-decide from scratch.

Swarm vs. supervisor vs. hierarchical: when to use which

Three patterns cover most production multi-agent designs in LangGraph:

  • Supervisor: one node owns routing. Good when you want centralized policy, auditability of every routing decision, or when specialists shouldn't be able to talk to each other without oversight (a compliance-sensitive support desk, for example).
  • Swarm: agents route directly to each other. Good when the specialists have natural collaboration patterns your users will trigger unpredictably, for example a coding agent handing off to a testing agent, which hands off to a docs agent, and back to the coding agent, without a human articulating "please switch to the docs agent" every time.
  • Hierarchical (supervisor of supervisors): nested supervisors, each owning a sub-team. Good when the specialist count is large enough that one flat router's prompt gets unwieldy.

The rule of thumb: reach for swarm when the *conversation*, not a policy engine, should decide the next specialist, and the agents themselves are in a good position to judge that ("the user is now asking about billing, not shipping, hand off to billing_agent"). Reach for supervisor when you want a single place to enforce who is allowed to talk to whom.

Installing langgraph-swarm

The reference implementation lives in the langgraph-swarm Python package, built on top of langgraph and langgraph.prebuilt.

pip install langgraph langgraph-swarm langchain-anthropic

You'll also want a checkpointer for memory across turns. For local development, InMemorySaver from langgraph.checkpoint.memory is enough; for production you'd swap in a Postgres or SQLite checkpointer.

Building a two-agent swarm

The smallest useful swarm has two specialists that can hand off to each other. Say you're building a travel assistant: one agent books flights, one agent books hotels, and either one might need to pull the other in mid-conversation.

from langchain_anthropic import ChatAnthropic
from langgraph.prebuilt import create_react_agent
from langgraph.checkpoint.memory import InMemorySaver
from langgraph_swarm import create_handoff_tool, create_swarm

model = ChatAnthropic(model="claude-sonnet-4-5")

# Domain tools for each specialist
def search_flights(origin: str, destination: str, date: str) -> str:
    """Search available flights between two cities on a date."""
    return f"3 flights found from {origin} to {destination} on {date}"

def book_flight(flight_id: str) -> str:
    """Book a specific flight by its id."""
    return f"Flight {flight_id} booked"

def search_hotels(city: str, checkin: str, checkout: str) -> str:
    """Search available hotels in a city for a date range."""
    return f"5 hotels found in {city} for {checkin} to {checkout}"

def book_hotel(hotel_id: str) -> str:
    """Book a specific hotel by its id."""
    return f"Hotel {hotel_id} booked"

# Handoff tools: each one is named for the agent it transfers to
transfer_to_hotel_agent = create_handoff_tool(
    agent_name="hotel_agent",
    description="Transfer to the hotel booking agent for hotel-related requests.",
)
transfer_to_flight_agent = create_handoff_tool(
    agent_name="flight_agent",
    description="Transfer to the flight booking agent for flight-related requests.",
)

flight_agent = create_react_agent(
    model,
    tools=[search_flights, book_flight, transfer_to_hotel_agent],
    prompt="You are a flight booking assistant. Handle flight questions. "
           "If the user asks about hotels, hand off to hotel_agent.",
    name="flight_agent",
)

hotel_agent = create_react_agent(
    model,
    tools=[search_hotels, book_hotel, transfer_to_flight_agent],
    prompt="You are a hotel booking assistant. Handle hotel questions. "
           "If the user asks about flights, hand off to flight_agent.",
    name="hotel_agent",
)

checkpointer = InMemorySaver()

workflow = create_swarm(
    [flight_agent, hotel_agent],
    default_active_agent="flight_agent",
)
app = workflow.compile(checkpointer=checkpointer)

Every agent built with create_react_agent needs a unique name, since that's what the handoff tools and the active_agent state reference. create_swarm wires the agents into a single StateGraph, adds the routing edges, and adds the active_agent field to the shared state schema for you.

Running it

config = {"configurable": {"thread_id": "trip-1"}}

result = app.invoke(
    {"messages": [{"role": "user", "content": "Find me a flight from Delhi to Goa on March 5"}]},
    config,
)
print(result["messages"][-1].content)

result = app.invoke(
    {"messages": [{"role": "user", "content": "Also find me a hotel there for the same dates"}]},
    config,
)
print(result["messages"][-1].content)
print(result["active_agent"])

The first call starts at flight_agent (the default_active_agent) and searches flights. The second call, same thread_id, still starts wherever the swarm left off. Because the flight agent's prompt tells it to hand off on hotel questions, it calls transfer_to_hotel_agent, the graph updates active_agent to "hotel_agent", and the hotel agent responds with hotel search results, in the same turn, without the user having to say "talk to the hotel agent" explicitly. On the next user turn, the graph routes straight to hotel_agent because that's who's active, not back to flight_agent.

That's the core value: continuity of "who's talking" persists across turns via the checkpointer, and continuity of "who should talk next" is negotiated by the agents themselves via handoff tools, not recomputed by a router on every message.

What a handoff tool actually does

create_handoff_tool builds a tool that, when called, returns a Command object instead of a plain string. A Command in LangGraph can update state and specify the next node to jump to in a single return value. Under the hood a handoff tool does two things:

  1. Appends a ToolMessage to the shared messages state so the target agent (and any transcript viewer) sees that a handoff happened and why.
  2. Sets active_agent to the target agent's name and routes execution to that agent's node.

You can write your own handoff tool if you need custom behavior, for example carrying structured data across the handoff instead of just messages:

from langgraph.types import Command
from langchain_core.tools import tool
from langchain_core.tools.base import InjectedToolCallId
from langgraph.prebuilt import InjectedState
from typing import Annotated

@tool
def transfer_to_billing_agent(
    reason: str,
    state: Annotated[dict, InjectedState],
    tool_call_id: Annotated[str, InjectedToolCallId],
) -> Command:
    """Transfer to the billing agent, passing along the reason for the handoff."""
    tool_message = {
        "role": "tool",
        "content": f"Transferring to billing_agent: {reason}",
        "tool_call_id": tool_call_id,
    }
    return Command(
        goto="billing_agent",
        update={"messages": state["messages"] + [tool_message], "active_agent": "billing_agent"},
        graph=Command.PARENT,
    )

create_handoff_tool covers the common case; write a custom one when you need to pass structured context (a ticket ID, a partially filled form) rather than relying on the specialist re-reading the message history to figure out where things stand.

Shared state and memory

By default the swarm's state schema is just messages plus active_agent. All agents in the swarm read and write the same messages list, so every specialist sees the full conversation, including turns that were handled by other agents. That's usually what you want: the hotel agent shouldn't need the user to repeat travel dates that were already given to the flight agent.

If your agents need private scratch state that shouldn't leak to peers (a flight agent's intermediate search results, say), extend the state schema with per-agent keys and reducer functions that keep those keys separate from the shared messages channel. create_swarm accepts a state_schema argument for exactly this.

For persistence across sessions, the checkpointer is what makes the thread_id durable: the same thread_id on a later invoke call resumes with the full message history and the correct active_agent, so a user can close the app and come back to a mid-handoff conversation without losing state.

Streaming a swarm

Swarms are ordinary compiled LangGraph graphs, so the normal streaming modes apply:

for chunk in app.stream(
    {"messages": [{"role": "user", "content": "Book the second flight option"}]},
    config,
    stream_mode="updates",
):
    print(chunk)

stream_mode="updates" gives you one event per node execution, which is the most useful mode for debugging handoffs: you'll see the acting agent's node fire, then (if a handoff tool was called) the target agent's node fire in the same turn, with active_agent visible in the state update. stream_mode="values" gives you the full state after each step if you want the whole message list at every point instead of a diff.

Nesting a swarm inside a larger graph

A compiled swarm is just a graph, which means you can drop it in as a subgraph node inside something bigger, for example a top-level graph that does input validation and guardrails before handing off to the swarm, and output formatting after:

from langgraph.graph import StateGraph, START, END, MessagesState

def validate_input(state: MessagesState) -> MessagesState:
    # guardrail logic here
    return state

def format_output(state: MessagesState) -> MessagesState:
    # post-processing here
    return state

outer = StateGraph(MessagesState)
outer.add_node("validate", validate_input)
outer.add_node("swarm", app)  # compiled swarm from earlier
outer.add_node("format", format_output)

outer.add_edge(START, "validate")
outer.add_edge("validate", "swarm")
outer.add_edge("swarm", "format")
outer.add_edge("format", END)

outer_app = outer.compile(checkpointer=checkpointer)

This is a common way to combine swarm and supervisor thinking: use a thin supervisor-style shell for policy and guardrails, and delegate the actual multi-specialist collaboration to a swarm subgraph where the agents are trusted to route among themselves.

Debugging handoffs

Two failure modes show up repeatedly when people build their first swarm:

Ping-pong handoffs. Agent A hands off to B, B immediately hands off back to A, and the swarm loops without making progress. This almost always traces back to vague handoff tool descriptions or system prompts. Be explicit in each agent's prompt about what it owns and, just as important, what it does not own, so the model doesn't reflexively hand back questions it could actually answer.

Silent context loss. If you write a custom handoff tool and forget to append the shared messages correctly, the receiving agent starts "cold," and you'll see it re-ask the user for information it should already have. When this happens, stream with stream_mode="values" and inspect the message list right after the handoff ToolMessage to confirm what the receiving agent will actually see as its input.

For anything beyond toy examples, run the swarm with LangSmith tracing turned on (LANGCHAIN_TRACING_V2=true, LANGCHAIN_API_KEY set). The trace view shows each agent node, each tool call including handoff tools, and the state diff at each step, which makes ping-pong loops and context loss obvious at a glance instead of something you infer from printed output.

Common pitfalls

  • Too many peers. A swarm with a dozen agents that can all hand off to each other means every agent's prompt has to reason about a dozen possible destinations. Past a handful of specialists, group them and go hierarchical: a swarm of swarms, or a supervisor over sub-swarms, rather than one flat mesh.
  • No default active agent. If you don't set default_active_agent, the first message in a new thread has nowhere defined to go. Always set it explicitly to whichever agent should greet the user.
  • Handoff tools with weak descriptions. The model chooses to call a handoff tool the same way it chooses any other tool: by reading the tool's docstring/description against the user's message. A vague description ("switches agents") produces unreliable routing. Describe precisely which requests belong with that agent.
  • Assuming swarm replaces evaluation. Swarms make collaboration easier to build, not easier to verify. You still need transcripts and test conversations that exercise multi-hop handoffs (A to B to C and back) before shipping, because the failure modes are different from a single agent: a bad handoff can strand the user with the wrong specialist mid-task.
  • Forgetting the checkpointer in production. Without a durable checkpointer, active_agent resets every process restart, and users mid-handoff get bounced back to the default agent. Use a persistent backend (Postgres, SQLite, or your platform's managed checkpointer) outside of local development.

FAQ

Is langgraph-swarm a separate framework from LangGraph? No. It's a small library built on top of langgraph and langgraph.prebuilt that packages the handoff-tool and active-agent-tracking pattern into create_handoff_tool and create_swarm helper functions. You could hand-build the same graph yourself with StateGraph, Command, and custom routing edges; the library just saves you from wiring that boilerplate every time.

Do agents in a swarm need to use the same model? No. Each agent is built independently with create_react_agent (or your own custom node), so you can mix models, for example a cheaper model for a simple FAQ agent and a stronger model for a complex reasoning agent, as long as each one can reliably call its handoff tools.

How is a swarm different from tool-calling between agents? In a plain tool-calling setup, agent A calls agent B as a tool, waits for B's return value, and stays in control, similar to a function call. In a swarm, a handoff transfers control: B becomes the active agent going forward, and the next user turn goes to B directly, not back through A. That's the difference between "delegate a subtask and get an answer back" and "pass the conversation to a peer."

Can a swarm have more than two agents? Yes, create_swarm accepts a list of any number of agents. The scaling limit isn't the library, it's prompt complexity: each agent needs a handoff tool per peer it can transfer to, and its system prompt needs to reason about when to use each one. Beyond four or five peers, consider grouping specialists into sub-swarms behind a supervisor.

What state does a handoff carry over? By default, the full shared messages list, so the receiving agent has the entire conversation history. active_agent also updates so the state machine knows who's in charge. If you need additional structured context to survive a handoff, write a custom handoff tool with Command that updates extra state keys, as shown above.

Does a swarm work with human-in-the-loop interrupts? Yes, since it's a standard LangGraph graph, you can use interrupt() inside any agent node the same way you would in a single-agent graph, and resume with a checkpointer exactly as usual. The interrupt happens inside whichever agent is currently active; handoffs that occurred before the interrupt are already reflected in active_agent when you resume.

When should I not use a swarm? Skip it when you need centralized, auditable control over every routing decision, for example a regulated workflow where a compliance rule must approve every specialist transfer, or when your specialist count is large enough that a flat mesh of handoff tools becomes unmanageable. A supervisor, or a hierarchy of supervisors, fits those cases better.