In LangGraph, how does add_conditional_edges decide which node runs next?
answer
- it is a function of state, not a node
- runs after the source node's update lands
- returns a destination, never a state update
- a list return means parallel fan-out
- unmapped return value raises at runtime
basics
~20 sadd_conditional_edges attaches a router function to a source node. After that node runs and its update is applied, LangGraph calls the router with the current state and uses its return value to name the next node, or END to stop that path.
solid answer
~50 s`builder.add_conditional_edges(source, path, path_map)` registers a plain Python callable — the router — against a source node. When the source node finishes, LangGraph applies its state update first, then calls the router with the resulting state; the router may be sync or async. Its return value is either a node name, `END`, a key of the optional `path_map` that resolves to one of those, or a **list** of them, which fans out to several nodes in the next step. The router is a decision function only: it is not a node, it cannot write to state, and it is not checkpointed or emitted as a step in streaming. Anything the decision needs must already be in state, written by the node before it. If the router returns a value that maps to no destination, the run raises at that point rather than at compile time.
code
python · 24 linesfrom typing import Literal, TypedDict
from langgraph.graph import END, START, StateGraph
class State(TypedDict):
count: int
def tick(state: State) -> dict:
return {"count": state["count"] + 1}
def route(state: State) -> Literal["again", "stop"]:
return "again" if state["count"] < 3 else "stop"
builder = StateGraph(State)
builder.add_node("tick", tick)
builder.add_edge(START, "tick")
builder.add_conditional_edges("tick", route, {"again": "tick", "stop": END})
graph = builder.compile()
print(graph.invoke({"count": 0})) # {'count': 3}go deeper
Know that add_conditional_edges takes a source node and a function, and that the function returns where to go next — a node name or END. Be able to write a two-branch router over a simple state key.
Explain that the router is called with the full post-update state, may return a list to fan out, and cannot write state. Mention that an unmapped return value fails at runtime, not at compile time.
Show the operational angle: routers are unit-testable pure functions, unbounded cycles need an explicit budget in state rather than a raised recursion limit, and routing decisions worth auditing must be written to state by a node because the router is not checkpointed.
Own the placement question — which decisions deserve to be edges in the graph at all, how much logic a router may hold before the diagram stops describing the system, and how branch structure interacts with observability and resumability across a fleet of graphs.
## The problem conditional edges solve A LangGraph graph is nodes plus edges. A node is a function that receives the state and returns a partial update; an edge says what runs next. `add_edge("a", "b")` is unconditional — after `a`, always `b`. Agent-shaped work needs the other kind: after the model node runs, go to the tool node if the model asked for a tool, otherwise go to the responder or finish. `add_conditional_edges` is how that branch becomes an explicit, drawable part of the graph instead of an `if` buried in a node body. ## The call `builder.add_conditional_edges(source, path, path_map=None)` takes three things: the name of the source node (`START` is also legal as a source), the router callable, and an optional mapping from the router's return values to destination node names. When `path_map` is omitted, the router must return actual node names or `END`. ## The router's contract The router is an ordinary function, not a node. It is called with the **full graph state as it exists after the source node's update has been merged** — not with whatever dict the source node returned. So a router can read any key in the schema, including keys written many steps earlier. It may be `def` or `async def`. What it may return: - a single node name string; - `END` (the sentinel exported from `langgraph.graph`, whose value is the string `"__end__"`), which terminates that path; - a key of `path_map`, which is then resolved to a node name or `END`; - a list of any of the above, which schedules every named node to run in the next superstep, in parallel. What it may **not** do: change state. A router's return value is interpreted purely as routing. If you compute something useful inside the router and want it persisted, you have to move that computation into the source node and write it to state there. ## Where it sits in execution LangGraph executes in supersteps: a set of nodes runs, their writes are applied to the channels, a checkpoint is written if a checkpointer is configured, and then the next set of nodes is chosen. The router runs at that boundary. Consequences that show up in practice: the router is not a separately checkpointed step, so you cannot pause at it; it does not appear as a node in the drawn graph or in per-node streaming output; and a slow or throwing router fails the transition rather than a node. ## Fan-out and termination Returning a list is the simplest fan-out mechanism: `return ["summarize", "classify"]` runs both nodes concurrently in the next step, and their writes must be reconcilable — a plain key written by both raises, an annotated key with a reducer accumulates. Returning `END` from one branch ends only that path; other paths already scheduled continue to run. ## Typical failure modes 1. **A return value with no destination.** Node names are not validated against arbitrary router outputs at compile time, so a typo or an unmapped literal surfaces at runtime, mid-run, often after a paid model call. 2. **Routing on state the node never wrote.** The router sees the post-update state, so a router reading `state["decision"]` when the node forgot to return that key reads the previous value (or raises a `KeyError` on a TypedDict that never had it). 3. **Unbounded loops.** Conditional edges are what make cycles possible — a router that keeps returning the same node loops forever. LangGraph guards this with a recursion limit (25 steps by default, overridable per call via the `recursion_limit` config key) and raises `GraphRecursionError` when it is exceeded. Treat that error as a routing bug, not as a limit to raise blindly; usually the fix is a counter or an attempt cap in state that the router checks. 4. **Hiding the decision.** If the router is 40 lines of business logic, the graph diagram lies about where the complexity lives. Routers should be small and readable. ## Testing Because the router is a plain function of state, it is the cheapest thing in the whole graph to unit test: call it with hand-built state dicts and assert on the returned label. Do that instead of running the whole graph to exercise a branch.
- Can the router itself update the state while it decides?No. Whatever a router returns is interpreted as routing, never as a state update, and there is no second return channel. If the decision produces data worth keeping — a chosen strategy, a parsed intent, a retry counter — the source node must compute and return it, and the router then reads it from state. Keeping routers pure also makes them trivially unit-testable as functions of state.
- What stops a conditional edge that keeps routing back to the same node from looping forever?LangGraph counts supersteps and raises GraphRecursionError once the recursion limit is hit — 25 by default, settable per invocation through the recursion_limit config key. That is a backstop, not a design. The real fix is an explicit budget in the state: have the node increment an attempt counter and have the router return END once it crosses a threshold, so the graph terminates deliberately rather than by exception.
- Where does the router run relative to checkpointing and streaming?At the boundary between supersteps, after the source node's writes are applied. It is not a node, so it produces no checkpoint of its own, appears in no per-node stream output, and cannot be a pause point. If you need to observe or intervene on a branch decision, the decision has to be materialised into state by a node first — then it is visible in the state snapshot.
saying these in an interview costs you the question
- Thinking the router receives only the source node's return dict
- Believing a router can return a state update alongside the destination
- Assuming node names in a router are validated at compile time
- Treating the router as a node that shows up in traces and checkpoints
- Raising recursion_limit instead of fixing a router that never terminates