Module 03
LangGraph Stateful Graphs
On this page
In plain words
Think of an IRCTC booking that dies on the payment page. You want to come back to that page, not retype everything. A long agent run is the same. So build the agent as a small flowchart: one state dictionary, small functions as steps, arrows between them, and save the full state after every step. Then a crash at step 38 costs you one step, not thirty-eight.
How it flows
- 1Define state
- 2Write nodes
- 3Connect edges
- 4Save checkpoint
- 5Resume on failure
A tiny example
state = {"step": 0, "route": ""}
node = "classify"
while node != "END":
state.update(run_node(node, state))
checkpoint.save(session_id, state)
node = next_node(node, state)
state = checkpoint.load(session_id)Notice the save sits inside the loop after every node, so the last line can pick the run back up exactly where it stopped.
What you will learn
- How to think of an agent as a small machine with steps, not one big function call.
- What a checkpoint is, and why saving state after every step saves you hours.
- The three common shapes for arranging many agents: supervisor, swarm, hierarchical.
- Where this design breaks, and how interviewers ask about it.
The problem, simply
See, think of booking a Tatkal ticket on IRCTC. You fill passenger details, you pick a berth, you go to the payment page, you enter UPI, and then at the last screen the site says "session expired". Now what? You do not restart from your name and age, right? A good site brings you back to the payment step with everything you typed still there.
Now imagine your site made you retype all four passengers every single time. That is exactly what happens with most agent code.
An agent doing a real job is not one call to a model. It is a long chain of steps. Read the ticket, search the codebase, edit a file, run tests, read the failure, edit again. Forty steps is normal. And step 38 is exactly where the network hiccups.
If your agent keeps everything in local Python variables, a crash at step 38 wipes all of it. You pay for all 38 steps again. So the real problem is not "how do I call the model". It is: where does this run's memory live, and can I bring it back?
The idea
The agent is a state machine
Basically, stop thinking "function that calls an LLM". Start thinking "small flowchart".
There is one object called state that holds everything about this run: the messages so far, which step number we are on, which branch we took, whether a human approved. Every step of the agent reads that state and returns a small update to it.
Four pieces make up the whole design:
- State — one typed dictionary. Everyone reads it, nobody hides data outside it.
- Nodes — plain functions. They take the state and return a small update dict. Nothing else.
- Edges — arrows saying which node runs next. Some arrows are fixed, some are chosen by looking at the state.
- START and END — the two ends of the flow.
The framework everyone names for this is LangGraph, a Python library that lets you build an agent as exactly this kind of graph. You may also hear "state graph" — same thing.
- 1START
- 2classify
- 3route by state
- 4refund node
- 5human gate
- 6send
- 7END
Checkpointing, the actual magic
Here is the trick. After every single node returns, the runtime takes the whole state, converts it into plain text or bytes, and writes it to a store — SQLite, Postgres, Redis, whatever you like. That saved copy is a checkpoint. The thing that writes it is called a checkpointer.
Now failure is boring. The run died at step 38? Load the checkpoint from step 37 and continue. That is all. This is called durable execution — a fancy phrase meaning "your long run survives a crash".
TipTip: Give every run a session id. Checkpoints are keyed by that id, so resume is just
load(session_id).
An edge that thinks
A conditional edge is an arrow whose destination is decided by a function of the state. Suppose Priya writes a support agent for a Flipkart-style store. Her classify node reads the customer message and puts route = "refund" into state. The conditional edge after classify looks at state["route"] and sends the run to the refund node instead of the bug node.
Concretely: Priya's agent gets "my order 4127 never arrived, ₹2,499 gone". Classify sets route to refund. The refund node drafts a reply. Then a human gate node stops the run, because nobody wants an agent refunding ₹2,499 on its own. State is saved. Two hours later Sneha from support opens that saved state, sets human_approval = True, and resumes. The run continues from the send node — classify and refund do not run again.
That last part is human-in-the-loop. It is almost free here, because the state was already serialized between nodes. A human is just one more thing that edits the state.
Streaming and memory
Since state moves node by node, each node can also push out partial output as it works. That is streaming — why a chat UI shows text appearing instead of freezing.
Memory has two kinds. Short-term is the conversation inside this run, sitting in the state. Long-term survives across runs, and needs the checkpointer plus a separate store.
Three ways to arrange many agents
When one agent is not enough, you connect several. Three shapes come up again and again:
- 1Supervisor routes
- 2Swarm hands off
- 3Hierarchy nests
- Supervisor — one central router agent decides which specialist handles the task. Like a team lead assigning work.
- Swarm — no boss. Agents hand off to each other directly through shared tools. Peer to peer.
- Hierarchical — supervisors managing supervisors. Each sub-team is itself a graph used as a single node. A graph inside a graph is called a subgraph.
Remember: nodes are functions, edges are arrows, and state is the only thing that is real. If data is not in state, resume cannot bring it back.
Build it
"""A tiny stateful graph with checkpoint and resume. Standard library only."""
import json
# ---- the checkpointer: saves full state after every node ----
class Checkpointer:
def __init__(self):
self.store = {} # pretend this is SQLite
def save(self, sid, state):
self.store[sid] = json.dumps(state) # must serialize, so keep state plain
def load(self, sid):
return json.loads(self.store[sid]) if sid in self.store else None
# ---- nodes: each takes state, returns a small update ----
def classify(state):
text = state["message"].lower()
route = "refund" if "refund" in text or "money" in text else "bug"
return {"route": route, "step": state["step"] + 1}
def refund(state):
return {"draft": "Refund of Rs 2499 will reach you in 3 days.",
"step": state["step"] + 1}
def bug(state):
return {"draft": "Our team is checking the issue.", "step": state["step"] + 1}
def human_gate(state):
if not state["approved"]: # stop the run, state is already saved
raise PermissionError("waiting for human approval")
return {"step": state["step"] + 1}
def send(state):
return {"sent": state["draft"], "step": state["step"] + 1}
NODES = {"classify": classify, "refund": refund, "bug": bug,
"human_gate": human_gate, "send": send}
# conditional edge: next node is a function of state
EDGES = {"classify": lambda s: s["route"], "refund": lambda s: "human_gate",
"bug": lambda s: "human_gate", "human_gate": lambda s: "send",
"send": lambda s: "END"}
def run(state, node, cp, sid):
while node != "END":
try:
state.update(NODES[node](state))
except PermissionError as e:
print(f" paused at '{node}': {e}")
return state, node # resume point
cp.save(sid, state) # checkpoint after every node
print(f" step {state['step']}: finished '{node}'")
node = EDGES[node](state)
return state, "END"
cp = Checkpointer()
start = {"message": "I want my money back for order 4127", "step": 0,
"route": "", "draft": "", "sent": "", "approved": False}
print("first run:")
_, stuck = run(start, "classify", cp, "priya-1")
print("resume after Sneha approves:")
saved = cp.load("priya-1") # exact state from the last good node
saved["approved"] = True
final, _ = run(saved, stuck, cp, "priya-1")
print("sent:", final["sent"], "| total steps:", final["step"])Look at three things in the output. One, the first run stops at human_gate and never reaches send. Two, the resume starts at human_gate, not at classify — classify and refund are not repeated. Three, the step count keeps counting from where it stopped, because the number came back from the saved state, not from a fresh variable.
Where you will see this
- Coding agents like Claude Code and Cursor, which run long edit-test-fix loops and must survive a dropped connection.
- Customer-support bots at banks and e-commerce companies, where a refund step needs a human to approve before it goes through.
- Swiggy or Zomato style assistants that check an order, then a delivery partner, then raise a ticket — different branches per case.
- Approval workflows in general: expense claims, content moderation, anything where a human sits in the middle of an automated flow.
Common mistakes
- Checkpointing only the chat messages. Then tool results and memory writes are outside the checkpoint, and resume brings back a half-empty run. Serialize the full state.
- Non-deterministic nodes. If a node uses
random, the current time, or an uncontrolled external call, replaying it gives a different answer and the resumed state no longer matches. Capture those values into state instead. - Every edge conditional. A graph where everything branches is impossible to reason about or debug. Keep mostly straight chains with a few branches.
- Fat objects in state. If state cannot be turned into JSON cleanly, it cannot be checkpointed. Store file paths or ids, not open connections.
- Treating resume as a retry. Retry re-runs the failed step blindly. Resume restores exact state first, then continues. Different things.
If they ask in an interview
Q: Why model an agent as a graph instead of a simple loop?
A: A loop keeps its progress in local variables, so a crash loses everything. A graph puts all progress in one typed state object and saves it after every node. That gives you resume, human approval in the middle, and a clear picture of what ran.
Q: What is durable execution and how is it implemented?
A: It means a long run can survive a failure. After each node the runtime serializes the whole state and writes it to a checkpointer such as SQLite or Postgres, keyed by a session id. On restart you load that checkpoint and continue from the next node.
Q: What breaks resume?
A: Anything not captured in state. Random seeds, current time, and external API responses inside a node make it non-deterministic, so replaying does not reproduce the same state update. Partial checkpoints break it too, because tool state is left behind.
Try these
- Add a confidence score to
classify. If it is below 0.6, send the run straight to aneeds_humannode instead of refund or bug, then resume after setting the route by hand. - Replace the dictionary store with a real
sqlite3table of(session_id, step, state_json). Keep every checkpoint, not just the last one, and add a function to rewind to any step. - Break resume on purpose: put
random.random()inside a node and store it in state. Run, resume, and see how the values differ. Then fix it by generating the number once and reading it from state. - Make each node print a partial line before returning, so you get a streaming-style trace. Notice how little the node itself has to change.
Words, simply
| Word | Meaning in simple words |
|---|---|
| State | One dictionary holding everything about this run |
| Node | A plain function that reads state and returns an update |
| Edge | An arrow saying which node runs next |
| Conditional edge | An arrow whose destination is decided by looking at state |
| Checkpointer | The thing that saves state to disk after every node |
| Durable execution | Restarting from the last saved step instead of from zero |
| Subgraph | A whole graph used as a single node inside another graph |
| Supervisor | One router agent that assigns work to specialist agents |
Quick recap
- An agent is a state machine: typed state, function nodes, arrows between them.
- Save the full state after every node, and a crash at step 38 costs you one step, not 38.
- Keep nodes deterministic and state JSON-friendly, or resume quietly gives you the wrong run.