LangGraph or the raw loop
This block shows what LangGraph adds to the loop you already wrote, what it leaves to you, and how to tell when it earns its place.
Refunds above €200 at Northwind now wait for a manager, who may answer tomorrow morning. The server restarts for every deploy. The loop from the earlier block keeps messages in a local variable, so a refund waiting overnight has nowhere to live.
A case folder in an office
Picture a paper folder moving through an office. Each desk reads it, adds a sheet and sends it on. A sign at each desk says where it goes next, and some signs depend on what the folder says. Each time the folder moves on, a clerk files a photocopy under the case number. If the manager is out, the folder waits in a cabinet, and nobody has to stand there holding it.
LangGraph is that office.
In the office | In LangGraph | What it is |
|---|---|---|
The folder | State | Shared data that every node can read, here a |
A desk | Node | A function that reads state and returns updates |
A sign | Edge | Which node runs next, fixed or decided by a function |
The photocopy | Checkpoint | State saved at each step |
The case number | | Which saved state to load |
Your loop from the earlier block is an office with two desks. The if on the stop reason has become a sign.
For this plain loop you gain nothing: the logic is the same and you have new words to learn. The gain comes from the photocopies and the cabinet.
One refund through the office
Here is the folder for a refund. The model's part is left out so that the mechanism stays visible.
class Case(TypedDict):
order_id: str
amount_cents: int
approved: bool
log: Annotated[list[str], operator.add]A node returns only what it changes. By default a new value replaces the old one. log has a reducer, operator.add, so each update is appended. Without it, every desk would throw away the sheets before it.
The desk that waits calls interrupt:
def ask_manager(case: Case):
answer = interrupt({"order_id": case["order_id"],
"amount_cents": case["amount_cents"]})
return {"approved": answer == "approve",
"log": [f"manager said {answer}"]}The signs are ordinary functions that name the next node:
def route(case: Case) -> Literal["ask_manager", "refund"]:
return "ask_manager" if case["amount_cents"] > 20000 else "refund"
builder.add_conditional_edges("propose", route)Your code starts the run with a case number, and the graph stops at the manager's desk:
config = {"configurable": {"thread_id": "case-4417"}}
result = graph.invoke({"order_id": "4417", "amount_cents": 34900,
"approved": False, "log": []}, config)
result["__interrupt__"] # the order and amount for the manager's screenThe process can now exit. The next morning, a new process compiles the same graph with the same checkpointer and sends the answer:
graph.invoke(Command(resume="approve"), config)The value in Command(resume=...) becomes what interrupt returns. LangGraph's docs now recommend driving such runs with stream_events(..., version="v3"). That mode is still marked experimental, and invoke works and is shorter here.
The picture shows five checkpoints for three nodes: one empty start, one for the input and one after each node. ask_manager ran twice, because, as the block on durable runs said, a node restarts from its top. SqliteSaver is for local work. LangGraph's docs point to PostgresSaver for production. By default a checkpoint is written in the background while the next step runs (durability="async"), so there is a small risk that a crash loses the latest one. durability="sync" waits for each write before moving on.
A new thread_id means an empty folder. A retry that invents a fresh id starts the case again and does not resume it.
The same pause in the raw loop
The raw loop handles this better than people expect, because messages is already plain data. Save it, stop, and continue when the answer comes:
def pause(case_id, messages, results, block):
db.save(case_id, {"messages": messages, "results": results,
"pending": block})
def resume(case_id, decision):
run = db.load(case_id)
answer = apply_decision(run["pending"], decision) # a tool_result
run["messages"].append({"role": "user",
"content": run["results"] + [answer]})
return run_agent(run["messages"])The results of the other calls in that reply are kept, because on Anthropic's API they all go in one message. With one pause point, this is a table and two functions, and every prompt stays in your code. The comparison changes as the needs grow:
You need | Raw loop | LangGraph |
|---|---|---|
One approval before one tool | A table and two functions | |
Pick up after a crash at any step | Save after every step, write the recovery | Checkpoint per step, and |
Fixed steps mixed with model steps | | Nodes and conditional edges |
Parallel branches where one fails | Track yourself which ones finished | Finished nodes are not rerun |
Inspect or replay an old run | Build it | |
The last question in the picture is about durable-execution engines such as Temporal, from the block on durable runs.
What you pay, and what you still own
Anthropic's advice from 2024 is to start with LLM APIs directly and, if you take a framework, to understand the code under it, because wrong assumptions about it are a common source of errors. LangGraph sits low in this respect: its docs say it does not abstract prompts or force an architecture.
The price is real anyway. You learn its execution model, your state has to survive serialization, and old checkpoints pile up until you prune them or set a retention policy. Between 10 November 2025 and 10 November 2026 it had 50 releases on PyPI, patches and 10 pre-releases included, so pin the version.
A graph does not make a refund correct. The ownership check, the amount check and the idempotency key from the earlier blocks still live in your refund node.
Name the layer correctly. create_agent is imported from langchain.agents: it is LangChain's ready-made agent loop, built on top of LangGraph. LangGraph's own builder is StateGraph.
Conclusion
- LangGraph gives you state, nodes, edges, a checkpoint per step, and pause and resume by
thread_id. - One pause on one path is cheap in the raw loop. Several pauses, branches and recovery are where the graph pays.
- Prompts in LangGraph stay yours, and so do the checks on what a tool may do.