Subgraphs: Composition and Nesting

This chapter covers subgraphs: composing graphs as nodes, sharing state across boundaries, and the patterns that keep nesting manageable.

Why Subgraphs

Your graph has 15 nodes. Half of them are a "research phase" (plan, search, rank, summarize). The other half are a "writing phase" (draft, review, edit, publish). As the graph grows, reading it becomes harder.

Subgraphs let you package part of a graph as a single unit and use it as a node in a larger graph. The larger graph now has "research" and "writing" as single nodes; the details live inside each subgraph.

Benefits:

  • Readability. The top-level graph shows the shape, not the steps.
  • Reuse. The research subgraph can be used in three different top-level graphs.
  • Encapsulation. A subgraph's internals can change without breaking the parent.

A Subgraph Is a Compiled Graph

You build and compile a subgraph exactly like a top-level graph. Then you add it as a node.

from typing import TypedDict
from langgraph.graph import StateGraph, START, END

# Subgraph: does research
class ResearchState(TypedDict):
    topic: str
    findings: list[str]

def search(state: ResearchState) -> ResearchState:
    return {"findings": state["findings"] + [f"web result for {state['topic']}"]}

def summarize(state: ResearchState) -> ResearchState:
    return {"findings": [f"summary: {', '.join(state['findings'])}"]}

research_subgraph = (
    StateGraph(ResearchState)
    .add_node("search", search)
    .add_node("summarize", summarize)
    .add_edge(START, "search")
    .add_edge("search", "summarize")
    .add_edge("summarize", END)
    .compile()
)

Now use it as a node in a larger graph:

class MainState(TypedDict):
    topic: str
    findings: list[str]
    draft: str

def write_draft(state: MainState) -> MainState:
    return {"draft": f"Article on {state['topic']}: {state['findings']}"}

main_graph = (
    StateGraph(MainState)
    .add_node("research", research_subgraph)   # subgraph as a node
    .add_node("write", write_draft)
    .add_edge(START, "research")
    .add_edge("research", "write")
    .add_edge("write", END)
    .compile()
)

Shared State Keys

When MainState and ResearchState share keys (topic, findings), LangGraph passes those keys through. MainState's topic is visible to the subgraph; the subgraph's updates to findings are visible to MainState.

Keys that aren't shared are invisible to the other side. MainState's draft doesn't exist in the subgraph.

This is the default, and usually what you want: the subgraph treats its parent's state as its own, for the keys it cares about.

Different State Schemas

Sometimes the subgraph has its own internal keys that shouldn't leak. Use different state schemas and translate at the boundary.

class MainState(TypedDict):
    topic: str
    final_report: str

class SubState(TypedDict):
    query: str
    results: list[str]
    ranked: list[str]
    summary: str

def translate_to_sub(state: MainState) -> SubState:
    return {"query": state["topic"], "results": [], "ranked": [], "summary": ""}

def translate_from_sub(state: SubState) -> MainState:
    return {"final_report": state["summary"]}

Wire them up manually:

def research_node(state: MainState) -> MainState:
    sub_input = translate_to_sub(state)
    sub_output = research_subgraph.invoke(sub_input)
    return translate_from_sub(sub_output)

main_graph.add_node("research", research_node)

More explicit, more boilerplate, but the subgraph's internals don't pollute the parent.

Streaming Through Subgraphs

When you stream the parent, subgraph events are included. By default, you get parent-level updates.

To see subgraph internals:

async for chunk in main_graph.astream(input, subgraphs=True):
    print(chunk)

With subgraphs=True, chunks include subgraph namespace info so you can tell which graph emitted which event. Useful for deep debugging.

Persistence with Subgraphs

The parent's checkpointer persists subgraph state too. Subgraphs share the parent's thread. You don't configure a separate checkpointer for subgraphs; it just works.

Interrupts inside a subgraph pause the whole graph. Command(resume=...) from the parent's caller resumes the subgraph.

Common Patterns

Two-Phase Work

Research phase, then writing phase. Each is a subgraph; the parent orchestrates.

Tool-Heavy Subtask

Main graph is a chat loop. One branch leads to a subgraph that does a complex multi-step task (booking a flight: search, compare, reserve, confirm). The main graph doesn't need to know every step.

Reusable Modules

A "research" subgraph used by five different top-level graphs. Build once, reuse.

When Not to Subgraph

  • Two or three nodes. Not worth the wrapping. Just put them in the parent.
  • Tight coupling. If the subgraph reads 10 keys from parent state and writes 10 back, it's not really encapsulated; it's just noise.
  • Debugging cost. A subgraph adds one layer. Four levels of nesting makes debugging painful. Keep nesting shallow (one or two levels at most).

A Bigger Example

Two-phase research and writing:

from typing import TypedDict, Annotated
from langchain_core.messages import BaseMessage
from langgraph.graph.message import add_messages

class MainState(TypedDict):
    messages: Annotated[list[BaseMessage], add_messages]
    topic: str
    findings: list[str]
    draft: str

# (Assume research_subgraph from above.)

# Writing subgraph
class WritingState(TypedDict):
    findings: list[str]
    draft: str

def write_initial(state: WritingState):
    return {"draft": f"Initial draft on {state['findings']}"}

def revise(state: WritingState):
    return {"draft": f"Revised: {state['draft']}"}

writing_subgraph = (
    StateGraph(WritingState)
    .add_node("write", write_initial)
    .add_node("revise", revise)
    .add_edge(START, "write")
    .add_edge("write", "revise")
    .add_edge("revise", END)
    .compile()
)

# Main graph
main = (
    StateGraph(MainState)
    .add_node("research", research_subgraph)
    .add_node("writing", writing_subgraph)
    .add_edge(START, "research")
    .add_edge("research", "writing")
    .add_edge("writing", END)
    .compile()
)

The main graph has two nodes. Each does a lot. When you read the main graph, you see the shape; when you need to modify research, you open the research subgraph.

Common Pitfalls

Too much nesting. Three or four layers of subgraphs. Debugging a deep call is brutal. Flatten where you can.

Hidden state keys. A subgraph that reads a key the parent didn't know about. Runtime KeyError.

Different checkpointers. The subgraph inherits the parent's. Don't try to attach your own.

Subgraph with cycles in the parent's control flow. If the parent loops back to a subgraph node, the subgraph runs fresh each time. Stateful subgraphs need explicit state design.

Relying on subgraph internals in the parent. Breaks encapsulation. Use outputs, not reach-in.

Next Steps

Continue to 09-multi-agent.md to coordinate several agents.