One-Line Summary: Each agent is a fully independent StateGraph with its own state schema, compiled separately and invoked as a single node inside a parent graph for maximum encapsulation and modularity.
Prerequisites: nodes.md, state-and-state-schema.md, graph-compilation.md
What Is Subgraph Architecture?
Think of a large corporation with autonomous departments. The marketing department has its own internal processes, roles, and documents that the engineering department never sees. When the CEO needs a marketing campaign, they send a brief to marketing and receive a finished deliverable. The CEO does not manage marketing's internal steps -- they just provide input and receive output.
In LangGraph, a subgraph is a complete StateGraph that is compiled independently and then added as a node in a parent graph. The subgraph can have an entirely different state schema from the parent. It maintains private internal state that the parent never sees. State transformation happens at the boundaries -- input is mapped from parent state into the subgraph's schema on entry, and results are mapped back on exit.
This architecture is essential when building complex multi-agent systems where each agent has domain-specific internal logic, private working memory, and a different data model. The parent graph only knows the agent's public interface, not its internals.
How It Works
Defining a Subgraph with Its Own Schema
from typing import TypedDict, Annotated
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
# The subgraph has its OWN state schema
class ResearchState(TypedDict):
topic: str
sources: list[str]
draft: str
internal_notes: list[str] # Private -- parent never sees this
def gather_sources(state: ResearchState):
return {"sources": ["arxiv.org/paper1", "arxiv.org/paper2"]}
def synthesize(state: ResearchState):
summary = f"Research on {state['topic']} from {len(state['sources'])} sources"
return {"draft": summary, "internal_notes": ["Used academic sources only"]}
research_builder = StateGraph(ResearchState)
research_builder.add_node("gather", gather_sources)
research_builder.add_node("synthesize", synthesize)
research_builder.add_edge(START, "gather")
research_builder.add_edge("gather", "synthesize")
research_builder.add_edge("synthesize", END)
research_graph = research_builder.compile()Embedding the Subgraph in a Parent Graph
class ParentState(TypedDict):
messages: Annotated[list, add_messages]
research_output: str
def call_research(state: ParentState) -> dict:
# Transform parent state into subgraph input
result = research_graph.invoke({"topic": state["messages"][-1].content})
# Transform subgraph output back to parent state
return {"research_output": result["draft"]}
parent_builder = StateGraph(ParentState)
parent_builder.add_node("research", call_research)
parent_builder.add_edge(START, "research")
parent_builder.add_edge("research", END)
parent_graph = parent_builder.compile()Overlapping Keys for Automatic State Mapping
When parent and subgraph share key names, LangGraph can map state automatically:
class ParentState(TypedDict):
messages: Annotated[list, add_messages]
topic: str
class SubState(TypedDict):
topic: str # Shared key -- mapped automatically
internal_data: str # Private key -- not visible to parent
sub_builder = StateGraph(SubState)
# ... add nodes and edges ...
sub_graph = sub_builder.compile()
parent_builder.add_node("sub_agent", sub_graph) # Direct embeddingWhy It Matters
- Encapsulation -- each agent's internal complexity is hidden behind a clean input/output boundary.
- Independent development -- teams can build, test, and iterate on subgraphs without touching the parent.
- Schema isolation -- subgraphs can track private fields like retry counts, intermediate drafts, or internal notes that would pollute the parent state.
- Reusability -- a compiled subgraph can be embedded in multiple parent graphs as a reusable component.
- Independent testing -- each subgraph can be invoked and tested in isolation with its own test data.
Key Technical Details
- A compiled subgraph is added via
builder.add_node("name", compiled_subgraph)-- it behaves like any other node. - Keys that exist in both parent and subgraph schemas are automatically mapped at the boundary.
- Keys that exist only in the subgraph schema remain private and invisible to the parent graph.
- Subgraphs can have their own checkpointers, enabling independent persistence of internal state.
- A wrapper function (as shown in
call_research) gives full control over the state transformation logic. - Subgraphs can be nested -- a subgraph can itself contain subgraphs for hierarchical architectures.
- The parent graph's streaming events include subgraph events, so observability is not lost.
- Each subgraph
invoke()call is a complete execution from START to END of that subgraph.
Common Misconceptions
- "Subgraphs must share the same state schema as the parent." Subgraphs can have completely different schemas. That is one of their primary advantages over plain nodes.
- "The parent can read the subgraph's internal state." Only keys shared between both schemas are visible to the parent. Private subgraph fields are inaccessible from the parent.
- "Subgraphs add significant performance overhead." A subgraph invocation is essentially a function call. The overhead is negligible compared to the LLM calls happening inside the subgraph.
- "You must use subgraphs for multi-agent systems." Simple multi-agent systems work fine with plain nodes. Subgraphs become valuable when agents need private state or independent schemas.
Connections to Other Concepts
supervisor-pattern.md-- supervisor systems often encapsulate each specialist as a subgraphagent-handoffs.md-- handoffs can route between subgraph-based agentsnodes.md-- subgraphs are registered as nodes, following the sameadd_nodeAPIgraph-compilation.md-- each subgraph is compiled independently before embeddingstate-and-state-schema.md-- schema design determines the boundary contract between parent and subgraphcheckpointers.md-- subgraphs can have independent checkpointer configurations