How to implement the supervisor pattern in LangGraph — routing, subgraphs, and shared state

When a single agent is not enough

A single ReAct agent with many tools works well up to a point. Beyond that point — too many tools, too many responsibilities, too many steps — it starts making mistakes. The supervisor pattern is the most practical solution: a lightweight routing agent coordinates a team of specialist agents, each focused on a narrow task.

Two ways to implement supervisor in LangGraph

LangGraph supports two approaches:

  • langgraph-supervisor package: a thin convenience wrapper that builds the graph for you — best for straightforward setups
  • Manual implementation: build the supervisor as a regular node with conditional edges — necessary for custom routing logic

This guide covers both.

Option 1: langgraph-supervisor package

Note (mid-2026): the langgraph-supervisor package's own docs now say they “now recommend using the supervisor pattern directly via tools rather than this library for most use cases” (i.e. the manual approach in Option 2, using handoff tools), and that the package is being kept compatible with LangChain 1.0 mainly to help existing projects upgrade. The specialist agents below are still built with the now-deprecated create_react_agent (langgraph.prebuilt) in the package's official examples -- langchain.agents.create_agent is the current recommended agent constructor elsewhere.

pip install langgraph-supervisor
 
from langgraph_supervisor import create_supervisor
from langgraph.prebuilt import create_react_agent
from langchain_openai import ChatOpenAI
from langchain_core.tools import tool
 
llm = ChatOpenAI(model='gpt-4o')
 
# --- Specialist agents ---
 
@tool
def web_search(query: str) -> str:
    """Search the web for current information."""
    return f'Search results for: {query}'
 
@tool
def run_python(code: str) -> str:
    """Execute Python code and return the output."""
    try:
        exec_globals = {}
        exec(code, exec_globals)
        return str(exec_globals.get('result', 'Code executed.'))
    except Exception as e:
        return f'Error: {e}'
 
research_agent = create_react_agent(
    model=llm,
    tools=[web_search],
    name='research_agent',
    prompt='You are a research specialist. Use web_search to find information.'
)
 
code_agent = create_react_agent(
    model=llm,
    tools=[run_python],
    name='code_agent',
    prompt='You are a Python specialist. Write and execute code to solve problems.'
)
 
# --- Supervisor ---
 
supervisor = create_supervisor(
    agents=[research_agent, code_agent],
    model=llm,
    prompt=(
        'You are a supervisor managing a research agent and a code agent. '
        'Route tasks to the appropriate specialist. '
        'For information retrieval, use research_agent. '
        'For calculations or data processing, use code_agent.'
    ),
).compile()
 
# Run it
result = supervisor.invoke({
    'messages': [{'role': 'user', 'content': 'What is the population of Tokyo, and what is that squared?'}]
})
print(result['messages'][-1].content)
 

Option 2: Manual supervisor graph

When you need custom routing logic (e.g. confidence-based routing, priority queues, or complex state), build the supervisor manually.

from typing import TypedDict, Annotated, Literal
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
from langchain_core.messages import BaseMessage, HumanMessage
from langchain_openai import ChatOpenAI
 
llm = ChatOpenAI(model='gpt-4o')
 
class SupervisorState(TypedDict):
    messages: Annotated[list[BaseMessage], add_messages]
    next_agent: str
    research_output: str
    code_output: str
 
# Supervisor node: decides who to call next
def supervisor_node(state: SupervisorState) -> SupervisorState:
    system = (
        'You are a supervisor. Given the conversation, decide which agent should act next.\n'
        'Reply with ONLY ONE of: research_agent, code_agent, FINISH'
    )
    msgs = [{'role': 'system', 'content': system}] + state['messages']
    response = llm.invoke(msgs)
    next_agent = response.content.strip()
    return {'next_agent': next_agent}
 
# Route based on supervisor decision
def route(state: SupervisorState) -> Literal['research_agent', 'code_agent', END]:
    return state['next_agent'] if state['next_agent'] != 'FINISH' else END
 
# Specialist nodes
def research_node(state: SupervisorState) -> SupervisorState:
    result = research_agent.invoke(state)  # subgraph call
    return {'messages': result['messages'], 'research_output': result['messages'][-1].content}
 
def code_node(state: SupervisorState) -> SupervisorState:
    result = code_agent.invoke(state)
    return {'messages': result['messages'], 'code_output': result['messages'][-1].content}
 
# Build the graph
builder = StateGraph(SupervisorState)
builder.add_node('supervisor', supervisor_node)
builder.add_node('research_agent', research_node)
builder.add_node('code_agent', code_node)
builder.add_edge(START, 'supervisor')
builder.add_conditional_edges('supervisor', route)
builder.add_edge('research_agent', 'supervisor')
builder.add_edge('code_agent', 'supervisor')
graph = builder.compile()
 

Shared state vs message passing

Approach How it works Best for
Shared TypedDict state All agents read/write the same state object Pipelines where agents build on each other's outputs
Message passing (add_messages) Agents append to a messages list Conversational multi-agent flows
Subgraph with private state Each subgraph has its own state, results surface via output mapping Truly independent agents with different state shapes

Preventing infinite loops

Without a termination condition, a supervisor can loop indefinitely — agent A outputs to supervisor, supervisor routes back to agent A, repeat.
from langgraph.graph import StateGraph
 
# Add a step counter to prevent infinite loops
class SafeSupervisorState(TypedDict):
    messages: Annotated[list[BaseMessage], add_messages]
    next_agent: str
    steps: int  # track iterations
 
MAX_STEPS = 10
 
def safe_route(state: SafeSupervisorState):
    if state['steps'] >= MAX_STEPS:
        return END  # force termination
    return state['next_agent'] if state['next_agent'] != 'FINISH' else END
 
def supervisor_with_counter(state: SafeSupervisorState):
    # ... supervisor logic ...
    return {'next_agent': 'research_agent', 'steps': state.get('steps', 0) + 1}
 

When to use supervisor vs other patterns

Pattern Use when
Supervisor Tasks require dynamic routing to 2-5 specialists
Sequential (pipeline) Steps are always in the same fixed order
Parallel (map-reduce) Independent subtasks can run concurrently and merge
Single ReAct agent One agent with 3-5 tools handles everything fine
Hierarchical (supervisor of supervisors) Very large systems with 10+ agents across domains
Start with a single ReAct agent. Add a supervisor layer only when you have clear evidence that the agent is struggling because of tool overload or responsibility conflicts, not preemptively.