Chapter 126 min read

Chapter 12: Subgraphs and Composition

Up until this point, we have been building single-agent architectures. We defined one global State, attached several nodes, and managed the control flow directly.

This works perfectly for small projects. But what happens when your application grows?

Imagine building an AI company that researchers topics, writes 50-page reports, peer-reviews the content, formats it for SEO, and publishes it. If you put all of those tools, prompts, LLMs, and routing logic into a single StateGraph, you will end up with a 30-node monster graph.

It will become impossible to test, impossible to debug, and terrifying to update.

In this chapter, we will solve the complexity problem using Subgraphs. We will learn how to encapsulate workflows, separate State logic, and compose massive enterprise applications.


12.1 What Is A Subgraph?

A Subgraph is exactly what it sounds like: a fully functional, independently compiled LangGraph application that exists entirely inside another graph.

Instead of building one massive 30-node graph, you build three small, 10-node graphs:

  1. Research Graph: Dedicated to fetching facts.
  2. Writing Graph: Dedicated to drafting paragraphs.
  3. Review Graph: Dedicated to critiquing and editing.

You compile them separately, test them separately, and then compose them together.


12.2 The Mind-Blowing Concept: Graph as a Node

In LangGraph, there is fundamentally no difference between a Python function and a compiled StateGraph.

A compiled graph can become a node inside another graph.

# 1. Compile the Child (Subgraph)
research_builder = StateGraph(ResearchState)
# ... add nodes and edges ...
research_subgraph = research_builder.compile()

# 2. Create the Parent Graph
parent_builder = StateGraph(ParentState)

# 3. Add the compiled Subgraph as a Node!
parent_builder.add_node("research_team", research_subgraph)

When the Parent Graph routes to the "research_team" node, it physically hands execution over to the Subgraph. The Subgraph runs its entire internal cycle (nodes, edges, LLM calls, loops) until it reaches its internal END node. Only then does it return control back to the Parent Graph.


12.3 Parent State vs Child State

The most confusing part of Subgraphs is understanding how data flows between them.

The Parent Graph has its own State (ParentState). The Child Subgraph has its own State (ChildState).

By default, when the Parent invokes the Child, it attempts to pass its entire ParentState dictionary directly into the Child. If the keys match perfectly, this works. But in enterprise systems, they rarely match perfectly.

  • ParentState might have {"global_budget": 500, "user_id": 123, "topic": "AI"}
  • ChildState only cares about {"topic": "AI", "research_notes": []}

To prevent the Parent from polluting the Child's state with irrelevant data (and to prevent the Child from accidentally overwriting the Parent's data), we use State Mapping.


12.4 State Mapping (Passing Data In and Out)

Instead of passing the compiled subgraph directly to add_node, you can pass a standard Python function that acts as a "bridge" or "adapter" between the two states.

def research_adapter_node(parent_state: ParentState):
    
    # 1. Extract ONLY what the child needs from the Parent State
    child_input = {
        "topic": parent_state["topic"]
    }
    
    # 2. Invoke the compiled subgraph manually
    child_result = research_subgraph.invoke(child_input)
    
    # 3. Extract ONLY the results we want to pass back to the Parent State
    return {
        # The parent doesn't care about the child's internal scratchpad.
        # It only wants the final notes.
        "final_report": child_result["research_notes"]
    }

# Add the adapter function to the Parent!
parent_builder.add_node("research_team", research_adapter_node)

This ensures complete encapsulation. The Research Subgraph has no idea it is inside a Parent Graph, and the Parent Graph has no idea how the Research Subgraph works internally.


12.5 Building the Subgraphs

Let's build a functional Composition. We will build a highly modular Content Pipeline.

1. The Research Subgraph

class ResearchState(TypedDict):
    topic: str
    facts: Annotated[list[str], operator.add]

def fetch_facts(state: ResearchState):
    print(f"[Research Team] Gathering facts about {state['topic']}...")
    return {"facts": [f"Fact 1 about {state['topic']}", f"Fact 2 about {state['topic']}"]}

research_builder = StateGraph(ResearchState)
research_builder.add_node("fetcher", fetch_facts)
research_builder.add_edge(START, "fetcher")
research_builder.add_edge("fetcher", END)
research_app = research_builder.compile()

2. The Writer Subgraph

class WriterState(TypedDict):
    provided_facts: list[str]
    draft: str

def write_draft(state: WriterState):
    print(f"[Writer Team] Drafting article using {len(state['provided_facts'])} facts...")
    return {"draft": "Here is the final article based on the facts."}

writer_builder = StateGraph(WriterState)
writer_builder.add_node("writer", write_draft)
writer_builder.add_edge(START, "writer")
writer_builder.add_edge("writer", END)
writer_app = writer_builder.compile()

12.6 Composing the Parent Graph

Now, we build the Parent Graph that orchestrates the two teams. We will use adapter nodes to map the data cleanly between them.

# The Parent holds the global application state
class ParentState(TypedDict):
    project_topic: str
    collected_research: list[str]
    final_article: str

# Adapter for Researcher
def run_research_team(state: ParentState):
    print("\n--- Parent: Delegating to Research Team ---")
    # Invoke child
    result = research_app.invoke({"topic": state["project_topic"]})
    # Map back to parent
    return {"collected_research": result["facts"]}

# Adapter for Writer
def run_writer_team(state: ParentState):
    print("\n--- Parent: Delegating to Writer Team ---")
    # Invoke child
    result = writer_app.invoke({"provided_facts": state["collected_research"]})
    # Map back to parent
    return {"final_article": result["draft"]}

# Build the Parent Topology
parent_builder = StateGraph(ParentState)

parent_builder.add_node("research_node", run_research_team)
parent_builder.add_node("writer_node", run_writer_team)

parent_builder.add_edge(START, "research_node")
parent_builder.add_edge("research_node", "writer_node")
parent_builder.add_edge("writer_node", END)

parent_app = parent_builder.compile()

# Execute the entire massive architecture with one call!
final_result = parent_app.invoke({
    "project_topic": "LangGraph Composition"
})

print("\n--- Parent: Final Output ---")
print(final_result["final_article"])

Execution Trace:

--- Parent: Delegating to Research Team ---
[Research Team] Gathering facts about LangGraph Composition...

--- Parent: Delegating to Writer Team ---
[Writer Team] Drafting article using 2 facts...

--- Parent: Final Output ---
Here is the final article based on the facts.

12.7 Enterprise Benefits of Subgraphs

Why go through the effort of building adapters and isolating state?

  1. Reusability: The research_app Subgraph we built can now be imported and used by 10 different Parent Graphs across the company.
  2. Isolation: If the Writer Team introduces a bug that causes an infinite loop, it only affects the WriterState. The ParentState and ResearchState remain completely untouched and safe.
  3. Testing: You can write simple unit tests for research_app in isolation without needing to mock the entire application workflow.
  4. Ownership: In a large company, Team A can maintain the Research Graph repository, while Team B maintains the Writer Graph repository.

12.8 Summary

A compiled LangGraph StateGraph is just another callable function. It can be embedded inside another graph, creating infinite layers of encapsulation and complexity.

By mastering State Mapping, you can protect your global application state from messy internal subgraph logic.

Now that we understand how to encapsulate specialized workflows into independent Subgraphs, we are ready to tackle the pinnacle of AI Architecture.

In the next chapter, we will build Multi-Agent Systems, where each agent is essentially a specialized Subgraph orchestrated by an LLM Supervisor.