Appearance
5.2 — Conditional Routing and Custom Reducers
1. What is it?
Conditional edges are LangGraph's mechanism for making the next node a runtime decision based on the current state, rather than a fixed connection defined at graph-construction time — this is the concrete graph-level implementation of Part 3.7's "routing" workflow pattern. Custom reducers (building on Part 5.1's introduction of add_messages) let you define exactly how any state field should combine updates from multiple nodes, beyond the built-in accumulate-or-overwrite defaults.
2. Why does it exist?
Part 5.1's graph.add_edge("classify", "respond") is a fixed edge — respond always runs after classify, unconditionally. But Part 3.7's routing pattern (classify a ticket, then dispatch to one of several category-specific handlers) needs the next node to depend on the classification's actual result. Conditional edges exist to express exactly this without needing to fall back to an open-ended, dynamically-decided agent (Part 3.7's core distinction) — the routing decision is still made by a deterministic function inspecting state, keeping the overall graph's possible paths fully enumerable and auditable, even though which specific path is taken varies per execution.
3. What problem does it solve?
It solves "let the graph's next step depend on data computed during execution, while keeping the set of possible next steps fixed and known in advance" — this is the crucial distinction from a true agent (Part 3.7): a conditional edge picks among a fixed, enumerable set of possible next nodes based on state, whereas an agent's model-driven tool selection can, in principle, chart an unbounded variety of paths. Custom reducers solve "how do multiple nodes correctly combine their contributions to a shared field when neither pure overwrite nor pure list-append is the right combination rule" — e.g., merging two dictionaries, keeping a running maximum, or de-duplicating a set of flagged issues contributed by several independent checks.
4. How does it work internally?
Conditional edges — a routing function plus a mapping
python
from langgraph.graph import StateGraph, END
def route_by_category(state: SupportState) -> str:
"""Returns the name of the next node to run, based on the classified category."""
category = state["ticket_category"]
if category == "billing":
return "handle_billing"
elif category == "technical":
return "handle_technical"
return "handle_general"
graph.add_conditional_edges(
"classify",
route_by_category,
{
"handle_billing": "handle_billing",
"handle_technical": "handle_technical",
"handle_general": "handle_general",
},
)add_conditional_edges takes the source node, a routing function (receiving the current state, returning a string key), and a mapping from possible keys to actual node names. At execution time, after classify completes and its state update is merged, LangGraph calls route_by_category(state) and transitions to whichever node the returned key maps to. Because the mapping is provided explicitly at graph-construction time, the full set of possible destinations is always known statically by reading the graph definition — even though which one is taken depends on runtime data. This is precisely the property that keeps this a workflow (Part 3.7) rather than an agent: you can enumerate and test every possible path through the graph in advance, unlike an agent's genuinely open-ended action space.
Custom reducers — the general mechanism
A reducer is just a function taking (current_value, new_value) and returning the combined result — add_messages (Part 5.1) is LangGraph's built-in reducer specifically for message lists, but you can write your own for any combination logic a field needs:
python
from typing import Annotated
def merge_flagged_issues(current: list[str], new: list[str]) -> list[str]:
"""Merges flagged issues from multiple independent checks, de-duplicating."""
return list(set(current) | set(new))
class LoanReviewState(TypedDict):
flagged_issues: Annotated[list[str], merge_flagged_issues]
# ... other fieldsIf credit_check_node returns {"flagged_issues": ["high_debt_ratio"]} and, in the same superstep, fraud_check_node returns {"flagged_issues": ["address_mismatch"]}, LangGraph calls merge_flagged_issues(["high_debt_ratio"], ["address_mismatch"]) (accounting for however multiple simultaneous updates in one superstep are actually combined — verify the exact multi-update-in-one-superstep reduction order against current documentation, since this detail matters for reducers that aren't commutative) rather than one node's update silently overwriting the other's — directly solving the problem Part 5.1's plain-overwrite default would create for two independent nodes both wanting to contribute to the same field.
Why this matters for the parallel-nodes pattern from Part 5.1
Part 5.1's loan-review example had each check write to its own separate field specifically to sidestep the need for a custom reducer. Custom reducers are what let you instead have multiple independent nodes contribute to a shared field (like a combined flagged_issues list) without needing artificially separate fields for each contributor — a real design choice with a real trade-off: separate fields (Part 5.1's approach) are simpler and need no custom reducer code, but require a downstream node to explicitly know about and read each separate field; a shared field with a custom reducer is more elegant and scales to an arbitrary number of contributing nodes without downstream changes, at the cost of writing and testing the reducer's combination logic correctly (especially its behavior for concurrent, same-superstep updates).
5. Simple mental model
A conditional edge is a signpost at a fork in a hiking trail with a fixed, printed list of destinations — the sign (routing function) reads a hiker's stated destination (state) and points to one of a small, fixed set of trails (the mapping), but the sign itself, and the set of trails it could ever point to, was fixed when the trail was built. This is different from a hiker with a map and total freedom to bushwhack anywhere (a true agent) — a conditional edge's destinations are enumerable in advance, even though which one gets used varies per hike. A custom reducer is like a shared collection box at a trailhead where multiple hikers can each drop in their own trail-report note, and a park ranger has a specific rule for how to combine all the day's notes into one summary (deduplicating repeated reports, keeping the most recent for each trail segment) — rather than the last hiker's note simply overwriting everyone else's.
6. Real-world example
An insurance claims-processing graph routes a claim to one of simple_auto_approval, manual_review, or fraud_investigation based on a risk-scoring node's output — a conditional edge with three possible destinations, chosen deterministically by a routing function inspecting the risk score, keeping every possible path (and its compliance/audit implications) fully known and reviewable in advance, exactly the auditability property Part 3.7's loan-review redesign argued for over an open-ended agent making the same decision.
7. Architecture diagram
classify nodesets state["ticket_category"]
route_by_category()called by LangGraph after classify's update is merged; reads state, returns a string key
the FULL SET of possible destinations, fixed and enumerable
billing node
technical node
general node
8. Production considerations
- Test every branch of a conditional edge's mapping explicitly (Part 1.9) — because the full set of destinations is enumerable, this is a genuinely comprehensive, achievable testing target, unlike testing every possible path of an open-ended agent.
- Write custom reducers to be commutative and order-independent where multiple nodes might update the same field in one superstep — a reducer whose result depends on update order can produce non-deterministic behavior in a genuinely parallel superstep, which is a subtle, hard-to-debug production issue.
- Keep routing functions themselves simple and deterministic — a routing function doing its own LLM call to decide is a legitimate pattern (Part 3.7's routing workflow), but should be a narrow, well-tested, bounded call, not itself an open-ended decision process.
9. Common mistakes
- Writing a non-commutative custom reducer (e.g., one that depends on which update happens to be processed "first") without realizing multiple nodes might update the same field within the same superstep, causing intermittent, hard-to-reproduce bugs.
- Not testing every branch of a conditional edge's mapping, missing a bug in a rarely-taken path (e.g., a
handle_generalfallback rarely exercised in practice, but silently broken). - Confusing a well-bounded conditional-edge routing decision with a genuinely open-ended agent decision, and applying the wrong level of testing/audit rigor as a result (Part 3.7's distinction, misapplied).
10. Security considerations
- Because conditional-edge destinations are fully enumerable, a security review can exhaustively verify every possible path's authorization/data-handling behavior — a genuine, concrete auditability advantage over an agent's open-ended action space (Part 9.2), worth stating explicitly when justifying a workflow-over-agent architecture decision to a security-conscious customer.
- A custom reducer combining data from multiple nodes should be reviewed for whether it could inadvertently combine or leak data across a boundary that should stay separate (e.g., merging two tenants' data into one shared field by mistake) — the same tenant-isolation discipline from Part 1.5/3.4/4.5 applies to reducer logic.
11. Performance considerations
- Routing functions should be fast/cheap (ideally no LLM call, or a small, fast model at most, Part 3.11) since they run synchronously as part of the graph's control flow on every relevant transition — a slow routing function adds latency to every single execution regardless of which branch is ultimately taken.
12. Cost considerations
- If a routing function does use an LLM call for classification (Part 3.7's routing pattern), the same model-selection discipline from Part 3.11 applies — this is exactly the kind of simple, well-defined, low-stakes task appropriate for a small, cheap, fast model rather than the application's largest model.
13. When to use it
Conditional edges: any time the next step in a fixed-set-of-possibilities workflow depends on runtime data (classification results, computed scores, validation outcomes). Custom reducers: whenever multiple nodes need to contribute to one shared state field using combination logic beyond simple overwrite or message-list append.
14. When NOT to use it
If a workflow's next step never actually varies (always the same next node regardless of any computed state), a plain fixed edge (Part 5.1) is simpler and there's no need for the added indirection of a conditional edge. If only one node ever writes to a given field, no custom reducer is needed — the default overwrite behavior is correct and simpler.
15. Alternatives and trade-offs
| Approach | Good for | Weak point |
|---|---|---|
| Conditional edges | Fixed, enumerable set of runtime-dependent next steps | Not appropriate for genuinely open-ended, unbounded decision spaces (use an agent instead, Part 3.7) |
| Separate state fields per contributing node (Part 5.1) | Simple, no custom reducer code needed | Downstream nodes must know about and read each separate field explicitly |
| Custom reducer on a shared field | Elegant, scales to arbitrary contributing nodes | Must be written and tested for correctness under concurrent, same-superstep updates |
16. Practical Python/code example
python
from langgraph.graph import StateGraph, END
def route_by_risk_score(state: "ClaimState") -> str:
"""Routes a claim to the appropriate handler based on its computed risk score."""
score = state["risk_score"]
if score < 0.2:
return "auto_approve"
elif score < 0.7:
return "manual_review"
return "fraud_investigation"
graph = StateGraph(ClaimState)
graph.add_node("score_risk", score_risk_node)
graph.add_node("auto_approve", auto_approve_node)
graph.add_node("manual_review", manual_review_node)
graph.add_node("fraud_investigation", fraud_investigation_node)
graph.set_entry_point("score_risk")
graph.add_conditional_edges(
"score_risk",
route_by_risk_score,
{"auto_approve": "auto_approve", "manual_review": "manual_review", "fraud_investigation": "fraud_investigation"},
)
for node in ["auto_approve", "manual_review", "fraud_investigation"]:
graph.add_edge(node, END)17. Production-quality example
A custom reducer with explicit, tested commutative behavior, addressing the section 8/9 concern directly:
python
from typing import Annotated, TypedDict
def merge_flagged_issues(current: list[str] | None, new: list[str]) -> list[str]:
"""
Merges flagged-issue lists from potentially multiple concurrent nodes,
de-duplicating and sorting for deterministic, order-independent output —
safe regardless of which contributing node's update is processed first.
Args:
current (list[str] | None): The field's existing value (None on first write).
new (list[str]): The new contribution to merge in.
Returns:
list[str]: The merged, de-duplicated, sorted list — identical regardless
of the order multiple concurrent updates are applied in.
"""
combined = set(current or []) | set(new)
return sorted(combined)
class LoanReviewState(TypedDict):
"""Loan review state with a shared, multi-contributor flagged-issues field."""
application_id: str
flagged_issues: Annotated[list[str], merge_flagged_issues]
async def credit_check_node(state: LoanReviewState) -> dict:
"""Contributes any credit-related flags to the shared, reducer-merged field."""
issues = await check_credit_issues(state["application_id"])
return {"flagged_issues": issues}
async def fraud_check_node(state: LoanReviewState) -> dict:
"""Contributes any fraud-related flags to the shared, reducer-merged field,
safely combining with credit_check_node's contribution regardless of order."""
issues = await check_fraud_issues(state["application_id"])
return {"flagged_issues": issues}Sorting the merged result (rather than leaving set order implicit) makes the reducer's output fully deterministic for a given set of inputs — directly addressing the non-commutative-reducer risk flagged in section 8 by construction, not just by convention.
18. Short exercise
Design a conditional edge for a document-processing graph that routes to extract_invoice_fields, extract_receipt_fields, or reject_unsupported_document based on a document_type field set by an earlier classification node. Write the routing function and the add_conditional_edges call, and then write one test case per branch (three tests total) verifying each destination is reached for the appropriate input.
19. Interview questions
- Explain why a conditional edge keeps a graph within Part 3.7's "workflow" category rather than making it an "agent," even though the exact path taken varies at runtime.
- Why must a custom reducer combining concurrent updates be written to be order-independent (commutative)?
- When would you choose separate state fields per contributing node over a shared field with a custom reducer, and vice versa?
20. FDE/customer scenario
Customer's compliance officer: "We need to be able to prove, for audit purposes, every possible path a claim could take through your review process."
This is precisely the auditability property conditional edges provide by construction (section 10): because every conditional edge's destination set is enumerable and fixed in the graph definition, you can produce a complete, exhaustive list of every possible path through the workflow directly from the code — a concrete, credible answer to a compliance requirement that a fully open-ended agent architecture (Part 3.7) could not provide with the same certainty.
Key takeaways
- Conditional edges route to a runtime-determined destination from a fixed, enumerable set — the concrete graph-level implementation of Part 3.7's routing pattern, distinct from an agent's genuinely open-ended action space.
- Custom reducers define how multiple nodes' updates to a shared field combine — must be written to be order-independent for correctness under concurrent, same-superstep updates.
- The enumerability of conditional-edge destinations is a genuine, provable auditability advantage worth stating explicitly in compliance-sensitive engagements.
Things you should be able to explain
- Why a conditional edge keeps a graph in "workflow," not "agent," territory.
- Why reducer commutativity matters for concurrent updates within one superstep.
Things you should be able to build
- A conditional-edge-routed graph with full branch test coverage, and a custom, order-independent reducer for a multi-contributor shared field.
Common mistakes
- Non-commutative custom reducers causing intermittent bugs under concurrent updates.
- Untested conditional-edge branches.
Recommended next chapter
03-checkpointing-persistence.md