Menu

LangGraph Multi-Agent: Supervisor, Swarm & Network

Build multi-agent AI systems in LangGraph using supervisor, swarm, and network patterns — with full code for each and a guide to choosing the right one.

Written by Selva Prabhakaran | 34 min read

Instead of one agent doing everything, you can split the work across focused agents that each own one job — cleaner prompts, fewer tools per agent, and better results.

You build an agent that researches topics, writes drafts, and checks facts. It works — until the prompt grows so long that the model forgets its own rules. You add more tools and the agent starts picking the wrong ones. This is the single-agent ceiling, and nearly every team hits it sooner or later.

The fix is to stop asking one agent to do everything. Instead, you split the work across agents that each have a clear role and a short prompt. These agents talk to each other through well-defined patterns — and that is what this post is about.

Before we touch any code, let me give you the quick picture of the three patterns we will cover.

In the supervisor pattern, one central agent plays manager. It gets every request, picks which worker to call, reads the result, and either calls the next worker or returns a final answer. The manager sees all. The workers see only what the manager sends them.

In the swarm pattern, there is no manager at all. Agents pass the baton to each other directly. Agent A finishes its piece and says, “Agent B should take this next.” The flow grows out of these hand-offs rather than from a central plan.

In the network (or mesh) pattern, any agent can reach any other agent at any time. There is no set order and no set chain. An agent decides mid-task that it needs help and calls for it. This is the most free-form pattern — and the hardest to trace.

We will build all three in LangGraph, compare their trade-offs, and then combine them into a real research team.

Why Do Single Agents Hit a Ceiling?

A lone ReAct agent handles small tasks just fine. Ask it to search the web and sum up the results — easy. But real work is rarely that small.

Think about a content pipeline: research a topic, draft an article, fact-check the draft, then format it for the blog. One agent doing all four jobs needs tools for every stage. Its system prompt balloons to hundreds of lines. The context window fills up with tool descriptions the agent does not need right now.

Three issues pop up again and again:

  • Noisy context. Every tool’s blurb sits in the prompt, even when the agent only needs one tool. More tools means more noise and worse tool picks.
  • Blurred roles. A single prompt that says “you are a researcher AND a writer AND a fact-checker” pulls the model in three ways at once. Each task gets done worse.
  • Context overload. Long chats with many tool calls eat the context window. The agent loses track of its own rules.

Multi-agent systems fix all three. Each agent gets a tight prompt, a short tool list, and its own chat slice.

Key Insight: More agents do not make any one agent smarter. They make the total system smarter by giving each agent a clean context and a clear focus.

Prerequisites

  • Python: 3.10 or newer
  • Packages: langgraph 0.4+, langgraph-supervisor 0.0.7+, langgraph-swarm 0.0.5+, langchain-openai 0.3+, langchain-core 0.3+
  • Install command: pip install langgraph langgraph-supervisor langgraph-swarm langchain-openai langchain-core
  • API key: An OpenAI key stored as OPENAI_API_KEY. See OpenAI’s docs to create one.
  • Estimated time: about 45 minutes
  • Background: Basic LangGraph ideas (nodes, edges, state, subgraphs) from the earlier posts in this series.

The first code block loads all the imports we need. We bring in the LLM wrapper, message types, tool helpers, and the multi-agent tools from langgraph-supervisor and langgraph-swarm.

python
import os
import json
from typing import Annotated, Literal, TypedDict

from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, SystemMessage, AIMessage
from langchain_core.tools import tool
from langgraph.graph import StateGraph, MessagesState, START, END
from langgraph.prebuilt import create_react_agent
from langgraph.types import Command
from langgraph_supervisor import create_supervisor
from langgraph_swarm import create_swarm, create_handoff_tool

os.environ["OPENAI_API_KEY"] = "your-api-key-here"

model = ChatOpenAI(model="gpt-4o-mini", temperature=0)

How Does the Supervisor Pattern Work?

Why would you want one agent calling the shots for all the others? Because some flows need a strict order. Picture a help desk where every ticket must be sorted before it goes to billing, tech support, or sales. A supervisor makes sure that happens.

Here is the data flow in plain text:

python
User → Supervisor → [Researcher | Writer | Fact-Checker] → Supervisor → ... → Final Answer

The supervisor always sits in the middle. Workers never chat with each other. Every message goes through the supervisor first.

Building the Worker Agents

Each worker needs its own tools and its own system prompt. We will make two simple ones: a researcher and a writer.

The @tool decorator turns a regular Python function into something LangGraph can use. Each tool gets a clear docstring so the LLM knows when to reach for it.

python
@tool
def search_web(query: str) -> str:
    """Search the web for information on a topic. Returns a summary of findings."""
    results = {
        "langgraph": "LangGraph is a framework for building stateful AI agents using graph-based workflows.",
        "multi-agent": "Multi-agent systems use multiple specialized AI agents that collaborate on complex tasks.",
    }
    for key, value in results.items():
        if key in query.lower():
            return value
    return f"Search results for '{query}': No specific results found. General AI topic."

@tool
def format_report(content: str, style: str = "markdown") -> str:
    """Format content into a structured report. Style can be 'markdown' or 'plain'."""
    if style == "markdown":
        return f"# Research Report\n\n{content}\n\n---\n*Generated by Writer Agent*"
    return f"REPORT\n{'='*40}\n{content}\n{'='*40}"

These tools are kept simple on purpose. In a real project, the search tool would call a live API. What matters here is the wiring, not the tools.

Wiring Up the Supervisor

create_supervisor from langgraph-supervisor does the bulk of the work. Hand it a list of workers and a model, and it builds a graph where the supervisor sends tasks to the right worker.

We make each worker with create_react_agent, giving it a short prompt and only the tools it needs. Then create_supervisor ties them into one managed flow. The output_mode="full" flag gives you the full chat history, not just the last reply.

python
research_agent = create_react_agent(
    model=model,
    tools=[search_web],
    prompt="You are a research specialist. Search for information and return factual summaries. Be thorough but concise.",
    name="researcher",
)

writer_agent = create_react_agent(
    model=model,
    tools=[format_report],
    prompt="You are a writing specialist. Take research findings and format them into clear, well-structured reports.",
    name="writer",
)

supervisor = create_supervisor(
    model=model,
    agents=[research_agent, writer_agent],
    prompt="You are a team supervisor. For research questions, delegate to the researcher first, then send findings to the writer for formatting. Always use both agents.",
    output_mode="full",
).compile()

Running the Supervisor

Calling the supervisor is like calling any LangGraph graph. Pass a message and get back the full chat, showing each agent’s work.

python
result = supervisor.invoke({
    "messages": [HumanMessage(content="Research what LangGraph is and write a short report about it.")]
})

for msg in result["messages"]:
    if hasattr(msg, "name"):
        print(f"[{msg.name}]: {msg.content[:100]}...")
    else:
        print(f"[{msg.type}]: {msg.content[:100]}...")

The output shows the supervisor sending the task to the researcher first, then passing the findings to the writer. Each worker only sees the job it was given. The supervisor glues the pieces together.

Tip: Keep the supervisor prompt about routing, not about the domain. The supervisor’s only job is to decide WHO does the work, not HOW. Domain know-how belongs in the worker prompts.

Quick check: What changes if you drop output_mode="full" from create_supervisor? You would only get the supervisor’s closing response — not the full chain of worker messages. The "full" mode is key for debugging because it shows every routing step.

exercise
{
  "type": "exercise",
  "id": "supervisor-ex1",
  "title": "Exercise 1: Add a Third Worker Agent",
  "difficulty": "intermediate",
  "exerciseType": "write",
  "instructions": "Add a fact-checker agent to the supervisor system above. The fact-checker should have a `verify_claim` tool that takes a claim string and returns whether it seems factual. Update the supervisor prompt to route: researcher → writer → fact_checker.",
  "starterCode": "@tool\ndef verify_claim(claim: str) -> str:\n    \"\"\"Verify whether a claim appears factual.\"\"\"\n    # TODO: return 'VERIFIED: <claim>' or 'UNVERIFIED: <claim>'\n    pass\n\nfact_checker = create_react_agent(\n    model=model,\n    tools=[verify_claim],\n    prompt=\"You are a fact-checker. Verify claims using the verify_claim tool.\",\n    name=\"fact_checker\",\n)\n\n# TODO: create supervisor with all three agents\nsupervisor_v2 = create_supervisor(\n    model=model,\n    agents=[research_agent, writer_agent, fact_checker],\n    prompt=\"# TODO: write routing prompt\",\n    output_mode=\"full\",\n).compile()",
  "testCases": [
    {"id": "tc1", "input": "print(len(supervisor_v2.nodes))", "expectedOutput": "5", "description": "Graph should have 5 nodes (supervisor + 3 workers + END)"},
    {"id": "tc2", "input": "print('fact_checker' in [n for n in supervisor_v2.nodes])", "expectedOutput": "True", "description": "fact_checker node should exist"}
  ],
  "hints": [
    "The verify_claim tool just needs to return a string — something like f'VERIFIED: {claim}' for the simulation.",
    "The supervisor prompt should say: 'Route to researcher first, then writer, then fact_checker. Always use all three agents in this order.'"
  ],
  "solution": "@tool\ndef verify_claim(claim: str) -> str:\n    \"\"\"Verify whether a claim appears factual.\"\"\"\n    return f'VERIFIED: {claim}'\n\nfact_checker = create_react_agent(\n    model=model,\n    tools=[verify_claim],\n    prompt=\"You are a fact-checker. Verify claims using the verify_claim tool.\",\n    name=\"fact_checker\",\n)\n\nsupervisor_v2 = create_supervisor(\n    model=model,\n    agents=[research_agent, writer_agent, fact_checker],\n    prompt=\"Route to researcher first, then writer, then fact_checker. Always use all three in this order.\",\n    output_mode=\"full\",\n).compile()",
  "solutionExplanation": "Adding a third agent follows the same pattern: create the agent with create_react_agent, give it a focused tool and prompt, then include it in the supervisor's agent list. The supervisor prompt must mention the new agent and its position in the routing sequence.",
  "xpReward": 20
}

How Does the Swarm Pattern Work — Agents Handing Off Without a Boss?

The swarm flips the supervisor model on its head. No central controller exists. Instead, each agent carries a hand-off tool that lets it pass control straight to another agent.

When does this beat a supervisor? When the flow branches in ways you cannot predict up front. Think of a support chatbot: a triage agent might send you to billing, billing might loop in a specialist, the specialist might send you back to triage. Encoding all those paths in one supervisor prompt gets ugly fast. With swarms, each agent only needs to know its neighbours.

How Hand-Offs Work Under the Hood

create_handoff_tool builds a tool that an agent calls to hand over control. When triggered, LangGraph updates the internal active_agent marker and sends the next turn to the target agent.

The key difference from a supervisor: the agent doing the hand-off picks where to go next. The decision is spread out, not locked in one place.

We make two agents — a triage agent and a specialist — each with a hand-off tool that points at the other. create_swarm fuses them into one graph.

python
triage_handoff = create_handoff_tool(
    agent_name="specialist",
    description="Hand off to the specialist for detailed technical questions about AI and ML.",
)

specialist_handoff = create_handoff_tool(
    agent_name="triage",
    description="Hand back to triage when the technical question is answered.",
)

triage_agent = create_react_agent(
    model=model,
    tools=[triage_handoff],
    prompt="You are a triage agent. Classify the user's question. For technical AI/ML questions, hand off to the specialist. For general questions, answer directly.",
    name="triage",
)

specialist_agent = create_react_agent(
    model=model,
    tools=[specialist_handoff, search_web],
    prompt="You are an AI/ML specialist. Answer technical questions thoroughly. When done, hand back to triage.",
    name="specialist",
)

swarm = create_swarm(
    agents=[triage_agent, specialist_agent],
    default_active_agent="triage",
).compile(recursion_limit=25)
Warning: Two agents that keep handing off to each other will loop forever. Always write clear rules in each agent’s prompt for WHEN to hand off versus WHEN to answer. Set `recursion_limit` as a safety net so an infinite loop does not eat your budget.

Running the Swarm

Triage goes first (it is the default_active_agent). If the message looks technical, triage uses its hand-off tool to send control to the specialist.

python
result = swarm.invoke({
    "messages": [HumanMessage(content="Can you explain how attention mechanisms work in transformers?")]
})

for msg in result["messages"]:
    if hasattr(msg, "name") and msg.content:
        print(f"[{msg.name}]: {msg.content[:120]}...")

Triage sees a technical question and passes the baton to the specialist. No central brain made that call — triage decided on its own.

Think about it: What would happen if you sent “Hello, how are you?” to this swarm? Triage would answer directly because the question is not technical. No hand-off would fire. The specialist would never wake up. That is the beauty of swarms — agents only jump in when they are needed.

Supervisor vs. Swarm — A Side-by-Side Look

TraitSupervisorSwarm
Who decides the route?One central agentEach agent on its own
Best suited forSet sequencesBranching, organic flows
Who sees everything?The supervisorNo single agent
Bottleneck riskSupervisor is a single pointNo single chokepoint
How hard to debug?Easier — follow the supervisorHarder — trace hand-offs
Adding new agentsUpdate the supervisor promptDrop in a hand-off tool

Pick the supervisor when you need a locked-in order. Pick the swarm when the path depends on what the user asks.

exercise
{
  "type": "exercise",
  "id": "swarm-ex1",
  "title": "Exercise 2: Add a Third Agent to the Swarm",
  "difficulty": "intermediate",
  "exerciseType": "write",
  "instructions": "Add a 'billing' agent to the swarm. The triage agent should be able to hand off to either the specialist or the billing agent. The billing agent should be able to hand back to triage. Create the handoff tools and the billing agent, then compile a three-agent swarm.",
  "starterCode": "billing_handoff_from_triage = create_handoff_tool(\n    agent_name=\"billing\",\n    description=\"# TODO: describe when to hand off to billing\",\n)\n\nbilling_to_triage = create_handoff_tool(\n    agent_name=\"triage\",\n    description=\"Hand back to triage after billing question is resolved.\",\n)\n\n# Update triage agent to have both handoff tools\ntriage_v2 = create_react_agent(\n    model=model,\n    tools=[triage_handoff, billing_handoff_from_triage],\n    prompt=\"Classify questions. Technical → specialist. Billing → billing agent. General → answer directly.\",\n    name=\"triage\",\n)\n\nbilling_agent = create_react_agent(\n    model=model,\n    tools=[billing_to_triage],\n    prompt=\"# TODO: billing agent prompt\",\n    name=\"billing\",\n)\n\nswarm_v2 = create_swarm(\n    agents=[triage_v2, specialist_agent, billing_agent],\n    default_active_agent=\"triage\",\n).compile(recursion_limit=25)",
  "testCases": [
    {"id": "tc1", "input": "print('billing' in [n for n in swarm_v2.nodes])", "expectedOutput": "True", "description": "billing node should exist in the swarm graph"},
    {"id": "tc2", "input": "print('triage' in [n for n in swarm_v2.nodes])", "expectedOutput": "True", "description": "triage node should still exist"}
  ],
  "hints": [
    "The billing handoff description should explain when triage should route there — e.g., 'Hand off to billing for payment, invoice, or subscription questions.'",
    "The billing agent prompt should focus on billing tasks: 'You are a billing specialist. Answer questions about payments, invoices, and subscriptions.'"
  ],
  "solution": "billing_handoff_from_triage = create_handoff_tool(\n    agent_name=\"billing\",\n    description=\"Hand off to billing for payment, invoice, or subscription questions.\",\n)\n\nbilling_to_triage = create_handoff_tool(\n    agent_name=\"triage\",\n    description=\"Hand back to triage after billing question is resolved.\",\n)\n\ntriage_v2 = create_react_agent(\n    model=model,\n    tools=[triage_handoff, billing_handoff_from_triage],\n    prompt=\"Classify questions. Technical → specialist. Billing → billing agent. General → answer directly.\",\n    name=\"triage\",\n)\n\nbilling_agent = create_react_agent(\n    model=model,\n    tools=[billing_to_triage],\n    prompt=\"You are a billing specialist. Answer questions about payments, invoices, and subscriptions. Hand back to triage when done.\",\n    name=\"billing\",\n)\n\nswarm_v2 = create_swarm(\n    agents=[triage_v2, specialist_agent, billing_agent],\n    default_active_agent=\"triage\",\n).compile(recursion_limit=25)",
  "solutionExplanation": "Adding agents to a swarm means: (1) create handoff tools connecting the new agent to existing agents, (2) update existing agents' tool lists to include the new handoff, (3) add the new agent to create_swarm. Each agent only needs handoff tools for its direct neighbors.",
  "xpReward": 20
}
Key Insight: Supervisors give you control. Swarms give you freedom. Most teams begin with a supervisor and only move to a swarm when the routing rules outgrow a single prompt.

How Does the Network Pattern Let Any Agent Call Any Other?

The network pattern is the most open layout — and the riskiest. Any agent can reach any other agent at any time. There is no ladder and no preset hand-off path.

You build it yourself with LangGraph’s Command object. There is no create_network helper because the wiring is fully up to you.

When does this make sense? When agents need to team up on the fly. A group of analysts where any one might need input from another — that is a network. Just keep in mind: networks are tough to trace.

Building a Network with Command

Command lets agents pick their next hop at runtime. Each node returns a Command that holds both the state update and the name of the next node. The goto field drives all routing.

Below we build three agents: a planner that lays out a plan, an executor that carries it out, and a reviewer that checks the result. The reviewer can approve and exit, or send the work back to the executor for a redo.

python
class NetworkState(TypedDict):
    messages: Annotated[list, "messages"]
    current_agent: str
    task_status: str

def planner_node(state: NetworkState) -> Command:
    """Plans the approach for a given task."""
    messages = state["messages"]
    response = model.invoke([
        SystemMessage(content="You are a planner. Create a brief plan for the task. Respond with the plan only."),
        *messages
    ])
    return Command(
        update={"messages": [response], "current_agent": "planner", "task_status": "planned"},
        goto="executor"
    )

The planner always sends work to the executor. But the executor and reviewer route on the fly — the reviewer decides whether to approve or ask for changes.

python
def executor_node(state: NetworkState) -> Command:
    """Executes the plan and produces results."""
    messages = state["messages"]
    response = model.invoke([
        SystemMessage(content="You are an executor. Follow the plan and produce a concise result."),
        *messages
    ])
    return Command(
        update={"messages": [response], "current_agent": "executor", "task_status": "executed"},
        goto="reviewer"
    )

def reviewer_node(state: NetworkState) -> Command:
    """Reviews the result and either approves or sends back."""
    messages = state["messages"]
    response = model.invoke([
        SystemMessage(content="You are a reviewer. If acceptable, say 'APPROVED: ' followed by the result. Otherwise say 'REVISION NEEDED: ' with feedback."),
        *messages
    ])
    content = response.content
    update = {"messages": [response], "current_agent": "reviewer"}
    if "APPROVED" in content.upper():
        update["task_status"] = "approved"
        return Command(update=update, goto=END)
    else:
        update["task_status"] = "needs_revision"
        return Command(update=update, goto="executor")

The reviewer’s branch creates a feedback loop. It either exits the graph or bounces back to the executor. This is a strong pattern for quality-control flows.

Putting the Network Graph Together

We wire the nodes with StateGraph. We only need one entry edge from START to planner — after that, Command objects handle all routing at runtime.

python
network_graph = StateGraph(NetworkState)
network_graph.add_node("planner", planner_node)
network_graph.add_node("executor", executor_node)
network_graph.add_node("reviewer", reviewer_node)
network_graph.add_edge(START, "planner")

network = network_graph.compile()

result = network.invoke({
    "messages": [HumanMessage(content="Write a two-sentence summary of what multi-agent systems are.")],
    "current_agent": "",
    "task_status": "pending"
})

print(f"Final status: {result['task_status']}")
print(f"Final message: {result['messages'][-1].content[:200]}")

The reviewer might approve on the first pass, or kick the work back two or three times. You cannot predict the path ahead of time — that is both the strength and the danger.

Note: The network pattern trades set paths for flexibility. Use it only when you truly cannot map out the agent order at design time. For most shipped systems, a supervisor or swarm is the safer bet.
exercise
{
  "type": "exercise",
  "id": "network-ex1",
  "title": "Exercise 3: Add a Maximum Revision Count",
  "difficulty": "advanced",
  "exerciseType": "write",
  "instructions": "The current network pattern can loop between executor and reviewer indefinitely. Modify the NetworkState to include a `revision_count` integer. Update the reviewer_node to increment this count on each revision and force approval after 3 revisions (to prevent infinite loops).",
  "starterCode": "class NetworkStateV2(TypedDict):\n    messages: Annotated[list, \"messages\"]\n    current_agent: str\n    task_status: str\n    revision_count: int  # NEW: track revision count\n\ndef reviewer_node_v2(state: NetworkStateV2) -> Command:\n    messages = state[\"messages\"]\n    count = state.get(\"revision_count\", 0)\n    response = model.invoke([\n        SystemMessage(content=\"If acceptable, say 'APPROVED: ' with result. Otherwise 'REVISION NEEDED: ' with feedback.\"),\n        *messages\n    ])\n    content = response.content\n    update = {\"messages\": [response], \"current_agent\": \"reviewer\"}\n    # TODO: Force approval if revision_count >= 3\n    # TODO: Otherwise, check for APPROVED/REVISION NEEDED as before\n    # TODO: Increment revision_count on revision\n    pass",
  "testCases": [
    {"id": "tc1", "input": "state = {'messages': [], 'current_agent': 'reviewer', 'task_status': 'executed', 'revision_count': 3}\nresult = reviewer_node_v2(state)\nprint(result.goto)", "expectedOutput": "__end__", "description": "Should force approval after 3 revisions"},
    {"id": "tc2", "input": "print(type(NetworkStateV2.__annotations__['revision_count']).__name__)", "expectedOutput": "type", "description": "revision_count should be an int type"}
  ],
  "hints": [
    "Check `if count >= 3:` at the top of the function and force an APPROVED Command with goto=END regardless of the LLM's response.",
    "On revision, set revision_count to count + 1 in the update dict: update['revision_count'] = count + 1"
  ],
  "solution": "def reviewer_node_v2(state: NetworkStateV2) -> Command:\n    messages = state[\"messages\"]\n    count = state.get(\"revision_count\", 0)\n    response = model.invoke([\n        SystemMessage(content=\"If acceptable, say 'APPROVED: ' with result. Otherwise 'REVISION NEEDED: ' with feedback.\"),\n        *messages\n    ])\n    content = response.content\n    update = {\"messages\": [response], \"current_agent\": \"reviewer\"}\n    if count >= 3:\n        update[\"task_status\"] = \"approved\"\n        update[\"revision_count\"] = count\n        return Command(update=update, goto=END)\n    if \"APPROVED\" in content.upper():\n        update[\"task_status\"] = \"approved\"\n        update[\"revision_count\"] = count\n        return Command(update=update, goto=END)\n    else:\n        update[\"task_status\"] = \"needs_revision\"\n        update[\"revision_count\"] = count + 1\n        return Command(update=update, goto=\"executor\")",
  "solutionExplanation": "Adding a revision_count to the state and checking it in the reviewer prevents infinite loops. After 3 revisions, the reviewer forces approval regardless of the LLM's judgment. This is a common production safety pattern for any network graph with feedback loops.",
  "xpReward": 20
}

How Should Agents Share State?

How much should each agent know? That is the core question.

LangGraph gives you two ways to handle it. Shared state means all agents read from and write to the same channels. That is the default with create_supervisor and create_swarm. Isolated state uses subgraphs with their own schemas — each agent keeps private data locked away from the rest.

Here is a quick demo of isolated state. The parent sends a task to a sub-agent. The agent does its work but only returns the final answer. Its scratch notes stay hidden.

python
class AgentInternalState(TypedDict):
    task: str
    internal_notes: str
    result: str

def isolated_agent_node(parent_state: MessagesState) -> dict:
    """Runs an agent with isolated internal state."""
    last_message = parent_state["messages"][-1].content

    # Agent's internal state — invisible to parent graph
    internal = {"task": last_message, "internal_notes": "", "result": ""}

    response = model.invoke([
        SystemMessage(content="Analyze the task and provide a concise result."),
        HumanMessage(content=last_message)
    ])

    # Only the result goes back — internal notes stay private
    return {"messages": [AIMessage(content=response.content, name="isolated_agent")]}

The parent only gets the final AIMessage. The agent’s inner notes never leak to other agents. This matters when agents handle private data or when one agent’s chatty output would crowd another’s context.

Tip: Use shared state as your starting point. Isolation adds moving parts. Reach for it only when you have a real need: private data, prompt-injection risks, or verbose middle steps that would crowd another agent’s view.

Real-World Example: A Multi-Agent Research Team

Let us tie everything together in a practical project. We will build a research team with three agents: a planner, a researcher, and a writer. We use the supervisor pattern because we want a fixed order: plan first, research second, write last.

Each agent has exactly one job and the tools to match. No tool overlap — that is what makes agents worth having.

python
@tool
def create_outline(topic: str) -> str:
    """Create a research outline with key questions to investigate."""
    return f"Research Outline for '{topic}':\n1. Definition and core concepts\n2. Key applications\n3. Current limitations\n4. Future directions"

@tool
def gather_sources(question: str) -> str:
    """Gather information from sources about a specific question."""
    return f"Source findings for '{question}': Multiple authoritative sources confirm this is an active area of research with significant recent developments."

@tool
def write_section(outline_point: str, research: str) -> str:
    """Write a polished section based on an outline point and research."""
    return f"## {outline_point}\n\n{research}\n\nThis section synthesizes findings from multiple sources."

The supervisor prompt lays out the exact order: planner first, then researcher, then writer. Clear rules stop the supervisor from skipping a step or calling agents in the wrong order.

python
planner_agent = create_react_agent(
    model=model,
    tools=[create_outline],
    prompt="You are a research planner. Given a topic, create a structured outline. Return the outline only.",
    name="planner",
)

researcher_agent = create_react_agent(
    model=model,
    tools=[gather_sources],
    prompt="You are a researcher. Investigate each outline point by gathering information. Return findings by outline point.",
    name="researcher",
)

writer_agent_team = create_react_agent(
    model=model,
    tools=[write_section],
    prompt="You are a report writer. Write a polished report from the research findings using the write_section tool.",
    name="writer",
)

research_team = create_supervisor(
    model=model,
    agents=[planner_agent, researcher_agent, writer_agent_team],
    prompt=(
        "You manage a research team. For any research request: "
        "1) Send it to the planner first to create an outline. "
        "2) Send the outline to the researcher to gather information. "
        "3) Send the findings to the writer for the final report. "
        "Always follow this exact sequence."
    ),
    output_mode="full",
).compile()
python
result = research_team.invoke({
    "messages": [HumanMessage(content="Research the current state of multi-agent AI systems and write a brief report.")]
})

for msg in result["messages"]:
    if hasattr(msg, "name") and msg.name and msg.content:
        preview = msg.content[:150].replace("\n", " ")
        print(f"\n[{msg.name}]: {preview}...")

The supervisor sends work to the planner, then the researcher, then the writer — the exact order we told it to follow.

[UNDER THE HOOD]
How does the supervisor pick which agent goes next? It relies on the LLM. The supervisor prompt lists the workers. The model’s tool-call skill picks which one to run. Good routing comes from a clear prompt — vague wording leads to random choices.

How Do You Pick the Right Pattern?

Picking a layout is a design choice based on the shape of your task, not a style thing.

Go with Supervisor when:
– The workflow has a known order (plan, run, review)
– You need central logging, auditing, or rate limits
– Easy debugging matters more than flexibility

Go with Swarm when:
– The flow branches based on the input (triage to billing, support, or sales)
– Agents are peers, not subordinates
– You want to add agents without touching a central prompt

Go with Network when:
– Any agent might need any other agent at any time
– The sequence truly cannot be laid out up front
– You accept the extra debugging work

My advice: start with a supervisor for most projects. It is the easiest to reason about. Move to a swarm once the supervisor prompt gets unwieldy. Save the network for cases where the agents genuinely need to team up on the fly.

When Are Multi-Agent Systems Not Worth the Trouble?

More agents means more moving parts. Sometimes one well-prompted agent is all you need.

Skip multi-agent when:
– Your job fits in one prompt with two or three tools. The overhead is not justified.
– Speed matters a lot. Each agent hop adds an LLM call. A three-agent supervisor means four or more LLM calls — four times the wait and the cost.
– Your “agents” are really just one function after another with no decisions to make. That is a pipeline, not an agent system.

Known limits to keep in mind:
Cost. Every agent step is an LLM call. Supervisor patterns add extra calls for routing. A three-agent supervisor can cost four to six times what a single agent costs.
Snowball errors. If Agent A gives bad output, Agent B builds on it. By the time you see the problem, the damage runs deep. You need checks between agents.
Growing context. The shared chat gets longer with every agent turn. Long threads can burst the context limit. Trim or sum up state between agents when needed.

Warning: Do not use multi-agent because it sounds fancy. Use it because your single-agent setup is clearly failing. The simplest design that gets the job done is the right one.

Common Mistakes and How to Avoid Them

Mistake 1: Putting domain knowledge in the supervisor prompt

python
# Wrong — supervisor tries to answer on its own
supervisor = create_supervisor(
    model=model,
    agents=[researcher, writer],
    prompt="You are an expert in AI research. When users ask about transformers, explain attention first...",
)

The problem: The supervisor answers the question itself instead of handing it to a worker.

python
# Right — supervisor only routes
supervisor = create_supervisor(
    model=model,
    agents=[researcher, writer],
    prompt="Route research questions to the researcher. Route writing tasks to the writer. Never answer directly.",
)

Mistake 2: No recursion limit on swarms

python
# Wrong — no safety net
swarm = create_swarm(agents=[agent_a, agent_b]).compile()

# Right — cap the loop depth
swarm = create_swarm(agents=[agent_a, agent_b]).compile(recursion_limit=25)

Why this matters: Without a cap, agents that volley the baton back and forth will loop until your credits run out.

Mistake 3: Sharing everything when some agents need privacy

Wrong: Feeding the full chat history to every agent, including private data from other agents’ tool calls.

Right: Use isolated subgraphs when agents handle sensitive data or when one agent’s chatty output would muddy another agent’s context. See the state-sharing section above for how.

[BEST PRACTICE]
Turn on LangSmith tracing for every multi-agent project. Set LANGSMITH_API_KEY and LANGSMITH_TRACING=true. Debugging agents without traces is blind guesswork. With traces, you see every routing choice, every tool call, and every agent reply on one screen.

Practice Exercise

Build a two-agent supervisor. The “translator” agent turns text into French. The “summarizer” agent writes a one-line English summary of what was translated.

Click to see the solution
python
@tool
def translate_to_french(text: str) -> str:
    """Translate English text to French."""
    return f"[French translation of: {text}]"

@tool
def summarize_text(text: str) -> str:
    """Write a one-sentence summary of the given text."""
    return f"Summary: The text discusses {text[:50]}..."

translator = create_react_agent(
    model=model,
    tools=[translate_to_french],
    prompt="Translate the given text to French using the translate tool.",
    name="translator",
)

summarizer = create_react_agent(
    model=model,
    tools=[summarize_text],
    prompt="Summarize the translated text in one English sentence.",
    name="summarizer",
)

pipeline = create_supervisor(
    model=model,
    agents=[translator, summarizer],
    prompt="Send text to the translator first, then send the translation to the summarizer.",
    output_mode="full",
).compile()

result = pipeline.invoke({
    "messages": [HumanMessage(content="Translate and summarize: Machine learning is transforming healthcare.")]
})

for msg in result["messages"]:
    if hasattr(msg, "name") and msg.content:
        print(f"[{msg.name}]: {msg.content[:100]}")

The supervisor sends the text to the translator, then passes the result to the summarizer. Each agent has one tool and one task.

Complete Code

Click to expand the full script (copy-paste and run)
python
# Complete code from: Multi-Agent Systems — Supervisor, Swarm, and Network Architectures
# Requires: pip install langgraph langgraph-supervisor langgraph-swarm langchain-openai langchain-core
# Python 3.10+

import os
import json
from typing import Annotated, Literal, TypedDict

from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, SystemMessage, AIMessage
from langchain_core.tools import tool
from langgraph.graph import StateGraph, MessagesState, START, END
from langgraph.prebuilt import create_react_agent
from langgraph.types import Command
from langgraph_supervisor import create_supervisor
from langgraph_swarm import create_swarm, create_handoff_tool

os.environ["OPENAI_API_KEY"] = "your-api-key-here"

model = ChatOpenAI(model="gpt-4o-mini", temperature=0)

# --- Tools ---

@tool
def search_web(query: str) -> str:
    """Search the web for information on a topic."""
    results = {
        "langgraph": "LangGraph is a framework for building stateful AI agents using graph-based workflows.",
        "multi-agent": "Multi-agent systems use multiple specialized AI agents that collaborate on complex tasks.",
    }
    for key, value in results.items():
        if key in query.lower():
            return value
    return f"Search results for '{query}': No specific results found."

@tool
def format_report(content: str, style: str = "markdown") -> str:
    """Format content into a structured report."""
    if style == "markdown":
        return f"# Research Report\n\n{content}\n\n---\n*Generated by Writer Agent*"
    return f"REPORT\n{'='*40}\n{content}\n{'='*40}"

@tool
def create_outline(topic: str) -> str:
    """Create a research outline with key questions."""
    return f"Research Outline for '{topic}':\n1. Definition and core concepts\n2. Key applications\n3. Current limitations\n4. Future directions"

@tool
def gather_sources(question: str) -> str:
    """Gather information from sources about a specific question."""
    return f"Source findings for '{question}': Active area of research with significant recent developments."

@tool
def write_section(outline_point: str, research: str) -> str:
    """Write a polished section based on research."""
    return f"## {outline_point}\n\n{research}\n\nSynthesized from multiple sources."

# --- Supervisor Pattern ---

research_agent = create_react_agent(
    model=model, tools=[search_web],
    prompt="You are a research specialist. Search and return factual summaries.",
    name="researcher",
)

writer_agent = create_react_agent(
    model=model, tools=[format_report],
    prompt="You are a writing specialist. Format findings into clear reports.",
    name="writer",
)

supervisor = create_supervisor(
    model=model, agents=[research_agent, writer_agent],
    prompt="Delegate research to the researcher, then send findings to the writer. Always use both.",
    output_mode="full",
).compile()

result = supervisor.invoke({
    "messages": [HumanMessage(content="Research what LangGraph is and write a short report.")]
})
for msg in result["messages"]:
    if hasattr(msg, "name"):
        print(f"[{msg.name}]: {msg.content[:100]}...")

# --- Swarm Pattern ---

triage_handoff = create_handoff_tool(
    agent_name="specialist",
    description="Hand off to the specialist for technical AI/ML questions.",
)
specialist_handoff = create_handoff_tool(
    agent_name="triage",
    description="Hand back to triage when the question is answered.",
)

triage_agent = create_react_agent(
    model=model, tools=[triage_handoff],
    prompt="Classify questions. Hand off technical AI/ML questions to the specialist.",
    name="triage",
)
specialist_agent = create_react_agent(
    model=model, tools=[specialist_handoff, search_web],
    prompt="Answer technical questions thoroughly. Hand back to triage when done.",
    name="specialist",
)

swarm = create_swarm(
    agents=[triage_agent, specialist_agent],
    default_active_agent="triage",
).compile(recursion_limit=25)

result = swarm.invoke({
    "messages": [HumanMessage(content="How do attention mechanisms work in transformers?")]
})
for msg in result["messages"]:
    if hasattr(msg, "name") and msg.content:
        print(f"[{msg.name}]: {msg.content[:120]}...")

# --- Network Pattern ---

class NetworkState(TypedDict):
    messages: Annotated[list, "messages"]
    current_agent: str
    task_status: str

def planner_node(state: NetworkState) -> Command:
    messages = state["messages"]
    response = model.invoke([
        SystemMessage(content="Create a brief plan for the task."),
        *messages
    ])
    return Command(
        update={"messages": [response], "current_agent": "planner", "task_status": "planned"},
        goto="executor"
    )

def executor_node(state: NetworkState) -> Command:
    messages = state["messages"]
    response = model.invoke([
        SystemMessage(content="Follow the plan and produce a concise result."),
        *messages
    ])
    return Command(
        update={"messages": [response], "current_agent": "executor", "task_status": "executed"},
        goto="reviewer"
    )

def reviewer_node(state: NetworkState) -> Command:
    messages = state["messages"]
    response = model.invoke([
        SystemMessage(content="If acceptable, say 'APPROVED: ' with the result. Otherwise 'REVISION NEEDED: ' with feedback."),
        *messages
    ])
    content = response.content
    update = {"messages": [response], "current_agent": "reviewer"}
    if "APPROVED" in content.upper():
        update["task_status"] = "approved"
        return Command(update=update, goto=END)
    else:
        update["task_status"] = "needs_revision"
        return Command(update=update, goto="executor")

network_graph = StateGraph(NetworkState)
network_graph.add_node("planner", planner_node)
network_graph.add_node("executor", executor_node)
network_graph.add_node("reviewer", reviewer_node)
network_graph.add_edge(START, "planner")
network = network_graph.compile()

result = network.invoke({
    "messages": [HumanMessage(content="Write a two-sentence summary of multi-agent systems.")],
    "current_agent": "", "task_status": "pending"
})
print(f"Final status: {result['task_status']}")
print(f"Final message: {result['messages'][-1].content[:200]}")

# --- Research Team ---

planner_agent = create_react_agent(
    model=model, tools=[create_outline],
    prompt="Create a structured research outline. Return the outline only.",
    name="planner",
)
researcher_agent = create_react_agent(
    model=model, tools=[gather_sources],
    prompt="Investigate each outline point by gathering source information.",
    name="researcher",
)
writer_agent_team = create_react_agent(
    model=model, tools=[write_section],
    prompt="Write a polished report from the research findings.",
    name="writer",
)

research_team = create_supervisor(
    model=model,
    agents=[planner_agent, researcher_agent, writer_agent_team],
    prompt="1) Planner creates outline. 2) Researcher gathers info. 3) Writer produces report. Follow this sequence.",
    output_mode="full",
).compile()

result = research_team.invoke({
    "messages": [HumanMessage(content="Research multi-agent AI systems and write a brief report.")]
})
for msg in result["messages"]:
    if hasattr(msg, "name") and msg.name and msg.content:
        print(f"\n[{msg.name}]: {msg.content[:150]}...")

print("\nScript completed successfully.")

Summary

Multi-agent systems in LangGraph solve the single-agent ceiling — noisy context, blurred roles, and overloaded prompts. The supervisor gives you central control with a clear order. The swarm spreads decisions through agent-to-agent hand-offs. The network offers the most room to move but is the toughest to debug. Shared state lets agents talk freely, while isolated subgraphs keep private data locked away. Start with a supervisor, move to swarm once routing gets tricky, and save network for truly open-ended teamwork.

Frequently Asked Questions

Can you mix supervisor and swarm in the same app?

Yes. A popular setup puts a supervisor at the top level while some of its workers are swarm-based subgraphs. For example, the supervisor routes to a “customer support” worker that inside uses swarm hand-offs between billing, tech, and sales agents. LangGraph lets you nest because every pattern compiles down to a standard graph.

How do you give multi-agent systems memory?

Pass a checkpointer when you compile: supervisor.compile(checkpointer=MemorySaver()). Each thread_id gets its own chat history. For memory that spans conversations, use a Store instance with langgraph-supervisor.

python
from langgraph.checkpoint.memory import MemorySaver
supervisor = create_supervisor(...).compile(checkpointer=MemorySaver())

How many agents can a supervisor handle well?

Things get shaky around 8 to 10 workers. The supervisor prompt must list every agent, and LLMs lose accuracy at tool picks past that count. For bigger setups, use a tree: a top-level supervisor routes to sub-supervisors that each manage 3 or 4 agents.

Do all agents have to use the same LLM?

Not at all. Each create_react_agent call takes its own model argument. Use GPT-4o for the supervisor (routing accuracy matters), GPT-4o-mini for workers (cost matters), and niche models for specialized tasks. Mixing models is a common way to cut costs.

What if an agent fails in production?

Wrap agent calls in error handling inside your node functions. Return fallback messages, retry with different inputs, or route to a backup agent. The retry and fallback patterns from the error-handling post in this series apply directly to multi-agent nodes.

References

  1. LangGraph Documentation — Multi-Agent Systems: https://langchain-ai.github.io/langgraph/concepts/multi_agent/
  2. langgraph-supervisor GitHub Repository: https://github.com/langchain-ai/langgraph-supervisor-py
  3. langgraph-swarm GitHub Repository: https://github.com/langchain-ai/langgraph-swarm-py
  4. LangChain Blog — Command: A New Tool for Multi-Agent Architectures: https://blog.langchain.com/command-a-new-tool-for-multi-agent-architectures-in-langgraph/
  5. LangChain Blog — Benchmarking Multi-Agent Architectures: https://blog.langchain.com/benchmarking-multi-agent-architectures/
  6. LangChain Documentation — Handoffs: https://docs.langchain.com/oss/python/langchain/multi-agent/handoffs
  7. LangChain Documentation — Subgraphs: https://docs.langchain.com/oss/python/langgraph/use-subgraphs
  8. Wu et al., “AutoGen: Enabling Next-Gen LLM Applications via Multi-Agent Conversation,” arXiv:2308.08155, 2023.
Free Course
Master Core Python — Your First Step into AI/ML

Build a strong Python foundation with hands-on exercises designed for aspiring Data Scientists and AI/ML Engineers.

Start Free Course
Trusted by 50,000+ learners
Related Course
Master Gen AI — Hands-On
Join 5,000+ students at edu.machinelearningplus.com
Explore Course
Free Callback - Limited Slots
Not Sure Which Course to Start With?
Talk to our AI Counsellors and Practitioners. We'll help you clear all your questions for your background and goals, bridging the gap between your current skills and a career in AI.
10-digit mobile number
📞
Thank You!
We'll Call You Soon!
Our learning advisor will reach out within 24 hours.
(Check your inbox too — we've sent a confirmation)
⚡ Before you go

Python.
SQL. NumPy.
All free.

Get the exact 10-course programming foundation that Data Science professionals use.

🐍
Core Python — from first line to expert level
📈
NumPy & Pandas — the #1 libraries every DS job needs
🗃️
SQL Levels I–III — basics to Window Functions
📄
Real industry data — Jupyter notebooks included
R A M S K
57,000+ students
★★★★★ Rated 4.9/5
⚡ Before you go
Python. SQL.
All Free.
R A M S K
57,000+ students  ★★★★★ 4.9/5
Get Free Access Now
10 courses. Real projects. Zero cost. No credit card.
New learners enrolling right now
🔒 100% free ☕ No spam, ever ✓ Instant access
🚀
You're in!
Check your inbox for your access link.
(Check Promotions or Spam if you don't see it)
Or start your first course right now:
Start Free Course →
Scroll to Top
Scroll to Top
Course Preview

Machine Learning A-Z™: Hands-On Python & R In Data Science

Free Sample Videos:

Machine Learning A-Z™: Hands-On Python & R In Data Science

Machine Learning A-Z™: Hands-On Python & R In Data Science

Machine Learning A-Z™: Hands-On Python & R In Data Science

Machine Learning A-Z™: Hands-On Python & R In Data Science

Machine Learning A-Z™: Hands-On Python & R In Data Science