Skip to content

Supervisor pattern

In a supervisor (router) graph, a router reads the request and selects the worker agents to run. The selected workers run in parallel, and a gather node combines their results.

The example is examples/langgraph-integration/design_patterns/supervisor/toy/main.py. Its router is an LLM; its two workers are AsyncFlow tasks that sleep for 3 s each, so a run that selects both shows them running at the same time.

flowchart LR
  START((start)) --> R[llm_router]
  R -->|Send| A[agent_A]
  R -->|Send| B[agent_B]
  A --> G[gather]
  B --> G
  G --> END((end))

Run it

The router and the gather node use OpenRouter: set OPEN_ROUTER_API_KEY, then run make examples-supervisor-toy. CI, which has no credentials, runs the example with a scripted chat model in place of OpenRouter (tests/integration/test_design_patterns_offline.py); tests/integration/test_integration.py runs it live when FLOWGENTIC_RUN_LLM_INTEGRATION=1 is set.

The state

The workers write into results, whose reducer merges the dictionaries that parallel workers return:

class GraphState(BaseModel):
    query: str = Field(..., description="User query to route")
    routing_decision: Optional[List[str]] = Field(
        default=None, description="List of agents to route to"
    )
    routing_rationale: Optional[str] = Field(
        default=None, description="Decision to move to a set of nodes"
    )
    results: Annotated[Dict[str, str], operator.or_] = Field(
        default_factory=dict, description="Merge dicts from parallel agents"
    )
    final_summary: Optional[str] = Field(
        default=None, description="Combined summary from gather node"
    )
    messages: Annotated[List[BaseMessage], add_messages] = []

The router

create_llm_router(responsibilities, model) (flowgentic.langGraph.utils.supervisor) returns a node that asks the model which agents should handle state.query, with structured output, and stores its choice in routing_decision and routing_rationale. The example runs it as an EXECUTION_BLOCK:

        # Define the routing prompt template
        routing_prompt_agents_responsibility = """

- agent_A: Handles data processing and analysis tasks
- agent_B: Handles question answering and information retrieval
- both: When the query requires both processing AND answering

"""

        # Define the model for routing
        router_model = ChatLLMProvider(
            provider="OpenRouter", model="google/gemini-2.5-pro"
        )

        # Create and decorate the router function using the factory
        llm_router = agents_manager.execution_wrappers.asyncflow(
            create_llm_router(routing_prompt_agents_responsibility, router_model),
            flow_type=AsyncFlowType.EXECUTION_BLOCK,
        )

The workers

@agents_manager.execution_wrappers.asyncflow(
    flow_type=AsyncFlowType.FUNCTION_TASK
)
async def agent_a(state: GraphState) -> dict:
    start = time.perf_counter()
    logging.info(f"📊 agent_a START - Processing data for: '{state.query}'")
    await asyncio.sleep(3.0)  # simulate work
    elapsed = (time.perf_counter() - start) * 1000
    logging.info(f"📊 agent_a END   took_ms={elapsed:.1f}")
    return {"results": {"agent_A": f"Data analysis complete: {state.query}"}}

@agents_manager.execution_wrappers.asyncflow(
    flow_type=AsyncFlowType.FUNCTION_TASK
)
async def agent_b(state: GraphState) -> dict:
    start = time.perf_counter()
    logging.info(f"💬 agent_b START - Answering: '{state.query}'")
    await asyncio.sleep(3.0)  # simulate work
    elapsed = (time.perf_counter() - start) * 1000
    logging.info(f"💬 agent_b END   took_ms={elapsed:.1f}")
    return {
        "results": {
            "agent_B": f"Answer: {state.query} - Parallelism is executing multiple tasks simultaneously!"
        }
    }

The graph

supervisor_fan_out returns one Send per agent in routing_decision, so LangGraph runs the selected workers in parallel. The path_map lists the agents the router may choose:

graph.add_node("llm_router", llm_router)
graph.add_node("agent_A", agent_a)
graph.add_node("agent_B", agent_b)
graph.add_node("gather", gather)

# Edges
graph.add_edge(START, "llm_router")
graph.add_conditional_edges(
    "llm_router", supervisor_fan_out, path_map=["agent_A", "agent_B"]
)
graph.add_edge("agent_A", "gather")
graph.add_edge("agent_B", "gather")
graph.add_edge("gather", END)

app = graph.compile()

When both workers ran, the gather node asks a second model, built with langchain.agents.create_agent, to combine their outputs; with one worker, it passes the result through.

For an agent that hands work to another agent through a tool call, see Hand off to another agent.