RemoteGraph and subgraph nesting: using a compiled graph as a node
Responsibilities
LangGraph lets you treat a compiled graph — either a local Pregel instance or a remote RemoteGraph — as a node inside another graph. RemoteGraph (RemoteGraph:118) is the client implementation of PregelProtocol, calling remotely-deployed graphs through the LangGraph Server API. The local StateGraph.compile() product is itself a Pregel (also a PregelProtocol), so you can directly parent_graph.add_node(child_graph) to nest it.
This layer is responsible for abstracting away "nested graph": when the parent graph schedules this node, whether the backing instance is a local Pregel or a remote HTTP API, it goes through the same invoke / stream interface. PregelNode uses find_subgraph_pregel (find_subgraph_pregel:47) to recursively locate the Pregel instance inside a node's runnable and attaches it to node.subgraphs, so the parent's get_subgraphs can enumerate the subgraph and stream(subgraphs=True) can see subgraph-internal events.
Design motivation
- Distributed deployment: Large graphs are often split into multiple services (conversation management, retrieval, tool calling), each a LangGraph deployment. The parent graph strings them into a workflow via
RemoteGraph. Each service can scale and upgrade independently without redeploying the parent. - Independent nested checkpoints: After
add_node(compiled_graph), the subgraph can be configured with its own checkpointer (compile(checkpointer=...)) or inherit from the parent. The subgraph's thread_id is auto-prefixed with a namespace by the parent viaCONFIG_KEY_CHECKPOINT_NS(parent_node:<task_id>|child_node:<task_id>), so parent and child checkpoints never collide. PregelProtocolunified interface: Both localPregelandRemoteGraphimplementPregelProtocol—invoke/stream/astream/get_state/aget_state/get_subgraphsshare the same signatures. The parent does not need to treat them differently.- Stream pass-through: With
subgraphs=Trueenabled, the parent's_outputtags events with a namespace prefix on dequeue (get_subgraphs:1076); the caller gets(ns, mode, payload)triples and can tell which node at which nesting level an event came from. Command.PARENTacross levels: A subgraph node canreturn Command(graph=Command.PARENT, update=...)to write updates to the parent's state — passed via pending writes between parent and subgraph, and processed by the parent's_firstat finalization.
Key files
RemoteGraph:118—RemoteGraph(PregelProtocol)client; holdsassistant_id/client/sync_client/name.RemoteGraph.__init__:132— Acceptsurl/api_key/headers/client/sync_client; defaults to creating viaget_client/get_sync_client.RemoteGraph.stream:757— Sync streaming entry; forwards the request tosync_client.run_stream.RemoteGraph.astream:912— Async streaming entry; forwards to theclient.run_streamasync iterator.RemoteGraph.invoke:1133— Sync collapsing entry; internally goes throughstreamto take the last value.find_subgraph_pregel:47— Recursively searches a node'sboundrunnable forPregelProtocolinstances to identify subgraphs.PregelNode:97— ThePregelNodeclass; holds asubgraphs: Sequence[PregelProtocol]field.PregelNode subgraphs init:180—PregelNode.__init__callsfind_subgraph_pregel(self.bound)to auto-identify subgraphs.get_subgraphs:1076— Parent enumerates direct subgraphs; recurses downward whenrecurse=True.attach_node:1431— At compile time, turns theadd_node-registered action intoPregelNode(bound=node.runnable, ...); subgraph identification happens here.coerce_to_runnable:529— AnyRunnableinstance (includingPregel/RemoteGraph) is returned directly — this is the entry point for "compiled graph as node".
Data flow
add_node(compiled_child_graph) goes through coerce_to_runnable (coerce_to_runnable:529):
def coerce_to_runnable(
thing: RunnableLike, *, name: str | None, trace: bool
) -> Runnable:
"""Coerce a runnable-like object into a Runnable."""
if isinstance(thing, Runnable):
return thing
elif is_async_generator(thing) or inspect.isgeneratorfunction(thing):
return RunnableLambda(thing, name=name)
elif callable(thing):
# ...
elif isinstance(thing, dict):
return RunnableParallel(thing)
else:
raise TypeError(...)Both Pregel and RemoteGraph inherit from Runnable, so they hit the first branch and return directly. PregelNode.__init__ calls find_subgraph_pregel(self.bound) during the attach_node phase (find_subgraph_pregel:47):
def find_subgraph_pregel(candidate: Runnable) -> PregelProtocol | None:
from langgraph.pregel import Pregel
candidates: list[Runnable] = [candidate]
for c in candidates:
if (
isinstance(c, PregelProtocol)
# subgraphs that disabled checkpointing are not considered
and (not isinstance(c, Pregel) or c.checkpointer is not False)
):
return c
elif isinstance(c, RunnableSequence) or isinstance(c, RunnableSeq):
candidates.extend(c.steps)
elif isinstance(c, RunnableLambda):
candidates.extend(c.deps)
elif isinstance(c, RunnableCallable):
if c.func is not None:
candidates.extend(
nl.__self__ if hasattr(nl, "__self__") else nl
for nl in get_function_nonlocals(c.func)
)
elif c.afunc is not None:
candidates.extend(
nl.__self__ if hasattr(nl, "__self__") else nl
for nl in get_function_nonlocals(c.afunc)
)
return NoneIt recurses through RunnableSequence / RunnableLambda / RunnableCallable — even a node wrapped in a RunnableLambda that calls Pregel is recognized. The identified subgraph is attached to self.subgraphs = [subgraph], so the parent's get_subgraphs() (get_subgraphs:1076) can enumerate it.
At execution time, the parent's PregelRunner.tick calls the subgraph Pregel's stream / astream (or the corresponding RemoteGraph methods). The subgraph runs its own Pregel loop internally, and the events it produces are passed through by the parent's stream(subgraphs=True) — the parent prefixes CONFIG_KEY_CHECKPOINT_NS with the node name when calling the subgraph, the subgraph's events carry that namespace when written back to the parent's stream queue, and the parent's _output merges them with local events on dequeue.
Boundaries and failure modes
- Subgraphs with checkpointing disabled are not recognized:
find_subgraph_pregelexplicitly skips Pregels withc.checkpointer is False(skip no-checkpointer:54). Such "stateless subgraphs" are not treated as subgraphs — the parent'sget_subgraphsdoes not see them, and their events do not carry a namespace. RemoteGraphrequires at leasturlorclient: In__init__, when bothclientandsync_clientareNoneand nourlwas passed, neither client gets created, and a subsequent_validate_client()call raisesValueError(validate client:181).RemoteGraphdoes not support everystream_mode:_reject_v3_unsupportedrejects certain parameter combinations; the remote API's stream protocol has more restrictions than local Pregel (reject v3:195).- Nested namespaces use
NS_SEP: The namespace format isparent_node:<task_id>+NS_SEP+child_node:<task_id>, recursing downward.add_noderejects node names containingNS_SEP(reject NS_SEP:794) to avoid namespace ambiguity. - Subgraph
interrupt()propagates along the namespace: When a subgraph node'sinterrupt()raisesGraphInterrupt, the parent'stickdetects the subgraph's pending interrupt, and the parent also enters an interrupt state. On client resume,Command(resume=...)is routed to the corresponding subgraph by namespace viaCONFIG_KEY_RESUME_MAP(Command resume mapping:904). Command.PARENTboundary: When a subgraph node doesreturn Command(graph=Command.PARENT, ...), the subgraph'smap_commanddirectly raisesInvalidUpdateError("There is no parent graph")(PARENT guard:58). This is actually a fallback — the normal path is that the subgraph's_get_updatesmapper atattach_nodetime skipsCommand.PARENTand writes it verbatim into the subgraph's pending writes; the parent recognizes and routes it inside_first.
Summary
RemoteGraph lets LangGraph support cross-service, cross-process graph nesting, while local Pregel nesting is auto-identified via find_subgraph_pregel — both go through PregelProtocol. The parent's stream(subgraphs=True) passes subgraph events through via namespace. For subgraph event-stream details see /stream/run-stream; for subgraph node scheduling see /pregel/pregel.
See official docs: LangGraph docs · README.