n4nAI

LangGraph conditional edges: routing between agents

LangGraph conditional edges enable dynamic agent routing based on state — here's how they work, when to use them, and a production-ready example.

n4n Team5 min read1,044 words

Audio narration

Coming soon — every post will get a voice note here.

LangGraph conditional edges are functions that inspect graph state and return the next node name, enabling dynamic routing between agents without hardcoding transitions. Unlike static edges that always follow the same path, conditional edges let a single node fan out to multiple destinations based on runtime values — tool results, model outputs, or arbitrary state fields. This mechanism is the backbone of langgraph conditional edges routing in multi-agent systems.

How conditional edges work

At the graph level, a conditional edge is a callable that receives the current State object and returns a string (the next node name) or a list of strings for parallel execution. You register it with add_conditional_edges instead of add_edge. The function runs every time the source node completes, giving you a decision point before the graph continues.

from langgraph.graph import StateGraph, END
from typing import Literal

def route_after_classifier(state: State) -> Literal["researcher", "coder", "reviewer"]:
    decision = state["classification"]
    if decision == "research":
        return "researcher"
    elif decision == "code":
        return "coder"
    return "reviewer"

graph = StateGraph(State)
graph.add_node("classifier", classifier_node)
graph.add_node("researcher", researcher_node)
graph.add_node("coder", coder_node)
graph.add_node("reviewer", reviewer_node)

graph.add_conditional_edges("classifier", route_after_classifier)
graph.add_edge("researcher", END)
graph.add_edge("coder", END)
graph.add_edge("reviewer", END)

The routing function can be synchronous or async. It has full access to state — including messages, tool outputs, and any custom fields you’ve added. Return END to terminate the graph. Return a list like ["node_a", "node_b"] to fan out to multiple nodes in parallel (LangGraph executes them concurrently).

Why conditional edges matter for multi-agent workflows

Static graphs force every request through the same sequence. Real workloads don’t work that way. A coding task needs a planner, then a coder, then a tester. A research task needs a search agent, then a synthesizer, then a fact-checker. A simple Q&A might need none of the above.

Conditional edges let you:

  • Route by intent: Classify the user request once, then dispatch to the right specialist agent.
  • Implement guardrails: Check tool outputs for errors, policy violations, or low confidence, then route to a recovery agent or human escalation.
  • Enable iterative loops: Send the output of a generator back to a critic until quality thresholds are met, then exit.
  • Compose reusable subgraphs: Each agent can be its own compiled graph; the parent graph routes between them without knowing their internals.

This is where langgraph conditional edges routing becomes a composition primitive, not just a control flow feature. You build agents as black boxes, then wire them together with routing logic that lives outside any single agent.

Concrete example: triage → specialist → reviewer

Consider a support ticket system. Tickets arrive, get classified, route to a domain specialist (billing, technical, account), then pass through a quality reviewer before closing. The reviewer can bounce tickets back to the specialist with feedback.

from typing import TypedDict, Literal
from langgraph.graph import StateGraph, END
from langchain_core.messages import BaseMessage

class TicketState(TypedDict):
    messages: list[BaseMessage]
    category: Literal["billing", "technical", "account", "unknown"]
    specialist_output: str
    review_feedback: str
    retry_count: int

def triage(state: TicketState) -> Literal["billing_agent", "technical_agent", "account_agent", "escalate"]:
    # In practice, this calls an LLM or classifier
    category = state["category"]
    if category == "billing":
        return "billing_agent"
    elif category == "technical":
        return "technical_agent"
    elif category == "account":
        return "account_agent"
    return "escalate"

def review_quality(state: TicketState) -> Literal["close", "retry", "escalate"]:
    feedback = state["review_feedback"]
    retries = state["retry_count"]
    if "PASS" in feedback.upper():
        return "close"
    if retries >= 2:
        return "escalate"
    return "retry"

def billing_agent(state: TicketState) -> TicketState:
    # ... process billing ticket ...
    return {"specialist_output": "Resolved billing issue", "retry_count": 0}

def technical_agent(state: TicketState) -> TicketState:
    # ... process technical ticket ...
    return {"specialist_output": "Resolved technical issue", "retry_count": 0}

def account_agent(state: TicketState) -> TicketState:
    # ... process account ticket ...
    return {"specialist_output": "Resolved account issue", "retry_count": 0}

def reviewer(state: TicketState) -> TicketState:
    # ... review specialist_output ...
    return {"review_feedback": "PASS", "retry_count": state.get("retry_count", 0) + 1}

def escalate_node(state: TicketState) -> TicketState:
    return {"specialist_output": "Escalated to human"}

graph = StateGraph(TicketState)
graph.add_node("triage", lambda s: s)  # no-op, just a routing point
graph.add_node("billing_agent", billing_agent)
graph.add_node("technical_agent", technical_agent)
graph.add_node("account_agent", account_agent)
graph.add_node("reviewer", reviewer)
graph.add_node("escalate", escalate_node)

graph.add_conditional_edges("triage", triage)
graph.add_edge("billing_agent", "reviewer")
graph.add_edge("technical_agent", "reviewer")
graph.add_edge("account_agent", "reviewer")
graph.add_conditional_edges("reviewer", review_quality, {
    "close": END,
    "retry": "triage",  # loop back with incremented retry_count
    "escalate": "escalate"
})
graph.add_edge("escalate", END)

graph.set_entry_point("triage")
app = graph.compile()

Key details in this example:

  • The triage node is a no-op; all routing logic lives in the conditional edge function. This keeps nodes pure and testable.
  • The review_quality function implements a bounded retry loop. The retry_count in state prevents infinite cycles.
  • The conditional edge returns a mapping dictionary ({"close": END, "retry": "triage", ...}) — this is optional but recommended. It documents valid transitions and lets LangGraph validate the graph at compile time.
  • Specialists don’t know about each other or the reviewer. They only produce output. The graph structure handles coordination.

Common misconceptions

“Conditional edges are just if/else statements in the graph”

They look like if/else, but the distinction matters. An if/else inside a node couples routing logic to that node’s implementation. A conditional edge separates routing from execution. This means:

  • You can change routing without touching agent code.
  • Multiple nodes can share the same routing function.
  • Routing logic is testable in isolation — pass a State dict, assert the returned node name.
  • Visualization tools (LangGraph Studio, Mermaid export) show the decision point explicitly.

“You need a separate node for every decision”

A single node can have multiple conditional edges leaving it, each with different routing functions. You can also chain conditional edges: node_a → conditional → node_b → conditional → node_c. The graph doesn’t care how many decision points exist.

“Conditional edges hurt observability”

The opposite. Because routing is explicit in the graph definition, every transition appears in traces. You see which condition fired, what state values drove it, and where execution went next. Compare this to implicit routing buried inside a monolithic agent prompt — there, you only see the final output.

“Parallel fan-out requires special syntax”

Return a list from your routing function: return ["node_a", "node_b"]. LangGraph executes both in parallel, waits for both to complete, then merges state (last-write-wins per key, or you can provide a custom reducer). This is useful for “gather” patterns — e.g., send a query to both a web search agent and a code search agent simultaneously.

“State must be a Pydantic model”

LangGraph accepts TypedDict, dataclass, Pydantic BaseModel, or plain dict. Use whatever fits your codebase. TypedDict gives you IDE autocomplete without runtime overhead. Pydantic gives you validation. Both work with conditional edges.

Advanced patterns

Routing with structured output

Instead of parsing free-text LLM output, use structured output (function calling / tool calling) to produce a routing decision directly:

from pydantic import BaseModel
from langchain_core.utils.function_calling import convert_to_openai_tool

class RoutingDecision(BaseModel):
    next_node: Literal["researcher", "coder", "reviewer"]
    reasoning: str

routing_tool = convert_to_openai_tool(RoutingDecision)

def llm_router(state: State) -> Literal["researcher", "coder", "reviewer"]:
    response = llm_with_tools.invoke(state["messages"])
    tool_call = response.tool_calls[0]
    return tool_call["args"]["next_node"]

This eliminates prompt engineering for routing and makes the decision auditable — the reasoning field appears in traces.

Dynamic agent registration

In systems where agents are plugins, you can build the routing function at startup:

def make_router(agents: dict[str, Agent]) -> Callable[[State], str]:
    def route(state: State) -> str:
        intent = classify_intent(state["messages"][-1].content)
        return agents.get(intent, agents["default"]).name
    return route

graph.add_conditional_edges("dispatcher", make_router(registry))

New agents register themselves; the graph picks them up without recompilation.

Conditional edges with subgraphs

Each specialist in the ticket example could be a compiled subgraph with its own internal nodes (planner → executor → verifier). The parent graph only sees the subgraph as a single node. This scales: the parent stays simple while subgraphs grow complex independently.

billing_subgraph = build_billing_graph().compile()
technical_subgraph = build_technical_graph().compile()

parent = StateGraph(TicketState)
parent.add_node("billing", billing_subgraph)
parent.add_node("technical", technical_subgraph)
# ... conditional edges route to "billing" or "technical" ...

When to avoid conditional edges

  • Linear pipelines: If every request follows the same sequence, static edges are clearer.
  • Single-agent systems: No routing needed.
  • Extremely high-throughput paths: Each conditional edge adds a function call overhead. Negligible for LLM latency, but measurable in tight loops with thousands of iterations.

Testing conditional edges

Test the routing function in isolation:

import pytest

def test_triage_routes_billing():
    state = {"category": "billing", "messages": [], "retry_count": 0}
    assert triage(state) == "billing_agent"

def test_review_loops_twice_then_escalates():
    state = {"review_feedback": "FAIL", "retry_count": 0}
    assert review_quality(state) == "retry"
    
    state = {"review_feedback": "FAIL", "retry_count": 1}
    assert review_quality(state) == "retry"
    
    state = {"review_feedback": "FAIL", "retry_count": 2}
    assert review_quality(state) == "escalate"

Test the compiled graph with app.invoke() for integration coverage. LangGraph’s MemorySaver checkpointer lets you inspect intermediate state at each step.

Summary

LangGraph conditional edges routing is the mechanism that turns a static DAG into a dynamic, state-driven workflow. The routing function is a pure decision point — input state, output next node name. Keep it pure, test it in isolation, and use the mapping dictionary form for documentation and validation. Build agents as independent subgraphs, then compose them with conditional edges at the top level. This scales from three agents to thirty without the parent graph becoming a spaghetti mess.

Tagslanggraphconditional-edgesroutingagents

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →

All langgraph multi-agent workflows posts →