Free lesson · GenAI Agent Engineering

Build a multi-agent orchestrator

You can build a puppeteer-style orchestrator, implement a DAG-based agent network with topological ordering, build an RL-trained agent coordinator (epsilon-greedy), pick from multi-agent design patterns (research/analysis/sequence-routing), handle circular dependencies, weigh DAG vs linear pipelines, distinguish static composition vs dynamic orchestration, and apply DAG-network composition patterns.

Course: GenAI Agent Engineering · Chapter 37 · The Subgraph (Composition)

Free to read — no subscription required.

Introduction

When you need to coordinate multiple specialized agents — each handling its own slice of a workflow — a single monolithic graph quickly becomes hard to maintain and extend. A puppeteer-style orchestrator solves this by acting as a top-level controller that delegates tasks to self-contained subgraphs and threads shared state through each one in sequence. By the end of this lesson, you will know how to wire a puppeteer orchestrator in LangGraph, attach compiled subgraphs as its nodes, and route state between them so the composition behaves as one cohesive pipeline.

Key Terminology

  • Puppeteer Orchestrator — a top-level StateGraph that controls sequencing and state routing across multiple subgraphs, delegating all internal processing to each subgraph while remaining unaware of their internal nodes.
  • Subgraph — an independently built and compiled StateGraph that encapsulates its own nodes and edges, and is registered as an opaque node inside a parent orchestrator graph via add_node.
  • Shared State Schema — a single TypedDict (here PipelineState) that the orchestrator and every subgraph agree on, allowing state fields written by one subgraph to be read directly by the next without manual field-copying.
  • Compiled Subgraph — the result of calling .compile() on a StateGraph builder, which produces a callable that LangGraph treats as a single node; the orchestrator sees only the subgraph's entry and exit, not its internal structure.
  • Sequential Subgraph Routing — the use of add_edge in the orchestrator to specify the order in which compiled subgraphs execute, so that state flows from one subgraph's output directly into the next subgraph's input.

Concepts

The Puppeteer Pattern: Orchestration vs. Internal Choreography

A monolithic graph that grows to handle research, summarization, validation, and formatting quickly becomes an interconnected tangle where every node is aware of every other. The puppeteer pattern separates two distinct concerns: who runs next (the orchestrator's responsibility) and how a unit of work is done (each subgraph's responsibility). The orchestrator acts like a conductor — it sets the sequence and passes the baton, but it has no knowledge of what happens inside each section of the orchestra.

This separation means you can reason about, test, and swap individual subgraphs without touching the orchestrator wiring, and you can redesign the orchestration order without touching the subgraph logic. The compiled subgraph is the boundary — once compiled, it is opaque to the outside world.

Shared State as the Communication Bus

For the puppeteer pattern to work, every participant must speak the same language. In LangGraph this is achieved by declaring a single shared TypedDictPipelineState in the Code Walkthrough — and passing it to every StateGraph builder. Each subgraph reads the fields it needs and writes back only the fields it produces. When research_subgraph writes research_result, that updated dictionary is the exact object handed to summary_subgraph next — no adapter, no translation layer.

This design creates a clear data contract: the schema is the interface. Adding a new field to PipelineState (say, validation_result) costs one line in the TypedDict; the existing subgraphs that don't use it simply ignore it. The orchestrator never needs to inspect field values to route correctly when the pipeline is sequential — the edges encode the order, and the state carries the data.

Loading diagram...

Compiled Subgraphs as Opaque Nodes

When you call research_builder.compile(), LangGraph produces a runnable that looks, from the orchestrator's perspective, exactly like any other node function — it receives state in, returns a state delta out (see Code Walkthrough). The orchestrator registers it with add_node("research_agent", research_subgraph) and then edges manage the rest.

This opacity is a feature, not a limitation. The orchestrator's edges remain stable whether the research subgraph has one internal node or ten. Adding a third stage to the pipeline is additive: one add_node, two add_edge calls. None of the existing subgraphs require modification. This is what allows the pattern to scale cleanly — growth is always local to either a subgraph's internals or the orchestrator's routing table, never both at once.

Code Walkthrough

Now that you understand how subgraphs encapsulate their own nodes and state transitions, you can compose them under a single controlling graph — the puppeteer orchestrator — that decides which subgraph runs and in what order.

The pattern has three moving parts: a shared state schema that every subgraph and the orchestrator agree on, the subgraphs themselves compiled independently, and the orchestrator graph that adds those compiled subgraphs as ordinary nodes and routes between them.

Code snippetpython
1from typing import TypedDict 2from langgraph.graph import StateGraph, END 3 4class PipelineState(TypedDict): 5 topic: str 6 research_result: str 7 summary: str 8 9# --- Research subgraph --- 10def research_node(state: PipelineState) -> dict: 11 return {"research_result": f"Key facts about: {state['topic']}"} 12 13research_builder = StateGraph(PipelineState) 14research_builder.add_node("research", research_node) 15research_builder.set_entry_point("research") 16research_builder.add_edge("research", END) 17research_subgraph = research_builder.compile() 18 19# --- Summary subgraph --- 20def summarize_node(state: PipelineState) -> dict: 21 return {"summary": f"Summary — {state['research_result'][:80]}"} 22 23summary_builder = StateGraph(PipelineState) 24summary_builder.add_node("summarize", summarize_node) 25summary_builder.set_entry_point("summarize") 26summary_builder.add_edge("summarize", END) 27summary_subgraph = summary_builder.compile() 28 29# --- Puppeteer orchestrator --- 30orchestrator = StateGraph(PipelineState) 31orchestrator.add_node("research_agent", research_subgraph) 32orchestrator.add_node("summary_agent", summary_subgraph) 33orchestrator.set_entry_point("research_agent") 34orchestrator.add_edge("research_agent", "summary_agent") 35orchestrator.add_edge("summary_agent", END) 36pipeline = orchestrator.compile() 37 38result = pipeline.invoke({ 39 "topic": "LangGraph subgraph composition", 40 "research_result": "", 41 "summary": "", 42}) 43print(result["summary"])

The orchestrator invokes research_subgraph first, which populates research_result in the shared state, then passes that updated state directly to summary_subgraph. Each subgraph reads from and writes to the same PipelineState dictionary, so the output of one subgraph automatically becomes the input for the next without any manual field-copying between their internal nodes.

Because each subgraph is compiled before being registered as a node, LangGraph treats it as an opaque callable. The orchestrator sees only the subgraph's entry and exit — not its internal nodes. This is the puppeteer relationship: the orchestrator controls sequencing and state flow, while each subgraph manages its own internal choreography independently.

Adding a third subgraph later requires only a new add_node call and an updated edge — the existing subgraphs need no modification. That isolation is what makes the pattern scale cleanly as the pipeline grows.

Confirm that result["summary"] contains text derived from the topic string you passed in, and that swapping out one subgraph for a different compiled graph leaves the orchestrator wiring unchanged.

Do's and Don'ts

Having walked through building puppeteer-style orchestrator above, the following Do's and Don'ts distill it into practice.

Do's

  1. Do share a single TypedDict schema (PipelineState) across the orchestrator and every subgraph — all three graphs read from and write to the same dictionary, so research_result produced by research_subgraph is immediately visible to summary_subgraph without any manual field-copying.
  2. Do compile each subgraph with .compile() before passing it to orchestrator.add_node() — LangGraph treats the compiled graph as an opaque callable, which is what lets the orchestrator control sequencing while each subgraph manages its own internal node choreography independently.
  3. Do add new subgraphs with only a new add_node call and an updated add_edge — because each subgraph is self-contained, extending the pipeline to a third or fourth stage never requires modifying existing subgraphs, only the orchestrator's wiring.

Don'ts

  1. Don't register an uncompiled StateGraph builder as an orchestrator node — only the .compile() result is a callable that LangGraph can invoke as a node; passing the builder object directly will fail at runtime because the orchestrator expects an opaque callable, not a graph-under-construction.
  2. Don't use mismatched state schemas between the orchestrator and a subgraph — if summary_subgraph expects a research_result key that the orchestrator's state schema omits, the field will be absent when summarize_node reads state['research_result'], producing silent empty output or a KeyError with no obvious cross-graph error message.
  3. Don't copy state fields manually between subgraph calls — the puppeteer pattern relies on the orchestrator passing the same PipelineState dict through each node in sequence; introducing intermediate field-remapping logic breaks the clean handoff and creates a second source of truth that diverges when subgraph outputs change.

This lesson is free to read. Its hands-on lab — real code, in a cloud IDE — is part of the GenAI Agent Engineering subscription.

From · cancel anytime · Already a subscriber? Sign in →

More free lessons in GenAI Agent Engineering

All free lessons in GenAI Agent Engineering