07 Oct LangGraph Subgraphs: Build Reusable Graph Components
As graphs grow, one giant graph becomes hard to read and test. A subgraph is a complete graph used as a single node inside another graph. You build small pieces once, test them alone, and combine them.
Why use subgraphs?
- Organize: one big workflow becomes a few clear pieces
- Reuse: the same subgraph works in many parent graphs
- Test: run and debug each piece on its own
- Teamwork: different people can own different subgraphs
- Multi-agent systems: each agent can be its own subgraph (Chapter 18)
There are two ways to connect a subgraph, depending on whether parent and subgraph share state keys.
Way 1: Shared state keys (add the subgraph directly)
If the subgraph and the parent have some state keys with the same names, you can pass the compiled subgraph straight to add_node().
First, build the subgraph, a small text analyzer:
from typing import TypedDict
from langgraph.graph import StateGraph, START, END
class AnalyzerState(TypedDict):
text: str
word_count: int
level: str
def count_words(state: AnalyzerState):
return {"word_count": len(state["text"].split())}
def label_length(state: AnalyzerState):
return {"level": "short" if state["word_count"] < 10 else "long"}
sub_builder = StateGraph(AnalyzerState)
sub_builder.add_node("count_words", count_words)
sub_builder.add_node("label_length", label_length)
sub_builder.add_edge(START, "count_words")
sub_builder.add_edge("count_words", "label_length")
sub_builder.add_edge("label_length", END)
analyzer = sub_builder.compile()
Now the parent graph, which uses it as one node:
class ParentState(TypedDict):
text: str
level: str
report: str
def clean(state: ParentState):
return {"text": state["text"].strip()}
def make_report(state: ParentState):
return {"report": f"The text is {state['level']}."}
parent = StateGraph(ParentState)
parent.add_node("clean", clean)
parent.add_node("analyzer", analyzer) # the subgraph as a node
parent.add_node("make_report", make_report)
parent.add_edge(START, "clean")
parent.add_edge("clean", "analyzer")
parent.add_edge("analyzer", "make_report")
parent.add_edge("make_report", END)
graph = parent.compile()
result = graph.invoke({"text": " LangGraph helps you build AI apps as graphs "})
print(result)
Output:
{'text': 'LangGraph helps you build AI apps as graphs', 'level': 'short', 'report': 'The text is short.'}
How it works:
- clean trims the text in the parent state
- The parent hands the state to the analyzer subgraph. The keys text and level exist in both schemas, so they are shared.
- Inside the subgraph, count_words and label_length run as normal
- The subgraph’s level flows back to the parent, and make_report uses it
- word_count exists only inside the subgraph, and the parent never sees it. That is a private key, and it keeps internal details hidden.
Way 2: Different schemas (call the subgraph from a node)
When the parent and subgraph use different key names, wrap the subgraph in a normal function. The function translates state going in and coming out:
class ShoutState(TypedDict):
sentence: str
shout: str
def make_upper(state: ShoutState):
return {"shout": state["sentence"].upper()}
sub_builder = StateGraph(ShoutState)
sub_builder.add_node("make_upper", make_upper)
sub_builder.add_edge(START, "make_upper")
sub_builder.add_edge("make_upper", END)
shouter = sub_builder.compile()
class PageState(TypedDict):
title: str
banner: str
def call_shouter(state: PageState):
out = shouter.invoke({"sentence": state["title"]}) # translate in
return {"banner": out["shout"]} # translate out
parent = StateGraph(PageState)
parent.add_node("banner_maker", call_shouter)
parent.add_edge(START, "banner_maker")
parent.add_edge("banner_maker", END)
graph = parent.compile()
print(graph.invoke({"title": "welcome to langgraph"}))
Output:
{'title': 'welcome to langgraph', 'banner': 'WELCOME TO LANGGRAPH'}
Here, the parent knows title and banner, and the subgraph knows sentence and shout. The wrapper node maps between them. It takes a few extra lines, but gives you full control.
Which way to choose?
- Parent and subgraph share key names: add the subgraph directly as a node
- Different key names or shapes: wrap it in a function that calls invoke()
- You want to hide internal fields: either way works, as private keys stay inside
Memory and subgraphs
Give the checkpointer only to the parent. LangGraph passes it down to subgraphs automatically:
from langgraph.checkpoint.memory import InMemorySaver graph = parent.compile(checkpointer=InMemorySaver())
Don’t compile the subgraph with its own checkpointer unless you have a specific reason. One saved history for the whole run keeps things simple.
Seeing inside a subgraph
By default, streaming and diagrams treat a subgraph as one box. To look inside:
Streaming with subgraphs=True:
for namespace, chunk in graph.stream(
{"text": " LangGraph helps you build AI apps as graphs "},
stream_mode="updates",
subgraphs=True,
):
print(namespace, chunk)
Sample output (the ID after analyzer: will differ):
() {'clean': {'text': 'LangGraph helps you build AI apps as graphs'}}
('analyzer:3f9a1c2b-...',) {'count_words': {'word_count': 8}}
('analyzer:3f9a1c2b-...',) {'label_length': {'level': 'short'}}
() {'analyzer': {'level': 'short'}}
() {'make_report': {'report': 'The text is short.'}}
With subgraphs=True, each item becomes a (namespace, chunk) pair. An empty namespace () means the parent graph, and a namespace like (‘analyzer:…’,) means the update came from inside the subgraph. Notice that word_count shows up in the subgraph updates, and never in the parent.
Diagram with the subgraph expanded:
print(graph.get_graph(xray=True).draw_mermaid())
With xray=True, the diagram shows the subgraph’s nodes inside its box, so you can see the complete structure.
Subgraphs with agents
Because create_agent (Chapter 12) returns a compiled graph, you can use a prebuilt agent as a node the same way:
parent.add_node("helper_agent", agent) # `agent` from create_agent(...)
This works most smoothly when the agent and the parent share the messages key, for example when the parent uses MessagesState. Chapter 18 builds on this idea.
Common mistakes
- Expecting shared keys without matching names: if names differ, the subgraph receives nothing. Use the wrapper approach.
- Expecting private keys in the parent’s result: keys only in the subgraph stay inside it
- Putting a checkpointer on both: pass it to the parent only
- Forgetting subgraphs=True when streaming: you will see the subgraph as a single step
- Passing the builder instead of the compiled graph: call .compile() on the subgraph first
- Mismatched types on shared keys: both schemas should define the same type for a key, or updates may behave unexpectedly
- Making subgraphs too small: a subgraph with one trivial node is usually just a normal node
Summary
- A subgraph is a compiled graph used as a node inside another graph
- With shared keys, pass it straight to add_node(). With different schemas, wrap it in a function.
- Keys that exist only in the subgraph stay private
- Give the checkpointer to the parent only
- Use subgraphs=True in stream() and get_graph(xray=True) to see inside
Practice
- Add a second use of the analyzer subgraph in the parent (for example, analyzing a different field through a wrapper node) to see how reuse works
- Run the shared-keys example with subgraphs=True and identify which updates come from the parent and which from the subgraph
- Print get_graph(xray=True).draw_mermaid() and compare it to the version without xray
- Build a subgraph that cleans text (trim, lowercase, remove extra spaces), and plug it into the start of any earlier graph
If you liked the tutorial, spread the word and share the link and our website, Studyopedia, with others.
For Videos, Join Our YouTube Channel:Â Join Now
Read More:
- Generative AI Tutorial
- AI Ethics
- Machine Learning Tutorial
- Deep Learning Tutorial
- Ollama Tutorial
- Retrieval Augmented Generation (RAG) Tutorial
- ChatGPT Tutorial
- Microsoft Copilot Tutorial
No Comments