LangGraph Persistence Tutorial: Checkpoint and Resume Multi-Agent Workflows

LangGraph persistence is what makes a multi-agent workflow safe to pause, inspect, and continue later. Without it, a process restart, a human review step, or a temporary failure can mean starting the whole graph again—and possibly repeating work that already happened.

In the previous tutorial on routing and parallel execution, a router selected specialist agents and merged their results. This tutorial gives that workflow a durable thread: it checkpoints progress, pauses before a consequential action, and resumes from the same point when a reviewer is ready.

LangGraph persistence diagram showing workflow state before an interrupt and the preserved state after resume using the same thread ID.
Checkpointed state is preserved through the pause; the same thread resumes with the reviewer’s decision.

What you will build

You will add a review gate to a simple multi-agent workflow. The graph will:

  1. prepare a draft answer from worker findings;
  2. save its state at graph steps through a checkpointer;
  3. pause for a reviewer before sending the answer;
  4. resume using the same thread ID; and
  5. send only after approval.

The important distinction: a checkpointer stores the state of one workflow thread. It is not a general user-profile database. Long-term preferences and facts that must survive across separate threads belong in a LangGraph store.

How LangGraph persistence gives a workflow a stable thread

When you compile a graph with a checkpointer, LangGraph records state snapshots as the graph progresses. The thread_id in the invocation config tells it which sequence of snapshots to load. Reuse the same ID to continue one workflow; use a new ID to begin another.

from langgraph.checkpoint.memory import InMemorySaver

checkpointer = InMemorySaver()
graph = builder.compile(checkpointer=checkpointer)

config = {
    "configurable": {
        "thread_id": "support-case-4821"
    }
}

InMemorySaver is useful for learning and tests, but its data disappears when the process stops. For local development, use SQLite; for production, use a durable database-backed checkpointer such as Postgres. The official persistence guide lists the supported integrations.

Put the approval point in the graph

An interrupt is a deliberate pause inside a node. LangGraph saves the current state, returns a JSON-serialisable request to the application, and waits. The caller later supplies a response using Command(resume=...).

from typing import TypedDict

from langgraph.checkpoint.memory import InMemorySaver
from langgraph.graph import END, START, StateGraph
from langgraph.types import Command, interrupt


class ReviewState(TypedDict):
    findings: list[str]
    draft: str
    approved: bool


def write_draft(state: ReviewState) -> dict:
    findings = "\n".join(f"- {item}" for item in state["findings"])
    return {"draft": f"Proposed answer:\n{findings}"}


def request_approval(state: ReviewState) -> dict:
    decision = interrupt(
        {
            "action": "send_answer",
            "draft": state["draft"],
            "message": "Approve, reject, or edit this answer."
        }
    )
    return {
        "approved": decision["approved"],
        "draft": decision.get("edited_draft", state["draft"]),
    }


def send_answer(state: ReviewState) -> dict:
    if not state["approved"]:
        return {}
    # Call the email, ticket, or messaging service here.
    return {}


builder = StateGraph(ReviewState)
builder.add_node("write_draft", write_draft)
builder.add_node("request_approval", request_approval)
builder.add_node("send_answer", send_answer)
builder.add_edge(START, "write_draft")
builder.add_edge("write_draft", "request_approval")
builder.add_edge("request_approval", "send_answer")
builder.add_edge("send_answer", END)

graph = builder.compile(checkpointer=InMemorySaver())

The useful design choice is that the sending action sits after the approval gate. A reviewer can see and change the proposed answer before an external system receives anything.

Start, pause, and resume the same run

config = {
    "configurable": {
        "thread_id": "support-case-4821"
    }
}

paused = graph.invoke(
    {
        "findings": [
            "The customer was charged twice.",
            "A refund has not yet been initiated."
        ],
        "approved": False,
    },
    config=config,
)

# The application presents paused["__interrupt__"] to a reviewer.

completed = graph.invoke(
    Command(
        resume={
            "approved": True,
            "edited_draft": "We are sorry about the duplicate charge. Your refund is now being processed."
        }
    ),
    config=config,
)

The identical thread_id is essential. It reconnects the second call to the paused state. The resume value becomes the value returned by interrupt() inside request_approval.

Design for replay, not just the happy path

When a graph resumes from an interrupt, LangGraph restarts the interrupted node from its beginning. Any code before interrupt() runs again. That is intentional: the runtime can rebuild the node’s work consistently. It also means that a payment, email, database write, or third-party API request before the interrupt could happen twice.

Use these rules to avoid that mistake:

  • Put irreversible actions after the approval interrupt. Draft and inspect first; send or charge only once approval is present.
  • Make external actions idempotent. Pass a stable idempotency key to services that support one, or record a unique operation ID yourself.
  • Keep interrupt payloads simple. Send JSON-serialisable data the user interface can render: the proposed action, the relevant draft, and the allowed choices.
  • Use a durable checkpointer outside experiments. RAM-backed checkpoints cannot recover after a restart.

These rules are especially important in multi-agent systems: several workers may have produced useful findings, but only one final action should change the outside world.

Checkpointer or store?

Need Use
Continue this exact support case tomorrow Checkpointer + the same thread_id
Inspect earlier graph state while debugging Checkpointer
Remember a user’s language preference across future conversations Store, namespaced by user
Keep product knowledge shared by every workflow Store or another application data source

This boundary keeps state understandable. A checkpoint answers “where was this run?” A store answers “what should the application remember more broadly?”

A practical recovery checklist

  1. Assign a stable, non-sensitive thread ID to each workflow instance.
  2. Compile the graph with a checkpointer.
  3. Use interrupt() for approvals or missing human input.
  4. Resume with Command(resume=...) and the original thread ID.
  5. Ensure code before an interrupt can run more than once safely.
  6. Choose a persistent backend and a retention policy before production.

Where this fits in the series

Routing decides which agents should work. Persistence makes that work survivable when a person, an external dependency, or a system restart interrupts the flow. The next tutorial will focus on evaluating the system: whether it routed correctly, used tools appropriately, and delivered reliable answers at an acceptable cost.

Further reading

Leave A comment

Are you human? Please solve:Captcha