When should LangGraph routing live in a conditional edge instead of inside a node?
answer
- structure buys visibility and pause points
- every node is a step and a checkpoint
- branch on capability, not on formatting
- loops are always edges
- keep routers thin, put model calls in nodes
basics
~20 sMake the branch a conditional edge when someone needs to see, pause, resume, or retry at that decision point: edges are drawn in the graph, land on step boundaries, and split work into separately observable nodes. Keep trivial in-node conditionals inline.
solid answer
~50 sBoth are legal, so the criterion is what a graph edge buys. A conditional edge makes the decision **structural**: it appears in `get_graph().draw_mermaid()`, its destinations are separately scheduled nodes, and each destination is its own superstep — which is what gives you per-branch tracing, per-node retry policy, a checkpoint boundary, and a place a run can be paused or resumed. In-node branching buys none of that but costs nothing: no extra step, no extra checkpoint write, no extra name in the diagram. So the rule I use is: if the branch changes *which capability runs* — call a tool, escalate to a human, hand off to another agent, retry — it belongs on an edge. If it is data massaging inside one logical step (pick a prompt template, coerce a format, choose a default), it stays inline. The failure at both extremes is real: a graph with a node per `if` is unreadable, and a graph with one giant node has no auditable control flow at all.
go deeper
Know that a branch can live either inside a node's code or in a conditional edge, and that the edge form is what shows up as a branch in the graph.
Explain what the edge form buys — separate nodes mean separate steps, separate traces, and a branch visible in the drawn graph — and that inline branching avoids an extra step and an extra state key.
Argue the placement from operations: decisions that change capability, trigger external effects, or may need approval belong on edges because those are the points you must observe, retry, and resume at; keep routers pure and cheap.
Own it as a standard rather than a case-by-case call — where branches live, how routers declare destinations, and how coarse the decomposition should be, so that diagrams stay honest and checkpoint cost stays proportionate across every graph the team ships.
## The two forms Inside a node you can write any Python you like, including the branch itself, and return a single update. Alternatively you register a conditional edge, and LangGraph calls a router between supersteps to pick the next node. Nothing in the framework forces either choice, which is precisely why this comes up as a design question. ## What making it an edge actually buys 1. **Visibility in the artifact.** `graph.get_graph().draw_mermaid()` renders nodes and edges. A decision expressed as an edge (with `path_map` or a `Literal` return type declaring the destinations) is in the picture; a decision inside a node body is invisible there. On a system that other people operate, the diagram is documentation, and its accuracy is a property you either maintain or lose. 2. **Execution granularity.** Each destination is a node, so it gets its own entry in traces and per-node streaming, its own retry configuration, and its own boundary in the sequence of supersteps. If you want to know how often the graph escalated versus answered directly, the branch has to be structural — otherwise the counter lives in your own logging and drifts. 3. **Interruptibility.** Pausing, approving, and resuming happen at step boundaries between nodes. A decision buried inside a node cannot be a pause point; the same decision as an edge means the destination node can be gated. 4. **Testability of the decision on its own.** A router is a pure function of state. You can enumerate its branches in unit tests without constructing a model client. In-node branches are only reachable through the node's whole body. ## What it costs - **Another name to maintain.** Every destination is a node with a name, wiring, and a place in the diagram. Ten of them for ten trivial variations makes the graph less legible, not more — the diagram stops being a summary. - **Another superstep.** Steps are cheap but not free: with a checkpointer configured, each one writes a checkpoint, which is I/O and storage per run. A hot path split into six nodes has six times the checkpoint writes of one node doing the same work. - **State pressure.** Anything the split-out node needs must be in the state schema, because nodes communicate only through state. Inline logic can use a local variable; edge-split logic often means adding a key that exists purely as a hand-off, and every such key is a thing future readers must reason about. ## A usable rule Ask what changes downstream of the decision: - **Different capability, external effect, or human involvement** — tool call versus direct answer, escalate versus proceed, retry versus give up, hand work to a different agent: make it an edge. These are the moments an operator wants to see, and often the moments where a run must be able to stop. - **Different data handling inside one logical step** — choose a template, normalise a field, pick a default model parameter, short-circuit on an empty list: keep it inline. Splitting these produces nodes that only a compiler could love. - **Loops** are always edges, because a cycle *is* an edge in this model, and because the loop counter and its termination condition deserve to be visible. ## The second-order concerns a lead owns - **Diagram fidelity as a standard.** Decide as a team that branches which matter are edges and that routers declare their destinations (via `path_map` or a `Literal` return annotation). Otherwise the drawn graph slowly becomes a lie and reviews stop catching control-flow changes. - **Where the cost lives.** Each structural branch multiplies the paths a change can affect. Fine-graining a graph does not add capability; it adds surface. Prefer the coarsest decomposition that still exposes the decisions you need to audit. - **Routers stay thin.** A 60-line router with model calls in it is a node wearing an edge's clothes — it is not checkpointed, not retried, and not visible as work. If a decision needs a model call, put the call in a node, write its verdict to state, and let a two-line router read that key. This keeps the expensive thing observable and resumable. - **Consistency across a fleet.** When several graphs are maintained by different people, the value comes from every graph putting the same class of decision in the same place. That convention is worth more than any individual choice. ## The honest summary Conditional edges are not free structure and inline branches are not laziness. Edges are how you pay for observability, resumability, and an accurate picture; inline branching is how you keep the picture small enough to read. The senior mistake is treating node count as a proxy for engineering quality in either direction.
- Is it acceptable to make a model call inside the routing function itself?Avoid it. A router is not a node: it is not checkpointed, not retried, not traced as work, and not a place a run can pause — so an expensive call there is invisible and unrecoverable. The pattern that keeps everything observable is a classifier node that makes the call and writes its verdict to state, followed by a two-line router that reads that key. The decision then also becomes replayable from the checkpoint.
- What is the cost of splitting a hot path into many small nodes just to expose its branches?Each node is a superstep, so with a checkpointer every split adds a checkpoint write per run, and every hand-off between nodes needs a state key that inline code would have kept in a local variable. You also enlarge the diagram until it stops summarising anything. Split where you need to observe, retry, or pause; keep the rest inline and let the node body hold ordinary control flow.
- How do you keep a growing graph's drawn diagram trustworthy as a description of control flow?Make it a review standard: decisions that change which capability runs are edges, and every router declares its destinations through path_map or a Literal return annotation so the rendering shows real targets rather than assuming any node is reachable. Then the wiring file is the single place a reviewer reads control flow, and a pull request that changes routing shows up as a change to that file.
saying these in an interview costs you the question
- Treating more nodes as automatically better engineering
- Putting expensive model calls inside a routing function
- Assuming an in-node branch can still be a pause or approval point
- Ignoring that every extra node adds a checkpoint write per run
- Splitting formatting choices into nodes while leaving escalation inline