Dynamic subgraphs?

hi @sudokpr

I reckon, you can build and compile a subgraph inside a node on every invocation, but it’s usually not the pattern LangGraph is optimized for. For your goal (only exposing a subset of agents to the LLM), it’s typically better to keep the graph topology static and make the router’s prompt depend on selected_agents rather than recompiling a new subgraph each time.

This follows the way the official subgraphs examples build graphs once and reuse them, instead of compiling dynamically in the hot path.

1. Is compiling a subgraph per invocation “wrong”?

No, it’s allowed and will work, especially if:

  • Graphs are small and
  • Throughput is low (e.g., a few requests per minute, not hundreds per second).

StateGraph(...).compile() does more than just wrap functions:

  • Validates and freezes the topology
  • Wires in the checkpointer and config handling
  • Prepares the execution planner

Doing that on every node invocation is extra overhead that you normally avoid by compiling at startup and reusing the compiled graph.

You also lose some benefits of treating it as a first-class subgraph:

  • If you wire a compiled subgraph into the parent before calling compile, LangGraph can automatically share the checkpointer and make streaming/debugging across parent + subgraph much nicer.
  • If you instead build and invoke a fresh StateGraph inside a node, that inner execution looks more like an opaque function call from the parent graph’s point of view.

So it’s acceptable, but not the idiomatic or most efficient pattern unless you really need per-request topology changes.

2. A simpler pattern for your use case

Your use case is:

  • Supervisor router manages many sub‑agents.
  • For a given query you want to restrict which agents are offered to the LLM, to keep the prompt small.

You don’t actually need a dynamically rebuilt subgraph for that. Instead:

  • Keep a static set of agent nodes in the graph (or a static supervisor graph pattern like in the multi‑agent / supervisor examples).
  • Add to the state something like selected_agents: list[str] or rich metadata about the “active” subset.
  • In the router node, construct the system prompt / tool list from selected_agents only.

Conceptually:

class State(TypedDict):
    user_query: str
    selected_agents: list[AgentConfig]  # computed earlier in the graph


def router(state: State):
    # Build a prompt that only mentions selected agents
    instructions = """You are a supervisor. You may call ONLY these agents:
    {descriptions}
    """.format(
        descriptions="\n".join(a.description for a in state["selected_agents"]),
    )

    # Call your LLM with just those options
    # model = ...
    # response = model.invoke(...)
    ...

The underlying graph can still have all agents as nodes; the LLM just never hears about the irrelevant ones for a given query, so you’ve reduced context without touching the graph topology.

This is very close to the official “supervisor + workers” / multi‑agent workflow patterns, where the supervisor chooses among a fixed set of workers but you control what gets surfaced in the prompt.

3. When dynamic per‑query graphs do make sense

Dynamic graph compilation is useful in more advanced setups, for example:

  • A planner builds a DAG (Deep/Directed Acyclic Graph) at runtime from the user query (e.g., a plan of tools/agents to run with dependencies), and you then turn that plan into a StateGraph and compile it once for that request.

  • This is exactly the kind of “build a graph at runtime, then compile and execute it” pattern described in the community example “Building Dynamic Agentic Workflows at Runtime”.

In that pattern, you still avoid recompiling in a tight loop: you compile once per planned workflow, not once per node call inside a long‑lived parent graph.

If your agent selection logic is that complex, a good architecture is:

  • Top‑level entrypoint (or small parent graph) runs a planner / router.
  • The planner returns a plan or a list of agents.
  • Your application code builds a dedicated StateGraph for that plan, calls .compile() once, and then runs that compiled graph (graph.invoke / graph.stream).

4. Practical recommendation

Putting it together:

  • If you mainly care about shrinking the LLM context:
    • Keep a static graph.
    • Use selected_agents (or similar) in state to dynamically generate the router prompt/tool list.
  • If you truly need different topologies per query:
    • Build and compile a per‑query graph in a planner step, but don’t re‑compile inside every node.
  • Only compile per‑invocation inside a node if your workflows are small, low‑traffic, and you don’t mind the extra overhead and reduced observability.

That way you stay close to the idiomatic patterns shown in the subgraph and multi‑agent docs, but still get the context savings you’re after.


Official LangGraph Python docs on subgraphs, including examples of compiling once and calling from a node: LangGraph Python docs – Subgraphs.

LangGraph graphs reference, which documents the StateGraph / CompiledGraph model and the compile step: LangGraph graphs reference.

Overview of supervisor + worker multi‑agent patterns in LangGraph: LangGraph multi‑agent workflows blog.

Previous topic on the forum Dynamic Graph Creation at Runtime