LangGraph in depth
← All modules LangGraph in depth

Module 4 · Agent Field Guides · about 25 minutes

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
gave up good poor new query auto-retry approved rejected ends early START planLLM searchtool · retry ×3 gradeLLM · router rewrite_queryLLM · max 2 no_sourcesLLM · fallback readtool ×3 + LLM writeLLM human_reviewinterrupt() END
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)
ShapeA straight lineA while loop with if-statementsBoxes and arrows, including loops
Branches and loopsNoYes, in code only the author understandsYes, and you can draw them
Pause for a personNoCustom codeBuilt in
Survive a crashNoCustom code, often skippedBuilt in (checkpoints)
Read the logicEasy: it's a lineOnly by reading the codeDraw the map, and every step's state is saved
Best forFixed, short tasksQuick experimentsAgents 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 loop

def 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 saved
    if 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 modeWhat arrivesGood for
updatesWhat each step changedProgress messages ("Reading sources…")
valuesThe full state after each stepDebugging screens
messagesThe model's reply, word by wordTyping effect for the final report
customAnything 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.

Supervisor supervisor researcher writer task ↓ result ↑ Handoffs researcher writer fact-checker "needs another source"
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

  1. The graph is a readable map of the agent's decisions.
  2. State fields are what the agent can know.
  3. Every backwards arrow needs an exit rule.
  4. Decide retries and fallbacks per tool.
  5. Database checkpoints are a launch requirement.
  6. Approval points are product features.
  7. Time travel makes bugs reproducible.
  8. Show progress during long runs.
  9. 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?").
← Module 3 · LangChain in depth Module 5 · Langfuse in depth →