Chapter 18
Chapter 18 . Agent frameworks . LangGraph

LangGraph: agents you can draw

LangChain's create_agent decides the path for you. LangGraph lets you draw it: nodes do the work, edges set the order, routers decide, a checkpointer remembers, and an interrupt waits for a person. Eleven lessons build from a one-node graph to a pipeline that turns a Jira ticket into a tested, triaged report.

11
Lessons
7 run with no API key; 008, 009 and the capstone need a free Groq key.
13
Hand-drawn graphs
One per lesson, plus the overview and a then-and-now comparison.
7
Capstone nodes
fetch, analyse, plan, review, execute, triage, write_report.
1.2.12
langgraph version
Every output on these pages was produced on this version.

01Why a graph

An agent built with create_agent (chapter 17) is a loop the model drives: it decides when to call a tool and when to stop. That is fine until a test pipeline needs a fixed path, a retry cap, a step that runs in parallel, or a pause for a person to approve. LangGraph is the engine underneath create_agent, and it lets you declare that path yourself.

LangFlow (ch 5)LangChain create_agent (ch 17)LangGraph (ch 18)
You build it bydragging boxescalling one functiondeclaring nodes and edges
Loopsawkwardhidden inside the agentfirst class
Who decides the paththe flowthe modelyou, with the model's help where you choose
Pause for a humannonointerrupt()
Memory between runsper sessionvia LangGraphcheckpointer + thread_id
LangGraph at a glance: nodes, a conditional edge with a loop back, an interrupt for human approval, the shared state and the checkpointer
Nodes do the work, edges set the order, conditional edges decide, and the state carries data through the graph.

02The vocabulary

Eight words cover the whole library. Each lesson adds one of them.

ConceptWhat it isIn a test suite
StateA typed dict every node can read and updateThe test-run record shared across steps
NodeA Python function that does one job and returns what it changed"run the test", "parse the report"
EdgeWhere to go nextsetup, then execute, then teardown
Conditional edgeA router function that returns the next node's namepassed to log, failed to Jira, flaky to quarantine
LoopAn edge that points back to an earlier noderetry a failed test, at most three times
ReducerHow parallel writes to one key are mergedAPI, UI and perf results joined into one list
CheckpointerSaves the state after each step, per thread_idnightly and smoke keep separate histories
InterruptPauses the graph until a person answersa QA lead approves before tests are quarantined
One rule to remember: a node never returns the whole state. It returns only the keys it changed, and LangGraph merges them in.

03Live demo: run a graph step by step

This is lessons 4, 6 and 7 in one graph. run_test loops until the test passes or the cap is reached. A test that passed only after failing is flaky, so the graph pauses at approve and waits for you. Every step is saved as a checkpoint you can rewind to. Use Step to move one node at a time.

Test-result graph: retry, route, approveNo API key needed
START run_test approve quarantine report END
data-testid=lg-scenariodata-testid=lg-maxdata-testid=lg-stepdata-testid=lg-rundata-testid=lg-reset
Event log
    Python for this step
    
          State
          
    
          Verdict
          
    Checkpoints (click to rewind)
    Approving or declining resumes the run, the same as app.invoke(Command(resume=...), cfg): it continues to the end or to the next interrupt.

    04The eleven lessons

    Lessons 1 to 7 run offline and cover every core idea. Lessons 8 and 9 put a model inside the graph. Lessons 10 and 11 are complete QA tools.

    Lesson 01 Your first graph A graph is a set of nodes joined by edges. Every node reads the shared state and returns only what it changed. No key Lesson 02 A sequential pipeline Edges run nodes in order. Each step adds its result to the state, and nothing is lost on the way. No key Lesson 03 Conditional routing A router function reads the state and returns the name of the next node. Decisions live in that one function. No key Lesson 04 Retries with a limit An edge can point back to an earlier node. That makes a loop, and every loop needs a cap. No key Lesson 05 Parallel branches and reducers Independent nodes run at the same time. A reducer defines how their results merge into one key. No key Lesson 06 Checkpoints and memory A checkpointer saves the state after every step. Each thread_id keeps its own history. No key Lesson 07 Human approval gates interrupt() pauses the graph and waits for a person. Command(resume=...) continues it with their answer. No key Lesson 08 An LLM inside a node A node can call an LLM. Its structured answer decides which edge the graph takes next. Needs a key Lesson 09 The ReAct agent loop The model reasons, calls a tool, reads the result, and repeats until it can answer. Needs a key Lesson 10 Flaky test analyzer Chapter 5's flaky test finder, rebuilt with a decision, an approval pause and an optional LLM explanation. Key optional Lesson 11 From Jira ticket to test report Give it a Jira ticket. It plans the tests, runs them in a browser, and explains what failed and why. Needs a key

    05The whole API on one screen

    A condensed sheet of the calls the lessons use, in the order you write them. It runs as is.

    cheatsheet.py
    from typing import Literal, TypedDict
    from langgraph.graph import StateGraph, START, END
    from langgraph.checkpoint.memory import InMemorySaver
    
    class State(TypedDict, total=False):      # 1. the shared state
        status: str
        action: str
    
    def check(state: State) -> dict:          # 2. a node returns only what it changed
        return {}
    
    def route(state: State) -> Literal["fix", "done"]:   # 3. a router names the next node
        return "fix" if state["status"] == "failed" else "done"
    
    builder = StateGraph(State)
    builder.add_node("check", check)
    builder.add_node("fix", lambda s: {"action": "filed a bug"})
    builder.add_node("done", lambda s: {"action": "green"})
    builder.add_edge(START, "check")                                # 4. plain edges
    builder.add_conditional_edges("check", route, ["fix", "done"])  # 5. a fork
    builder.add_edge("fix", END)
    builder.add_edge("done", END)
    
    app = builder.compile(checkpointer=InMemorySaver())  # 6. memory per thread_id
    cfg = {"configurable": {"thread_id": "run-1"}}
    print(app.invoke({"status": "failed"}, cfg))         # {'status': 'failed', 'action': 'filed a bug'}
    flowchart LR
      S((START)) --> C[check]
      C -->|route: failed| F[fix]
      C -->|route: passed| D[done]
      F --> E((END))
      D --> E
    The cheat sheet as a graph: one node, one router, two leaves.

    06Set up and run

    One virtual environment for the whole chapter. Python 3.12 and langgraph==1.2.12 are what the outputs on these pages came from.

    terminal
    cd chapter_18_LangGraph
    python3 -m venv .venv && .venv/bin/pip install -r requirements.txt
    cp .env.sample .env        # only needed for 008, 009 and the LLM notes in 010
    cd src/chapters
    ../../.venv/bin/python 001_Hello_Graph.py

    The capstone has its own requirements (Playwright and a browser):

    terminal
    .venv/bin/pip install -r capstone/requirements.txt
    .venv/bin/python -m playwright install chromium
    cd capstone && ../.venv/bin/python check.py   # offline: should print all checks passed

    07Gotchas on langgraph 1.2.12

    • Cap every loop yourself. The default recursion limit is 10007 steps, not 25. Pass {"recursion_limit": N} or keep a counter in state.
    • Parallel writers need a reducer. Without one: InvalidUpdateError: At key 'results': Can receive only one value per step.
    • Type hints are read as input schemas. A node typed with a narrower state only sees those keys. Leave a fan-in node untyped or give it the full state.
    • Interrupts need a checkpointer and the same thread_id. Resume on another thread and the graph simply starts over.
    • A resumed node runs again from the top. Put side effects after interrupt(), or make them safe to repeat.
    • Model output is untrusted input. The capstone checks every LLM-written step against the real app before Playwright runs it.