Conditional edges decide where to go next based on state. They're how LangGraph turns a flowchart into an agent.
Pattern 1 — Continue or end
The most common pattern. After an LLM node, either call a tool or finish:
def should_continue(state: AgentState) -> str:
last = state["messages"][-1]
if hasattr(last, "tool_calls") and last.tool_calls:
return "tools"
return END
builder.add_conditional_edges("agent", should_continue)
should_continue returns the name of the next node (or END).
Pattern 2 — Multi-way routing
Route to one of several specialized handlers:
def route_query(state: AgentState) -> str:
classification = state["classification"]
if classification == "billing":
return "billing_handler"
elif classification == "technical":
return "technical_handler"
elif classification == "complaint":
return "complaint_handler"
else:
return "general_handler"
builder.add_conditional_edges("classifier", route_query)
Each handler is a separate node. The classifier examines the input and picks one.
Pattern 3 — Conditional with explicit mapping
You can map return values to node names explicitly:
def decide(state):
if state["needs_research"]:
return "research"
return "respond"
builder.add_conditional_edges(
"planner",
decide,
{
"research": "research_node",
"respond": "respond_node",
},
)
The third argument is a dict mapping return values to actual node names. Useful when the decision function returns categorical values that don't match node names directly.
Pattern 4 — Loop with exit condition
Loop until a condition is met:
MAX_ITERATIONS = 10
def should_continue_or_giveup(state):
if state["iteration"] >= MAX_ITERATIONS:
return "giveup"
last = state["messages"][-1]
if hasattr(last, "tool_calls") and last.tool_calls:
return "tools"
if "FINAL ANSWER" in last.content:
return END
return "agent" # Keep iterating
builder.add_conditional_edges("agent", should_continue_or_giveup)
The agent loops back to itself if it hasn't finished. Cap iterations to prevent runaway loops.
Pattern 5 — Conditional based on tool results
Different next steps based on what a tool returned:
def handle_tool_result(state):
last_tool_msg = state["messages"][-1]
result = json.loads(last_tool_msg.content)
if result.get("requires_approval"):
return "human_review" # Pause for human
if result.get("error"):
return "error_handler"
return "agent" # Normal flow
builder.add_conditional_edges("tools", handle_tool_result)
Tools can signal special handling (approval, errors) and the graph routes accordingly.
Sub-graphs
For complex agents, build sub-graphs and compose:
# A sub-graph for the "research" capability
research_builder = StateGraph(ResearchState)
research_builder.add_node("search", search_node)
research_builder.add_node("summarize", summarize_node)
# ... define research sub-flow
research_graph = research_builder.compile()
# Main agent uses research as a single node
main_builder = StateGraph(MainState)
main_builder.add_node("research", research_graph) # entire sub-graph is a node
# ... rest of main flow
Sub-graphs encapsulate complexity. The main graph doesn't care about research internals; it just calls "research" and gets results.
State filtering between graphs
When sub-graphs have different state schemas, transform state at the boundary:
def adapt_to_research(state):
return {"query": state["messages"][-1].content} # research only needs the query
def adapt_from_research(result):
return {"messages": [AIMessage(content=result["summary"])]}
main_builder.add_node(
"research",
lambda s: adapt_from_research(research_graph.invoke(adapt_to_research(s)))
)
Common conditional-edge mistakes
- No max iterations. Agents loop indefinitely if the LLM keeps requesting tools.
- Conditional returns a node name that doesn't exist. Compile catches some; runtime catches the rest.
- Forgetting to handle the "stuck" case. Always have an escape route (e.g., escalate to human or end with apology).
- Routing on raw state without parsing. "If state['stuff']" might fail if "stuff" is None. Defensive checks.
- Putting logic in the conditional edge that should be in a node. Conditional edges decide routing; nodes do work. Keep separation clean.
Debugging conditional logic
In LangSmith, every conditional edge call is traced. You can see:
- The state when the edge was evaluated.
- The return value.
- Which node was selected.
For local debugging:
def should_continue(state):
decision = ...
print(f" [edge] state.iteration={state['iteration']} → {decision}")
return decision
Print routing decisions. When agents misbehave, the cause is often a routing decision you didn't expect.
Patterns to avoid
- Complex nested conditionals in edges. Hard to debug. Use multiple simpler edges instead.
- Stateful logic in edges (e.g., counting calls). State should be in
AgentState, not edge closures. - Conditional edges that modify state. They should be pure functions: state → next node name. Side effects belong in nodes.
Takeaway
Five patterns: continue-or-end, multi-way, with mapping, loop-with-exit, route-on-result. Cap iterations. Have escape paths. Sub-graphs for complexity. Keep conditional edges pure — examine state, return node name, no side effects.