Learnixo

LangGraph Agents · Lesson 15 of 17

Subgraphs: Composing Large Agent Systems

Why Subgraphs?

As agent workflows grow complex, a single flat graph becomes hard to reason about and maintain. Subgraphs let you:

  • Define reusable agent components once and use them in multiple parent graphs
  • Test components in isolation
  • Keep each graph focused on one concern
  • Build hierarchical agent architectures (parent orchestrates children)

Basic Subgraph

A subgraph is just a compiled LangGraph — you use it as a node in a parent graph:

Python
from langgraph.graph import StateGraph, END
from typing import TypedDict, Annotated
import operator

# --- Subgraph 1: Drug Research ---
class ResearchSubState(TypedDict):
    drug_name: str
    research_findings: Annotated[list[str], operator.add]
    is_complete: bool

def search_literature(state: ResearchSubState) -> dict:
    """Search medical literature for drug information."""
    finding = f"Found mechanism data for {state['drug_name']}: inhibits VKOR enzyme"
    return {"research_findings": [finding]}

def extract_key_facts(state: ResearchSubState) -> dict:
    """Extract and structure key facts from research."""
    finding = f"Extracted: {state['drug_name']} — monitoring: INR 2.0-3.0"
    return {"research_findings": [finding], "is_complete": True}

research_graph = StateGraph(ResearchSubState)
research_graph.add_node("search", search_literature)
research_graph.add_node("extract", extract_key_facts)
research_graph.set_entry_point("search")
research_graph.add_edge("search", "extract")
research_graph.add_edge("extract", END)

research_subgraph = research_graph.compile()  # Compiled subgraph

# --- Subgraph 2: Safety Check ---
class SafetySubState(TypedDict):
    drug_name: str
    interactions: Annotated[list[str], operator.add]
    risk_level: str

def check_interactions(state: SafetySubState) -> dict:
    return {"interactions": [f"{state['drug_name']} interacts with NSAIDs (major)"]}

def assess_risk(state: SafetySubState) -> dict:
    return {"risk_level": "moderate"}

safety_graph = StateGraph(SafetySubState)
safety_graph.add_node("check", check_interactions)
safety_graph.add_node("assess", assess_risk)
safety_graph.set_entry_point("check")
safety_graph.add_edge("check", "assess")
safety_graph.add_edge("assess", END)

safety_subgraph = safety_graph.compile()

Parent Graph Using Subgraphs as Nodes

Python
class ParentState(TypedDict):
    drug_name: str
    # These fields must overlap with subgraph states
    research_findings: Annotated[list[str], operator.add]
    interactions: Annotated[list[str], operator.add]
    risk_level: str
    is_complete: bool
    final_report: str

def synthesize(state: ParentState) -> dict:
    """Combine all subgraph outputs into a final report."""
    report = f"""Drug Report: {state['drug_name']}

Research findings:
{chr(10).join(f'- {f}' for f in state['research_findings'])}

Safety:
{chr(10).join(f'- {i}' for i in state['interactions'])}
Risk level: {state.get('risk_level', 'unknown')}"""

    return {"final_report": report}

# Build parent graph
parent = StateGraph(ParentState)

# Add subgraphs as nodes
parent.add_node("research", research_subgraph)
parent.add_node("safety", safety_subgraph)
parent.add_node("synthesize", synthesize)

parent.set_entry_point("research")
parent.add_edge("research", "safety")
parent.add_edge("safety", "synthesize")
parent.add_edge("synthesize", END)

parent_app = parent.compile()

# Run the full pipeline
result = parent_app.invoke({
    "drug_name": "warfarin",
    "research_findings": [],
    "interactions": [],
    "risk_level": "",
    "is_complete": False,
    "final_report": "",
})

print(result["final_report"])

State Sharing Between Parent and Subgraph

The key requirement: subgraph state fields used for I/O must be present in the parent state. LangGraph automatically maps matching field names between parent and subgraph state.

Pattern:

  • Subgraph input fields: must exist in parent state (values passed in)
  • Subgraph output fields: must exist in parent state (values merged back)
  • Subgraph-internal fields: not required in parent state
Python
# Subgraph has: drug_name (input), internal_temp_data (internal), findings (output)
# Parent must have: drug_name (to pass in), findings (to receive output)
# Parent does NOT need: internal_temp_data (stays inside subgraph)

Parallel Subgraph Execution with Send

Use Send to run multiple subgraph instances in parallel:

Python
from langgraph.types import Send

class ParallelAnalysisState(TypedDict):
    drug_list: list[str]
    per_drug_results: Annotated[list[dict], operator.add]
    combined_report: str

def create_parallel_tasks(state: ParallelAnalysisState) -> list[Send]:
    """Fan out: one research task per drug."""
    return [
        Send("research_drug", {"drug_name": drug, "research_findings": [], "is_complete": False})
        for drug in state["drug_list"]
    ]

def collect_results(state: ParallelAnalysisState) -> dict:
    """Fan in: collect all parallel results."""
    report = f"Analyzed {len(state['per_drug_results'])} drugs:\n"
    for result in state["per_drug_results"]:
        report += f"- {result.get('drug_name', 'unknown')}\n"
    return {"combined_report": report}

multi_drug_graph = StateGraph(ParallelAnalysisState)
multi_drug_graph.add_node("research_drug", research_subgraph)
multi_drug_graph.add_node("collect", collect_results)

multi_drug_graph.add_conditional_edges("__start__", create_parallel_tasks)
multi_drug_graph.add_edge("research_drug", "collect")
multi_drug_graph.add_edge("collect", END)

Testing Subgraphs in Isolation

The modular design makes testing easy:

Python
# Test research subgraph independently
research_result = research_subgraph.invoke({
    "drug_name": "metformin",
    "research_findings": [],
    "is_complete": False,
})

assert research_result["is_complete"] is True
assert len(research_result["research_findings"]) > 0
assert "metformin" in research_result["research_findings"][0].lower()
print("Research subgraph test passed")

# Test safety subgraph independently
safety_result = safety_subgraph.invoke({
    "drug_name": "metformin",
    "interactions": [],
    "risk_level": "",
})

assert safety_result["risk_level"] in ("low", "moderate", "high")
print("Safety subgraph test passed")

Subgraph Design Guidelines

One concern per subgraph: Research, safety, and synthesis should be separate subgraphs — not combined into one large subgraph. Each should be independently testable.

Minimal state overlap: Only share state fields that are genuinely needed for hand-off. Internal working fields should stay within the subgraph's own state.

Versioning: Subgraphs can be versioned (v1_research_subgraph, v2_research_subgraph) — swap them in the parent without changing any other subgraph.

Checkpointing: When a parent graph is compiled with a checkpointer, subgraph state is also checkpointed. The full nested state is saved at each checkpoint.