Graphs inside graphs
A compiled graph can be a node inside another graph. That is how a program stays readable once it stops fitting on a screen.
Nothing new is being added to LangGraph here. A node is something that takes the state and returns an update, and a compiled graph does exactly that, so it can go straight in as one.
When both graphs share a state
from typing_extensions import TypedDict
from langgraph.graph import StateGraph, START
class State(TypedDict):
ticket: str
reply: strThe inner graph. One node, and it is a graph like any other.
writer = StateGraph(State)
writer.add_node("draft", lambda s: {"reply": f"About {s['ticket']}: we are on it."})
writer.add_edge(START, "draft")The outer graph adds the compiled inner one exactly like a normal node.
parent = StateGraph(State)
parent.add_node("tidy", lambda s: {"ticket": s["ticket"].strip()})
parent.add_node("write", writer.compile())
parent.add_edge(START, "tidy")
parent.add_edge("tidy", "write")A compiled graph is handed to add_node in the place a function would normally go. That is the whole trick.
print(parent.compile().invoke({"ticket": " charged twice ", "reply": ""}))The inner graph read the same keys and wrote back to the same keys, so nothing had to be translated on the way in or out.
When they do not
More often the inner graph has its own idea of the state and you would rather not force the two to agree. Then you call it from inside an ordinary node.
from typing_extensions import TypedDict
from langgraph.graph import StateGraph, START
class Outer(TypedDict):
ticket: str
reply: str
class Inner(TypedDict):
text: str
draft: strwriter = StateGraph(Inner)
writer.add_node("draft", lambda s: {"draft": f"About {s['text']}: we are on it."})
writer.add_edge(START, "draft")
writer_graph = writer.compile()The wrapper is three lines: build the inner state, run the inner graph, put the answer where the outer graph expects it.
def write(state):
done = writer_graph.invoke({"text": state["ticket"], "draft": ""})
return {"reply": done["draft"]}The first line translates in, with the outer name on the left and the inner name on the right. The return translates back out. Neither graph now knows anything about the other's shape.
parent = StateGraph(Outer)
parent.add_node("write", write)
parent.add_edge(START, "write")
print(parent.compile().invoke({"ticket": "charged twice", "reply": ""}))| Add the graph as a node | Call it inside a node | |
|---|---|---|
| Use when | The two states share keys | The two states are different |
| You write | One add_node line | A small wrapper function |
| Coupling | The two schemas must agree | None |
Why bother
- It fits in your head. Six nodes each, twice, beats twelve nodes at once.
- It gets reused. The same inner graph can be a node in three places.
- It gets tested. An inner graph is a graph, so you can invoke it on its own.
- Add a second node to the inner graph and confirm the outer one does not care.
- Run the inner graph on its own with
writer_graph.invoke. - Print the outer graph's nodes and see how the inner one appears.
You understood something today that you didn't yesterday.