Branching and Conditional Edges for Multi-Path Agents
Conditional edges turn state into control flow: after a node finishes, a router function inspects the shared state and chooses the next node (or END).
Search across all documentation pages
Conditional edges turn state into control flow: after a node finishes, a router function inspects the shared state and chooses the next node (or END).
That is how LangGraph agents escalate, call tools, skip steps, or fork work without hiding branches inside one opaque function.
Write a pure router that returns a path key, map keys to node names with add_conditional_edges, keep path labels stable, and test every branch with fixture state before connecting expensive models or tools.
risk, tool_calls, attempts).state -> str (or path key).END).from typing import Literal, TypedDict
from langgraph.graph import StateGraph, START, END
class SupportState(TypedDict):
message: str
risk: str # "low" | "high"
needs_tools: bool
outcome: str
def triage(state: SupportState) -> dict:
text = state["message"].lower()
risk = "high" if any(w in text for w in ("lawsuit", "fraud", "delete all")) else "low"
needs_tools = "order" in text or "tracking" in text
return {"risk": risk, "needs_tools": needs_tools}
def route_after_triage(
state: SupportState,
) -> Literal["tools", "escalate", "answer"]:
if state["risk"] == "high":
return "escalate"
if state["needs_tools"]:
return "tools"
return "answer"
def tools_node(state: SupportState) -> dict:
return {"outcome": f"looked up order context for: {state['message'][:60]}"}
def escalate_node(state: SupportState) -> dict:
return {"outcome": "handed to human queue"}
def answer_node(state: SupportState) -> dict:
return {"outcome": "auto-replied from FAQ"}
builder = StateGraph(SupportState)
builder.add_node("triage", triage)
builder.add_node("tools", tools_node)
builder.add_node("escalate", escalate_node)
builder.add_node("answer", answer_node)
builder.add_edge(START, "triage")
builder.add_conditional_edges(
"triage",
route_after_triage,
{
"tools": "tools",
"escalate": "escalate",
"answer": "answer",
},
)
builder.add_edge("tools", END)
builder.add_edge("escalate", END)
builder.add_edge("answer", END)
graph = builder.compile()
print(graph.invoke({
"message": "Where is my order?",
"risk": "",
"needs_tools": False,
"outcome": "",
})["outcome"])For tool-calling agents, the router often inspects the last AI message for tool_calls and returns "tools" vs END.
Same API; different signal.
add_conditional_edges(source, router, path_map) has two common styles:
END.Explicit maps catch typos at compile/review time.
From START (or any node) you can attach multiple fixed edges so several nodes run in the same superstep.
Combine with reducers on shared list fields so parallel writers append instead of clobber.
For dynamic map-reduce, LangGraph's Send API can dispatch per-item tasks (advanced; verify at build).
Treat routers as the smallest policy unit you can code-review.
If product says "never auto-refund over $X," put that check in the router or a dedicated gate node, not only in a prompt.
Nodes can also return Command(goto=..., update=...) to update state and jump in one step (useful after interrupt approval).
Prefer ordinary conditional edges for the main happy path so topology stays visible; use Command when the node must choose after an external resume value.
Name nodes after business steps (escalate, not node_3).
In LangSmith, conditional paths show up as different node sequences; that is how you measure how often traffic hits the dangerous branch.
path_map; fail loud in tests.escalate or END with error state, not crash mid-flight.| Approach | Strength | Weakness |
|---|---|---|
| Conditional edges | Explicit, testable topology | More graph wiring |
| Giant node with internal if/else | Fast draft | Opaque traces, hard HITL |
| LLM-as-router only | Flexible language | Unstable labels, cost |
| Rules engine outside graph | Shared enterprise policy | Two systems to sync |
| Always-linear pipeline | Simple | Cannot adapt mid-run |
Use code for safety and compliance branches. Use the model for fuzzy classification that still lands on an allowlisted path key validated in the router.
Fan-out is usually expressed with multiple edges or Send, not a single string. Check your LangGraph version docs for multi-destination patterns.
Call the router with crafted state dicts and assert return values. Separately invoke the compiled graph with mocked node bodies if needed.
Typos ("escallate") fail at runtime. Prefer Literal types and a single path_map constant shared by tests.
Branching chooses the next step in one graph. Handoff often transfers goals and context to another agent or subgraph. You can implement handoff as a branch into a subgraph node.
Transient tool failures belong in retry policies or a retry path with an attempt counter, not infinite unconditional edges back to the same node.
Only if path policy is data-driven (config/feature flags) and the graph already contains those nodes. New nodes still need a deploy.
Every run must eventually stop. Branches may rejoin a shared tail node that then edges to END, or each branch edges to END directly.
Related: Building Your First Stateful Agent Graph
Related: LangChain & LangGraph Basics
Related: Human-in-the-Loop Approval Nodes
Related: The LangGraph Mental Model
Related: LangChain & LangGraph Best Practices
Stack versions: Pins from the category manifest (verify at build): OpenRouter (~315+ models, July 2026 pricing/fees); LangGraph 1.0+; CrewAI 1.14+; Microsoft Agent Framework 1.0; Vercel AI SDK 6; Pydantic AI (latest); LlamaIndex (latest); OpenAI Agents SDK (latest + MCP); MCP (Linux Foundation governance); A2A (HTTP+SSE+JSON-RPC 2.0); Solana
@solana/web3.js+@solana/spl-token.
Reviewed by Chris St. John·Last updated Jul 16, 2026