Skip to course content
Free LangGraph course

LangGraph for Agentic Workflows

Unit 04.00: Routing on state, not on vibes

A router is the smallest piece of a graph and the one most likely to produce a bug you cannot reproduce. The fix is to make it read named fields and nothing else.

Explicit fields, exhaustive branches

A router should be readable as a truth table: for each combination of the fields it reads, exactly one destination. That is only possible if the fields are named and typed.

The code routes on error and rows, and runs three cases.

from typing import TypedDict
from langgraph.graph import StateGraph, START, END


class State(TypedDict):
    rows: int
    error: str
    route: str


def load(state: State):
    return {"route": ""}


def route(state: State):
    """Reads explicit fields. Every branch is reachable and testable."""
    if state["error"]:
        return "handle_error"
    if state["rows"] == 0:
        return "empty"
    return "process"


for name in ("handle_error", "empty", "process"):
    pass

g = StateGraph(State)
g.add_node("load", load)
for name in ("handle_error", "empty", "process"):
    g.add_node(name, (lambda n: (lambda s: {"route": n}))(name))
g.add_edge(START, "load")
g.add_conditional_edges("load", route,
                        {n: n for n in ("handle_error", "empty", "process")})
for name in ("handle_error", "empty", "process"):
    g.add_edge(name, END)
app = g.compile()

for case in [{"rows": 5, "error": ""}, {"rows": 0, "error": ""},
             {"rows": 5, "error": "timeout"}]:
    print(f"{str(case):40} -> {app.invoke({**case, 'route': ''})['route']}")

# The router reads named fields and nothing else. A router that inspects a
# free-text field, or asks a model which way to go, is a branch you cannot test.

Every branch is reachable and every case is testable by constructing a dictionary. There is no ordering subtlety beyond the one that is written down: error is checked first, so an errored run with zero rows takes the error path.

That ordering is a decision and it is visible in three lines. Compare with a router that inspects a free-text field for the word "error", where the same decision depends on wording nobody controls.

The mistake this prevents

The mistake is routing on the content of a model's output - asking it which way to go, or grepping its prose for a keyword. The branch is then as reliable as the phrasing, it cannot be tested exhaustively, and it changes when the model does. Have the model set a validated field, then route on the field.

Takeaway

Routers read explicit state fields and return a node name. If you cannot write the router as a truth table over named fields, the problem is the state design, not the router.