Resume Botie’s Work with LangGraph
The technician returns after a pause. Preserve the right request’s evidence, merge updates deliberately, and resume a review without repeating an external action.
16 sections · 4 questions · 1 coding challenge
The arrow keys move between sections too.
A Useful Workflow Sometimes Needs to Go Around Again
BOTIE
For Botie, the state contains the evidence gathered for one maintenance request. A node returns an update, and the application decides how that update changes what was already stored. The workshop builds that merge operation on small invented records before the reference code introduces a paused graph.
Sources for this history
The Technician Comes Back. Does Botie Remember the Right Job?
Botie has found a manual excerpt and prepared a proposed inspection. The technician needs to ask a colleague before continuing. Closing the page should not turn that request into an unrelated conversation or repeat work that already happened.
We need an explicit record of where the request stopped and what information it had. LangGraph represents work as nodes connected by routes, with state passed between them. We will start by deciding what to preserve before adding pause and resume.
The workshop demonstrates a reducer on checkpoint fixtures. It does not run the LangGraph scheduler.
Step 1 of 2
A checkpoint belongs to one request
Two technicians can discuss the same pump without sharing the same unfinished task.
| Thread | Saved evidence | Incoming update |
|---|---|---|
| request-17 | manual-7: old text | manual-7: revised text |
| request-28 | manual-2: other pump | No update |
request-17
- Saved evidence
- manual-7: old text
- Incoming update
- manual-7: revised text
request-28
- Saved evidence
- manual-2: other pump
- Incoming update
- No update
Step 2 of 2
Choose a merge rule instead of hoping a list does the right thing
Our policy keeps the position of an existing ID and replaces its text. New IDs go at the end.
| Before | Update | After |
|---|---|---|
| manual-7: old | manual-7: revised | manual-7: revised |
| manual-7: revised | manual-8: appendix | manual-7: revised; manual-8: appendix |
manual-7: old
- Update
- manual-7: revised
- After
- manual-7: revised
manual-7: revised
- Update
- manual-8: appendix
- After
- manual-7: revised; manual-8: appendix
A saved request needs explicit update rules
Thread selection preserves request identity. The reducer preserves evidence identity within that request.
What evidence should survive this update?
Choose the request checkpoint, then merge incoming excerpts by ID. Observe what happens when a lookup returns an excerpt already seen.
BOTIE
Updated evidence records
[
{
"id": "M7",
"text": "Inspect mounting."
},
{
"id": "M8",
"text": "Check alignment."
}
]Follow the operations
1. Evidence lists saved by thread ID
{ "request-17": [ { "id": "M7", "text": "Inspect mounting." } ] }2. Thread to resume
request-17
3. Load this thread's evidence
[ { "id": "M7", "text": "Inspect mounting." } ]4. New or revised excerpt records
[ { "id": "M8", "text": "Check alignment." } ]5. Merge evidence by ID
[ { "id": "M7", "text": "Inspect mounting." }, { "id": "M8", "text": "Check alignment." } ]6. Result
[ { "id": "M7", "text": "Inspect mounting." }, { "id": "M8", "text": "Check alignment." } ]
This is a sequential reducer demonstration. It does not persist real data, resolve concurrent conflicting versions, or execute an external job.
1. Give the unfinished request a state
State is the data the workflow needs as it moves between operations. For our request, it includes collected evidence and whether review has happened. A thread ID identifies the request whose saved state we want to continue.
Keep this ID separate from the asset ID. A pump can have several maintenance requests. Also keep it separate from user identity: the application must check that the signed-in user may access the selected thread. Knowing or guessing an ID does not grant access.
Write down which fields belong to the request before choosing how to save them. Otherwise unrelated concerns accumulate in one unstructured transcript.
2. Make nodes return updates
A node is a function that performs one step and returns changes to state. A retrieval node can return new evidence. A review node can return a decision. Edges describe which node runs next; a conditional edge chooses a route from the current result.
This can describe deterministic work as well as model calls. Our reference uses an invented manual lookup so that the behaviour of the graph is easier to isolate. Adding a model inside a node does not change the need to define that node’s inputs and outputs.
Use a fixed route when the sequence is known. Add a conditional route or cycle only when the task actually needs one.
3. Decide what an update means for each field
If a node returns a new approved value, replacing the previous boolean is sensible. Evidence is different: replacing the whole list may lose earlier excerpts, while appending everything may duplicate a repeated lookup.
A reducer is the function that combines a previous field value with an incoming update. Our evidence reducer uses each excerpt’s ID. New IDs are appended, and existing IDs receive their updated text in place. This is an application policy, not LangGraph’s automatic rule for every list.
Before = [{"id":"manual-7","text":"old wording"}]
Update = [{"id":"manual-7","text":"revised wording"}]
After = [{"id":"manual-7","text":"revised wording"}]The same update applied twice leaves one excerpt. That makes this particular merge idempotent: repeating it has the same result as applying it once.
Replace one identified excerpt
A dictionary uses the excerpt ID as its key. Assigning that existing key replaces its value without adding another entry. dict creates a new outer dictionary for this small mapping of strings.
saved = {"M2": "Old coupling wording.", "M5": "Inspect the filter."}
updated = dict(saved)
updated["M2"] = "Revised coupling wording."
print(list(updated))
print(updated["M2"])
print(saved["M2"])['M2', 'M5']
Revised coupling wording.
Old coupling wording.The ID stays in its original position and the saved mapping stays unchanged. The later challenge generalises this operation to lists of evidence records.
4. Messages and evidence need different reducers
LangGraph has an add_messages reducer for message histories. It can append messages and handle updates using message IDs. Our evidence records are ordinary dictionaries, so the example supplies a different reducer for them.
Do not reuse the word id without deciding what it identifies. A tool-call ID, message ID and document ID name different objects. If two document revisions need to remain separately citable, use versioned document identifiers instead of overwriting one ID’s text.
The workshop’s replace-by-ID policy is chosen for this fixture. An audit trail should retain the prior version somewhere separate when the application needs it.
Check yourself
Questions on Parts 1 to 4
Question 1 of 2
What should remain after the merge?
Use the information below.
Saved evidence is [{"id":"M7","text":"old"}]. Incoming evidence is [{"id":"M7","text":"new"}]. This lesson replaces text for an existing ID.5. Save a checkpoint that can actually survive the pause
A checkpointer stores graph state for a thread so a later invocation can continue it. InMemorySaver is convenient for learning, but its contents disappear when that Python process ends. A production service needs an appropriate persistent backend and an access policy for the saved data.
A store is a different persistence mechanism for application data shared across threads. Keep a technician’s preference separate from the unfinished state of a particular inspection request. Retention and deletion rules apply to both.
An in-memory demonstration of resume is not a demonstration of crash recovery across process restarts.
6. Pause at review and resume the same thread
interrupt exposes a value to the caller and pauses execution. When the application later invokes the same graph and thread with Command(resume=...), that value becomes the result of the interrupt call. The code in the interrupted node begins again on resume.
The reference pauses after collecting an invented excerpt. The automatic True near the end stands in for the service’s authenticated review handler. It is useful for observing control flow, but it is not an approval system.
Use the same thread configuration for resume. Creating a new thread would describe a new request rather than continuing the paused one.
Run in your own environment
A small graph that pauses with its evidence
This local reference uses no language model and requires no provider key. It demonstrates graph APIs outside the browser worksheet.
Create a Python environment and install a compatible LangGraph 1.x release. Record resolved versions in a lockfile.
Run as a local script. InMemorySaver persists only while this process remains alive.
The sample uses a fixed review value to demonstrate resume. A deployed handler must authenticate the reviewer and bind the decision to the exact proposed action.
from typing import Annotated, TypedDict
from langgraph.graph import StateGraph, START, END
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.types import interrupt, Command
def merge_evidence(old: list[dict], incoming: list[dict]) -> list[dict]:
by_id = {item["id"]: dict(item) for item in old}
for item in incoming:
by_id[item["id"]] = dict(item)
return list(by_id.values())
class State(TypedDict):
evidence: Annotated[list[dict], merge_evidence]
approved: bool
def collect(state: State):
return {"evidence": [
{"id": "manual-7", "text": "Inspect the mounting."}
]}
def review(state: State):
decision = interrupt({"evidence": state["evidence"]})
return {"approved": decision is True}
builder = StateGraph(State)
builder.add_node("collect", collect)
builder.add_node("review", review)
builder.add_edge(START, "collect")
builder.add_edge("collect", "review")
builder.add_edge("review", END)
graph = builder.compile(checkpointer=InMemorySaver())
config = {"configurable": {"thread_id": "maintenance-request-17"}}
paused = graph.invoke({"evidence": [], "approved": False}, config)
print(paused["__interrupt__"])
# In a service, obtain this decision from an authenticated reviewer.
resumed = graph.invoke(Command(resume=True), config)
print(resumed["approved"])The reducer controls evidence updates. The checkpointer controls saved thread state. Neither gives permission to execute an external action.
Official API documentation. Documentation checked .
7. Keep replay away from unprotected side effects
Imagine creating a work order before the review node pauses. Resuming the node could run that statement again. Now the technician has two orders even though there was one request. Place the effect after the decision, and still design the operation to tolerate retries and process failures.
An idempotency key lets the work-order service recognise the same intended action. The receiving service must store and enforce that key, ideally atomically with creation. A local set of completed IDs does not protect against a crash between creating the order and recording its ID.
Checkpointing the graph and making external effects idempotent solve related but different problems. Neither creates a universal exactly-once guarantee.
8. Concurrent updates need a deliberate conflict policy
Two retrieval branches may find different text under the same document ID. Our simple last-update-wins reducer assumes an ordered update stream. If the order is not meaningful, the answer can depend on which result reaches it last.
Use immutable versioned IDs, reject conflicting updates, or define an ordering from trusted revision information. Choose a rule that matches the source system. Adding a reducer removes an implementation ambiguity only when its policy is appropriate for the data.
The workshop is sequential. Its fixtures make the update order explicit rather than claiming to settle concurrent document conflicts.
9. Follow the saved evidence through a changed request
Select another thread in the workshop and run the same construction. It should load that thread’s excerpts before applying its update. Then replay an existing excerpt and check that one ID remains.
If the old machine simply concatenated lists, the repeated-lookup fixture exposes the mistake. If it hard-coded one thread’s evidence, the other-request fixture exposes that instead. These are different failure modes, so keep both checks after making a repair.
The Python challenge asks you to write the merge operation without mutating either input. Saved state is easier to reason about when an update does not quietly rewrite the fixture used by another test.
Check yourself
Questions on Parts 1 to 9
Question 1 of 2
Which behaviour must the application account for?
Use the information below.
A node writes a work order before calling interrupt. The reviewer later resumes it.Try it with Botie
Build with Botie
Write and run Python