LangGraph in depth: agents that loop, pause, crash and recover
LangGraph is an library for building an as a map of steps. It also saves the agent's progress as it goes. That saving is what lets an agent wait for a person, survive a crash, or go back in time. We'll use the same research assistant as the earlier modules.
Tap any word with a dotted underline for a plain-English definition.
Interactive demo
Checkpoints, crashes and time travel
Run the research assistant one step at a time. After every step LangGraph saves a , which you'll see pile up on the right. Then break things: crash the server, rewind to an earlier checkpoint, change the state and branch off a new run.
Simulation.No real LangGraph, AI model or search engine is running. Steps, results and prices are scripted examples, and the code lines show simplified versions of the real calls.
Question:
Keys: N next · P play
code: app = graph.compile(checkpointer=saver)
Try this: run until the search loops back (step 5), then click Rewind on checkpoint #2. Type a better query and Fork a new branch. Or press Crash the server at any point, then Resume.
Graph which step runs, and where it goes next
running nowalready ranerrorwaiting for a person
State the shared notebook, as of the latest checkpoint
Checkpoints thread rs-0142 · saved to the database
1 · Why graphs
Chain, hand-written loop, or graph?
There are three common ways to wire up an agent. LangGraph is the third. It's worth knowing what the other two give up.
Hand-written loop
(LangGraph)
Shape
A straight line
A while loop with if-statements
Boxes and arrows, including loops
Branches and loops
No
Yes, in code only the author understands
Yes, and you can draw them
Pause for a person
No
Custom code
Built in
Survive a crash
No
Custom code, often skipped
Built in (checkpoints)
Read the logic
Easy: it's a line
Only by reading the code
Draw the map, and every step's state is saved
Best for
Fixed, short tasks
Quick experiments
Agents going to production
LangGraph is made by LangChain Inc. but doesn't require LangChain: a step can call any model or code. LangChain's own create_agent helper runs on LangGraph underneath.
Two notes. Seeing what happened in production comes from tracing (module 5), whichever shape you choose. And LangGraph isn't the only way to get loops, pauses and crash recovery: provider agent SDKs (OpenAI Agents SDK, Claude Agent SDK, Google ADK), other agent frameworks such as CrewAI, and durable workflow engines such as Temporal cover some or all of it. Module 2's decision guide compares them.
PM takeaway
A graph turns the agent's hidden "what next?" logic into a map your team can read, test and discuss.
2 · State
State: the notebook every step shares
The is one shared record that's passed from step to step. Each step reads what it needs and returns only the fields it changed.
# Simplified. The fields of our research assistant's state.class ResearchState(TypedDict):
question: str
query: str
results: list[dict]
notes: Annotated[list[str], operator.add] # reducer: add to the list, don't replace it
rewrites: int
draft: str
approved: bool
Reducers: what happens when two steps write the same field
A is the rule for combining a new value with the old one. By default the new value replaces the old. For lists like notes, you usually want to add instead.
Default: replace
notes = ["13% gain [1]"]
read #2 returns ["hybrid: no loss [2]"]
→ notes = ["hybrid: no loss [2]"] ✗ lost [1]
Reducer: add
notes = ["13% gain [1]"]
read #2 returns ["hybrid: no loss [2]"]
→ notes = ["13% gain [1]", "hybrid: no loss [2]"] ✓
This matters most when steps run at the same time, like the three page reads in our read step. Chat-style agents use a ready-made reducer called add_messages for the conversation history.
PM takeaway
The state's fields are the agent's working memory. Ask to see them: they tell you exactly what the agent can and can't "know".
3 · Nodes and edges
Steps, arrows, and the decisions between them
A is a step: a plain function that takes the state and returns updates. An is an arrow to the next step. A chooses the arrow at run time.
# Simplified.
graph = StateGraph(ResearchState)
graph.add_node("plan", plan)
graph.add_node("search", search)
graph.add_node("grade", grade)
graph.add_node("rewrite_query", rewrite_query)
graph.add_edge(START, "plan")
graph.add_edge("plan", "search")
graph.add_edge("search", "grade")
graph.add_conditional_edges("grade", route_after_grade)
graph.add_edge("rewrite_query", "search") # an arrow pointing backwards = a loopdef route_after_grade(state):
if state["grade"] == "good": return"read"if state["rewrites"] < 2: return"rewrite_query"return"no_sources"# the exit that stops the loop
Loops need two limits
Our own rule (rewrites < 2) gives the loop a sensible, product-level exit. LangGraph also has a , a safety net that stops any run after too many steps. You can change it for each run.
Steps can run at the same time
If two edges leave the same node, both next steps run in parallel, and the reducers merge their results into the state. That works when you know the number of branches in advance.
When the number of parallel steps depends on the run
Our read step opens however many good results the search found: maybe 2, maybe 5. Fixed edges can't do that. Instead, a routing function returns one Send per item, and LangGraph runs a copy of the step for each, at the same time. This pattern is often called map-reduce.
# Simplified.def fan_out(state):
return [Send("read_one", {"url": r["url"]}) for r in state["good_results"]]
graph.add_conditional_edges("grade", fan_out) # one read_one per good result, in parallel
graph.add_edge("read_one", "write") # notes from every copy are merged by the reducer
Ask for a cap here too, such as "read at most 5 pages". Otherwise a search with 40 good results starts 40 reads at once.
Check your understanding · Nodes and edges
The search keeps returning poor results. What should stop the grade → rewrite → search loop?
PM takeaway
Every arrow that points backwards needs a written exit rule. The recursion limit is a safety net, not a product decision.
4 · Retries and errors
When a step fails
Tools fail all the time: timeouts, rate limits, flaky websites. A re-runs a failed step automatically instead of failing the whole run.
# Simplified. Up to 3 attempts, waiting a bit longer each time.
graph.add_node("search", search, retry_policy=RetryPolicy(max_attempts=3))
Retry
For temporary problems such as timeouts or a busy service. Same input, try again.
Route around it
For "worked, but the result is bad". Use a conditional edge, like our grade → rewrite_query loop.
Fail clearly
When retries run out, go to a fallback step that tells the user honestly, like our no_sources.
PM takeaway
Decide per tool: how many retries, how long to wait, and what the user sees when it still fails.
5 · Checkpointers and threads
Saving progress after every step
A saves a checkpoint (a copy of the state, plus what runs next) after every step. Checkpoints are grouped by a : one ID per conversation or job.
# Simplified.
app = graph.compile(checkpointer=PostgresSaver(db)) # saved to a real database
config = {"configurable": {"thread_id": "rs-0142"}}
app.invoke({"question": q}, config)
# The server crashed? Same thread ID, no new input: it carries on from the last checkpoint.
app.invoke(None, config)
Enables
Crash recovery
A run restarts at the last saved step instead of from scratch. That saves time and money.
Enables
Long waits
A run can pause for hours or days for a person, with nothing kept in the server's memory.
Enables
Memory across turns
A chat agent remembers earlier messages because every turn uses the same thread.
Watch out: an in-memory checkpointer is fine for demos, but it's lost when the server restarts. Production needs a database-backed one, such as Postgres. Try turning the checkpointer off in the demo, then crash the server.
Checkpoints are data too. Each one stores the full state: the question, the pages read, the draft, and anything a user typed. That's often personal or client data, saved after every step. Agree how long checkpoints are kept, who can read the database, and how they're deleted on request, just as you would for traces (module 5) and conversation logs (module 6).
PM takeaway
"Where are checkpoints stored?" is a launch-blocking question. If the answer is "in memory", runs and approvals can be lost.
6 · Human-in-the-loop
Approve, edit or reject
An pauses the graph inside a step and hands something to a person. Your app shows it, collects the decision, and resumes the same thread with the answer.
# Simplified.def human_review(state):
decision = interrupt({"draft": state["draft"]}) # pauses here; checkpoint savedif decision["action"] == "approve": return {"approved": True}
if decision["action"] == "edit": return {"draft": decision["text"], "approved": True}
return Command(goto="write", update={"feedback": decision["text"]})
# Later, when the reviewer clicks a button in your app:
app.invoke(Command(resume={"action": "approve"}), config)
Approve
Carry on as planned.
Edit
The person fixes the draft and their version continues. This is often faster than asking the model to redo it.
Reject
Send it back with feedback. The arrow loops back to write.
Design note. When the run resumes, the paused step starts again from its beginning. Put the interrupt at the start of the step, and don't do anything irreversible, like sending an email, before it in the same step.
Check your understanding · Human-in-the-loop
A step sends the client email, then calls interrupt() to ask for approval. What goes wrong?
PM takeaway
Approval points are a product feature. Design the reviewer's screen, their options, and what happens if no one answers.
7 · Time travel
Replay or branch off from any earlier step
Because every step is saved, you can go back to any checkpoint. You can replay from there, or fork: change the state first and branch off a new run. The original branch is kept.
# Simplified.
history = list(app.get_state_history(config)) # every checkpoint, newest first
past = history[-3] # e.g. "after plan"
fork = app.update_state(past.config, {"query": "remote work randomised study"})
app.invoke(None, fork) # a new branch runs from there
Debugging
"What if the query had been better?" Change one field and rerun from that point, without repeating the earlier steps.
Recovery
A human spots a wrong turn and corrects the state before the agent continues.
Building test cases
Save the checkpoint just before a failure, and rerun it after each fix.
Steps after the fork point run again for real, so model calls and tools may return different results, and they cost money again.
PM takeaway
Time travel turns "it went wrong somewhere" into "it went wrong at step 4, and here's the fix", without rerunning everything.
8 · Streaming
Showing progress while the agent works
A research run takes 10 to 20 seconds. sends updates to your app as each step finishes, so the user sees "Searching… Reading 3 sources… Writing…" instead of a spinner.
# Simplified.for update in app.stream({"question": q}, config, stream_mode="updates"):
show_progress(update) # e.g. {"search": {"results": [...]}}
Stream mode
What arrives
Good for
updates
What each step changed
Progress messages ("Reading sources…")
values
The full state after each step
Debugging screens
messages
The model's reply, word by word
Typing effect for the final report
custom
Anything a step chooses to send
"Read 2 of 3 pages"
PM takeaway
Long agent runs need visible progress. Agree with engineering which steps get a user-facing message.
9 · Subgraphs and multi-agent
Graphs inside graphs, and agents handing off
A is a whole graph used as one step of a bigger graph. That's how teams build systems of several agents.
With a supervisor, one agent decides who works next and every result comes back to it. With handoffs, each agent passes the work on directly. Supervisors are easier to control; handoffs use fewer steps.
When it helps
Clearly separate jobs with different tools or permissions
Parts that can run at the same time
One agent's instructions or memory are getting too long
What it costs
More model calls, so more cost and waiting
Information gets lost at each handoff
Harder to test, because there are more paths
PM takeaway
Start with one graph. Split into several agents only when you can name the specific limit you're hitting.
10 · Testing
Testing a graph, one piece at a time
A graph is easier to test than a hand-written loop, because each step and each decision is a separate piece. Test them separately, then test whole runs.
1 · Routing rules
Routing functions like route_after_grade are plain code, so test them without any model: given this state, does it pick the right arrow? Cheap, fast and exact.
2 · Single steps
Run one node with a saved state. For steps that call a model, use a fixed reply in unit tests, and a small eval set for the real model.
3 · Failure paths
Make the search time out, return junk, or return nothing. Check that the run retries, loops, or ends at no_sources as designed. Crash it mid-run and resume.
4 · Whole runs and their path
Run real questions end to end, several times each. Check the answer and the path: which steps ran, how many loops, whether it paused for review when it should.
5 · Replays of real bugs
When a run goes wrong in production, save the checkpoint just before the failure. Rerun from it after every fix, so the same bug can't quietly return.
6 · Safe to run twice
Run each step twice with the same state, as a resume or replay would. Anything with side effects, like saving or sending, must not happen twice.
# Simplified. A routing rule is just a function, so it's easy to test.def test_gives_up_after_two_rewrites():
state = {"grade": "poor", "rewrites": 2}
assert route_after_grade(state) == "no_sources"
Check your understanding · Testing
Which test would have caught the in-memory checkpointer problem before launch?
PM takeaway
Ask to see the failure-path tests, not just the happy path: timeouts, junk results, crashes and restarts.
11 · Deployment
Running it for real users
LangGraph is a library, so it runs wherever your code runs. The main decision is who operates the servers and the checkpoint database.
Option A
Run it yourself
Your own servers or cloud, with a database checkpointer such as Postgres. You get full control and keep data in-house, but your team handles scaling, queues and long-running jobs.
Option B
A managed service
LangChain offers hosting for LangGraph agents, called LangSmith Deployment since October 2025 (previously LangGraph Platform). It handles persistence, background runs and scaling, and comes with a visual debugger (LangSmith Studio). It's a paid product, so check current names and pricing.
PM takeaway
Ask who will be paged at 2 a.m. when runs get stuck. That answer often decides self-hosting versus a managed service.
Keep this
LangGraph cheat sheet
LangGraph = state + nodes + edges, saved by a checkpointer after every step.
The building blocks
State: shared notebook; reducers decide how updates merge
Node: one step (any function)
Edge / conditional edge: fixed arrow / arrow chosen at run time
Retry policy: automatic re-runs for flaky steps
Checkpointer + thread: saved progress per job
Interrupt: pause for a person; resume with their answer
Time travel: replay or fork from any checkpoint
Subgraph: a graph used as a step
Questions to ask your engineers
Where are checkpoints stored, and for how long?
What stops each loop, and what's the recursion limit?
Which steps retry, how many times, and what's the fallback?
What happens to a paused run if nobody approves it?
Can any step do something twice if it re-runs after a crash?
Can we fork a failed run to reproduce a bug?
Which failure paths have tests: timeouts, junk results, crashes?
How long are checkpoints kept, and who can read them?
PM takeaways
The graph is a readable map of the agent's decisions.
State fields are what the agent can know.
Every backwards arrow needs an exit rule.
Decide retries and fallbacks per tool.
Database checkpoints are a launch requirement.
Approval points are product features.
Time travel makes bugs reproducible.
Show progress during long runs.
One graph first; multi-agent only for a named reason.
Glossary
Learn from others
3 common mistakes teams make with LangGraph
1
Shipping with in-memory checkpoints
What it looks like
Everything works in testing. In production, a deploy or restart wipes every paused run, and reviewers' approvals disappear.
Fix
Use a database-backed checkpointer from the first real launch, and test a restart in the middle of a run.
2
Loops with no exit rule of their own
What it looks like
The only thing stopping a "try again" loop is the recursion limit. Runs end with a confusing error after many paid model calls.
Fix
Keep a counter in the state, route to a fallback after N tries, and tell the user what happened.
3
Steps that aren't safe to run twice
What it looks like
A step sends an email, then crashes or pauses. On resume or replay it runs again, and the client gets two emails.
Fix
Keep irreversible actions in their own small step after the approval, and make them safe to repeat (for example, check "already sent?").