LangGraphLangGraph 1.2 · Python 3.10+
1
Curious builder0 XP earned · 300 to level 2
0 daysFinish a lesson to begin
Badge collection0 of 6 unlocked
37 small wins to finish your pathNext lesson

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

python
from typing_extensions import TypedDict
from langgraph.graph import StateGraph, START

class State(TypedDict):
    ticket: str
    reply: str

The inner graph. One node, and it is a graph like any other.

python
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.

python
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.

Example
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.

python
from typing_extensions import TypedDict
from langgraph.graph import StateGraph, START

class Outer(TypedDict):
    ticket: str
    reply: str

class Inner(TypedDict):
    text: str
    draft: str
python
writer = 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.

python
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.

Example
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 nodeCall it inside a node
Use whenThe two states share keysThe two states are different
You writeOne add_node lineA small wrapper function
CouplingThe two schemas must agreeNone
A trap with shared keys
If a shared key has a reducer, take care. The inner graph returns the whole key and the reducer combines it with what was already there, so a list can come back doubled. This catches everybody once. When in doubt, use the wrapper.

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.
Try it yourself
  • 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.