Subgraphs and composition
A compiled graph is a callable, so it can be a node. Subgraphs keep a large workflow readable, as long as the state contract at the boundary is small, explicit and tested.
A graph is a node too
A compiled graph is itself a callable: it takes a state, runs its nodes and returns an update. That is exactly the shape of a node, so LangGraph lets you add one graph inside another. The child keeps its own state schema, its own edges and its own entry point, and the parent sees a single node.
This is how a large workflow stays readable. The parent describes the phases — research, draft, review, publish — and each phase is a graph that can be opened, tested and changed on its own. Without that boundary, a forty-node graph is one diagram nobody can hold in their head. A phase boundary is also a natural place to cache a result, retry a whole phase or run two phases in parallel.
Composition is not free. Every boundary is a place where state can be dropped, renamed or silently merged, and those bugs look like a node that returns nothing. The contract has to be explicit, and it has to be tested at the seam rather than only at the ends.
The boundary is a state contract
A subgraph should receive the slice of state it needs and return the keys it owns. Passing the whole parent state couples the child to every key the parent has, and the coupling is invisible until someone renames one. Pass {"documents": state["documents"]} and let the child return {"summary": ...}. The parent then owns the composition, and the child owns the work.
When the schemas differ, put a small translation node on each side. It is five lines and it is the only place that knows both shapes. The alternative — teaching the child about parent-only keys — spreads that knowledge through every node in the child, and every parent that mounts it.
Reducers still apply at the boundary. If the parent appends to a list and the child replaces it, the merge will surprise you. Decide whether the child returns a replacement or a delta, and make the parent key’s reducer agree before the first run rather than after the first duplicate.
Mounting a compiled graph
from langgraph.graph import StateGraph, START, END
child = StateGraph(ChildState)
child.add_node("clean", clean)
child.add_edge(START, "clean")
child.add_edge("clean", END)
child_graph = child.compile()
parent = StateGraph(ParentState)
parent.add_node("research", child_graph)
parent.add_edge(START, "research")
parent.add_edge("research", END)A child graph inside a parent
Run the parent and follow the trace paths. The child’s two steps are nested under the mount name, and the final state carries both the child’s keys and the parent’s.
Namespaces keep nested runs addressable
A nested run produces nested checkpoints. The parent thread owns the outer steps, and each subgraph invocation gets a namespace inside it, so the trace reads like a path: parent:research:clean. That path is what lets you resume the exact step that failed rather than the phase around it.
Streaming follows the same shape. Events from a child arrive tagged with their namespace, so a UI can show “research is on step two of four” without the parent pretending to know. Debugging a nested graph without namespaces is guesswork; with them, every event has an address. A trace without namespaces flattens two mounts into one list of steps, and the list cannot tell you which mount failed.
Give every mount a distinct node name even when the graph is the same. Two nodes called research in one parent produce a trace, a checkpoint history and a diagram that cannot be told apart.
Test the child alone
A compiled subgraph is the easiest thing in the system to test. Invoke it with a state, assert the update, and you have tested the phase without the parent, the model or the tools around it. The same fixture can drive it in a unit test and in the parent’s integration test.
Reuse follows from that. The same graph can appear twice in a parent — research then verify — as long as the two nodes have distinct names and the state keys do not collide. Prefix the keys or wrap each mount in a translation node; a silent collision is two phases writing to one field, which is a merge bug rather than a reuse feature.
Version the contract. A subgraph that starts requiring a new key is a breaking change for every parent that mounts it. Keep the input schema small, give new keys defaults, and the child can evolve without a coordinated deploy of everything above it. A default is cheaper than a migration.
Mount the same graph twice
Run both mounts and compare the checkpoint paths. Then give each mount its own key prefix so the second no longer overwrites the first.
Check what you learned
One question at a time. A wrong answer explains itself — take another go.
Part 1 of 5
What is a subgraph?
