Skip to course content
Free LangGraph course

LangGraph for Agentic Workflows

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.