Skip to course content
Free LangGraph course

LangGraph for Agentic Workflows

Unit 11.01: Building the conditional and error paths

The routing is where the design becomes a graph, and it is the part with four distinct outcomes to get right.

One router, four destinations

after_review handles everything: error, approval, revise, and exhaustion. The ordering of its checks is the specification of what takes precedence.

The code builds the full graph and runs it.

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

MAX_ATTEMPTS = 3


class State(TypedDict):
    findings: list
    draft: str
    approved: bool
    attempts: int
    error: str
    outcome: str


def research(state: State):
    if not state["findings"] and state["attempts"] > 0:
        return {"error": "no sources found"}
    return {"findings": ["s1", "s2"]}


def draft(state: State):
    return {"draft": f"draft from {len(state['findings'])} sources",
            "attempts": state["attempts"] + 1}


def review(state: State):
    return {"approved": state["attempts"] >= 2}


def after_review(state: State):
    if state["error"]:
        return "handle_error"
    if state["approved"]:
        return "publish"
    return "draft" if state["attempts"] < MAX_ATTEMPTS else "give_up"


g = StateGraph(State)
for name, fn in [("research", research), ("draft", draft), ("review", review)]:
    g.add_node(name, fn)
for name, text in [("publish", "published"), ("give_up", "gave up"),
                   ("handle_error", "errored")]:
    g.add_node(name, (lambda t: (lambda s: {"outcome": t}))(text))
    g.add_edge(name, END)
g.add_edge(START, "research")
g.add_edge("research", "draft")
g.add_edge("draft", "review")
g.add_conditional_edges("review", after_review, {
    "draft": "draft", "publish": "publish",
    "give_up": "give_up", "handle_error": "handle_error"})

print(g.compile().invoke({"findings": [], "draft": "", "approved": False,
                          "attempts": 0, "error": "", "outcome": ""}))

Error is checked first, so a run that errored and was somehow also approved takes the error path. Approval is checked before the attempt ceiling, so a run approved on its final permitted attempt publishes rather than giving up.

Both orderings are decisions. Written as a sequence of early returns they are three lines and unambiguous; expressed as nested conditionals or spread across multiple routers, the same decisions become something you have to reconstruct.

The mistake this prevents

The mistake is one router per condition - a separate conditional edge for the error check and another for the approval check. Precedence is then implicit in the graph's shape rather than explicit in one function, and changing it means rewiring edges rather than reordering three lines.

Takeaway

Put the whole routing decision in one function as ordered early returns. The order encodes precedence, and precedence is a decision that should be readable in one place.