Unit 02.02: Edges, and who decides the next step
There are two kinds of edge. One says "always go here." The other asks a function, and that function is where most routing bugs live.
A router reads state and returns a name
A conditional edge takes a routing function and a mapping from its return values to node names. The function gets the state and returns one string.
The code routes on a score and runs the graph twice.
from typing import TypedDict
from langgraph.graph import StateGraph, START, END
class State(TypedDict):
score: int
path: list
def grade(state: State):
return {"path": state["path"] + ["grade"]}
def pass_step(state: State):
return {"path": state["path"] + ["pass"]}
def fail_step(state: State):
return {"path": state["path"] + ["fail"]}
def route(state: State):
"""The router reads state and returns a name. That is all it does."""
return "pass_step" if state["score"] >= 50 else "fail_step"
g = StateGraph(State)
for name, fn in [("grade", grade), ("pass_step", pass_step), ("fail_step", fail_step)]:
g.add_node(name, fn)
g.add_edge(START, "grade")
g.add_conditional_edges("grade", route, {"pass_step": "pass_step", "fail_step": "fail_step"})
g.add_edge("pass_step", END)
g.add_edge("fail_step", END)
app = g.compile()
for score in (80, 20):
print(f"score {score:3} -> {app.invoke({'score': score, 'path': []})['path']}")
# A fixed edge says "always next". A conditional edge asks a function. The
# function gets state and returns a node name -- it must not do work itself.
route does one thing: reads score, returns a node name. It does no work, calls nothing, and has no side effects - which means it can be tested by calling it with a dictionary, without building a graph at all.
The mapping passed to add_conditional_edges matters more than it looks. It is the list of destinations the router is allowed to return, so a typo in the router surfaces as a clear error rather than as a silently unreachable branch.
The mistake this prevents
The mistake is doing work inside the router - fetching something, calling a model, updating a counter. A router that has side effects runs at a moment you have not thought about, and its effects are not captured in any node's returned update, so they never reach the checkpoint. Routers read; nodes write.
Takeaway
Fixed edges say always. Conditional edges ask a pure function that reads state and returns a node name. Keeping routers side-effect-free is what makes branches testable in isolation.
