You already know Supervisor, Handoff, and Agents-as-Tools as real architectural patterns from your earlier coursework. This module doesn’t re-teach any of that theory. It does exactly one thing: shows you precisely how each pattern actually becomes real, working LangGraph code — and recall Module 22’s own closing promise, each “agent” here is genuinely just a subgraph, nothing more exotic than what you already know how to build.
Example 1: the smallest possible supervisor
from typing import TypedDict, Literal
from langgraph.graph import StateGraph, START, END
from langchain.chat_models import init_chat_model
class State(TypedDict):
query: str
result: str
model = init_chat_model("openai:gpt-4o-mini")
def research_agent(state: State) -> dict:
return {"result": f"Research findings for: {state['query']}"}
def writing_agent(state: State) -> dict:
return {"result": f"Written content for: {state['query']}"}
def supervisor(state: State) -> str:
return "research_agent" if "research" in state["query"].lower() else "writing_agent"
builder = StateGraph(State)
builder.add_node("research_agent", research_agent)
builder.add_node("writing_agent", writing_agent)
builder.add_conditional_edges(START, supervisor, ["research_agent", "writing_agent"])
builder.add_edge("research_agent", END)
builder.add_edge("writing_agent", END)
graph = builder.compile()
print(graph.invoke({"query": "Research the current state of RAG", "result": ""})["result"])
flowchart TD
START --> supervisor
supervisor -->|research query| research_agent
supervisor -->|writing task| writing_agent
supervisor -->|code request| coding_agent
research_agent --> END
writing_agent --> END
coding_agent --> END
Recognize this shape directly: this is genuinely Module 8’s own conditional routing, applied to a real, new context — instead of routing to different response nodes, you’re routing to different real agents. The supervisor pattern, at its smallest, is nothing more than that.
Example 2: a third specialist agent
def coding_agent(state: State) -> dict:
return {"result": f"Code written for: {state['query']}"}
def supervisor(state: State) -> str:
q = state["query"].lower()
if "research" in q:
return "research_agent"
if "code" in q or "function" in q:
return "coding_agent"
return "writing_agent"
builder = StateGraph(State)
builder.add_node("research_agent", research_agent)
builder.add_node("writing_agent", writing_agent)
builder.add_node("coding_agent", coding_agent)
builder.add_conditional_edges(START, supervisor, ["research_agent", "writing_agent", "coding_agent"])
builder.add_edge("research_agent", END)
builder.add_edge("writing_agent", END)
builder.add_edge("coding_agent", END)
graph = builder.compile()
print(graph.invoke({"query": "Write a function to reverse a string", "result": ""})["result"])
Genuinely nothing structurally new — one more specialist, one more real branch in the supervisor’s own routing logic.
Example 3: adding genuinely shared state
Real multi-agent systems need specialists to see each other’s work, not operate in isolation.
from typing import Annotated
import operator
class State(TypedDict):
query: str
findings: Annotated[list[str], operator.add]
final_output: str
def research_agent(state: State) -> dict:
return {"findings": [f"Research: found 3 relevant papers on '{state['query']}'"]}
def writing_agent(state: State) -> dict:
context = " | ".join(state["findings"])
return {"final_output": f"Article draft using: {context}"}
builder = StateGraph(State)
builder.add_node("research_agent", research_agent)
builder.add_node("writing_agent", writing_agent)
builder.add_edge(START, "research_agent")
builder.add_edge("research_agent", "writing_agent")
builder.add_edge("writing_agent", END)
graph = builder.compile()
result = graph.invoke({"query": "RAG techniques", "findings": [], "final_output": ""})
print(result["final_output"])
Recall Annotated[list[str], operator.add] directly from Module 11 — the exact same reducer mechanism, now genuinely letting one specialist’s real output become visible input for the next. writing_agent never has to be told what research_agent found; it reads it straight from shared state.
Example 4: real supervisor routing, based on genuine model reasoning
from pydantic import BaseModel
from typing import Literal
class RoutingDecision(BaseModel):
next_agent: Literal["research_agent", "writing_agent", "coding_agent"]
routing_model = init_chat_model("openai:gpt-4o-mini").with_structured_output(RoutingDecision)
def supervisor(state: State) -> str:
decision = routing_model.invoke(
f"Which specialist should handle this request: '{state['query']}'? "
"Choose research_agent, writing_agent, or coding_agent."
)
return decision.next_agent
Recall structured output from your LangChain course, and Module 8’s own “computation belongs in a node” principle — here, the supervisor’s real routing decision genuinely comes from a model’s own reasoning, constrained to a valid set of choices via Literal, rather than simple keyword matching. This is the real, honest difference between a toy router and a genuinely intelligent one.
Example 5: human approval before a specialist executes
Recall Module 19’s interrupt() — it composes directly into a multi-agent system, exactly like any other node.
from langgraph.types import interrupt, Command
from langgraph.checkpoint.memory import InMemorySaver
class RefundState(TypedDict):
query: str
amount: float
result: str
def refund_agent(state: RefundState) -> dict:
if state["amount"] > 200:
approved = interrupt({"question": f"Approve ${state['amount']} refund from refund_agent?"})
if not approved:
return {"result": "Refund denied by reviewer."}
return {"result": f"Refund of ${state['amount']} processed."}
builder = StateGraph(RefundState)
builder.add_node("refund_agent", refund_agent)
builder.add_edge(START, "refund_agent")
builder.add_edge("refund_agent", END)
graph = builder.compile(checkpointer=InMemorySaver())
config = {"configurable": {"thread_id": "multi-agent-refund"}}
graph.invoke({"query": "refund my order", "amount": 300.0, "result": ""}, config=config)
result = graph.invoke(Command(resume=True), config=config)
print(result["result"])
Nothing about interrupt() needed to change for a multi-agent context — a specialist agent pausing for human review is genuinely just a node that happens to call interrupt(), exactly like any single-agent workflow from Module 19.
The Handoff pattern: agents deciding to transfer control directly
Supervisor routing decides before an agent runs. Handoff is genuinely different — an agent, mid-execution, decides another agent should take over.
from langgraph.types import Command
from typing import Literal
def research_agent(state: State) -> Command[Literal["writing_agent"]]:
findings = f"Research complete on '{state['query']}'"
return Command(update={"findings": [findings]}, goto="writing_agent")
builder = StateGraph(State)
builder.add_node("research_agent", research_agent)
builder.add_node("writing_agent", writing_agent)
builder.add_edge(START, "research_agent")
builder.add_edge("writing_agent", END)
graph = builder.compile()
Recall Command from Module 9 — this is genuinely the same mechanism, now used for one specialist agent to directly hand off to the next, without a central supervisor mediating every single transition. This is the real, structural difference between Supervisor (one central router deciding every hop) and Handoff (agents deciding to transfer control themselves, as they finish their own work).
Agents-as-tools: a genuinely different architecture
Instead of specialists as graph nodes, an entire specialist agent can be exposed as a single tool to one primary agent — recall Module 15’s own “an agent is just a graph” lesson, now composed one level deeper.
from langchain.tools import tool
from langgraph.prebuilt import create_react_agent
research_specialist = create_react_agent(model=init_chat_model("openai:gpt-4o-mini"), tools=[])
@tool
def do_research(query: str) -> str:
"""Delegate a research task to the research specialist agent."""
result = research_specialist.invoke({"messages": [{"role": "user", "content": query}]})
return result["messages"][-1].content
primary_agent = create_react_agent(model=init_chat_model("openai:gpt-4o-mini"), tools=[do_research])
result = primary_agent.invoke({"messages": [{"role": "user", "content": "Research RAG techniques for me"}]})
print(result["messages"][-1].content)
Notice do_research is a genuine tool, exactly like every tool from Module 14 — it simply happens to internally invoke an entire, separate agent rather than a plain function. The primary agent never sees research_specialist’s own internal reasoning or tool calls directly; it only sees this tool’s final, real output, exactly like any other tool result.
The real, prebuilt shortcut: langgraph-supervisor
from langgraph_supervisor import create_supervisor
from langgraph.prebuilt import create_react_agent
from langchain.chat_models import init_chat_model
model = init_chat_model("openai:gpt-4o-mini")
research_agent = create_react_agent(model=model, tools=[], name="research_agent")
writing_agent = create_react_agent(model=model, tools=[], name="writing_agent")
supervisor_graph = create_supervisor(
agents=[research_agent, writing_agent],
model=model,
).compile()
result = supervisor_graph.invoke({"messages": [{"role": "user", "content": "Research and write about RAG"}]})
print(result["messages"][-1].content)
create_supervisor, from the real, official langgraph-supervisor package, builds genuinely the same real architecture as Examples 1 through 4 — a real router deciding which specialist handles a request — with the model-based routing and message-passing conventions already wired in for you.
Why intelligent, selective routing is a genuine, documented production concern
It’s worth grounding the supervisor pattern’s real value directly, since “route to the right specialist” can sound like a purely academic concern. Walmart Global Tech built a real, documented conversational career-recommendation system, AdaptJobRec, and found that applying full agentic reasoning to every single request introduced real, unnecessary latency for simple queries — while a simpler, non-agentic approach genuinely struggled with more complex ones. Their real, working solution was exactly the pattern this module has built from Example 1 onward: applying agentic reasoning selectively, routing each real request to the level of reasoning it actually needs, rather than treating every request identically. This is precisely why Example 4’s supervisor uses genuine model-based reasoning to route, rather than sending every request through every specialist regardless of whether it’s actually needed.
Common mistakes worth avoiding
Building a multi-agent system when one well-tooled agent would genuinely suffice. Recall this course’s own recurring theme since Module 12 — “does this really need this abstraction.” A single agent with several tools, from your LangChain course, is often simpler and more directly debuggable than a multi-agent system, unless the specialists genuinely need separate models, separate prompts, or separate reasoning contexts.
Confusing Supervisor with Handoff and implementing the wrong one. Recall the real, structural difference — Supervisor centralizes every routing decision in one place; Handoff distributes that decision to the agents themselves. Building a “supervisor” where every agent secretly decides its own next step, without the supervisor’s real involvement, is actually Handoff wearing the wrong name.
Letting shared state grow without deliberate reducers. Recall Example 3 and Module 11’s own core lesson — multiple specialist agents writing to the same field without a real reducer produces exactly the same silent collision Module 11 first exposed.
What you should take away from this module
- Supervisor is genuinely Module 8’s conditional routing, applied to whole agents instead of simple response nodes.
- Shared state between specialists uses the exact reducer mechanism from Module 11 — no new concept required.
- Handoff uses
Commandfrom Module 9, letting an agent transfer control directly rather than routing through a central supervisor. - Agents-as-tools wraps an entire specialist agent as a single tool, exactly like Module 14’s tool pattern, just with an agent behind it instead of a plain function.
langgraph-supervisor’screate_supervisoris the real, official shortcut for the standard supervisor shape.
Where this goes next
The next module goes deeper on Parallel Execution at real, production depth — building directly on Module 10’s Send mechanism, now facing genuine, real trade-offs: partial failure, rate limits, latency, and token cost across many concurrent branches.
- Author
- TechByteByByte Editorial Team
- Reviewed by
- TechByteByByte Admin
- Published
- Last reviewed