Building stateful agents with LangGraph

Examples and sources reviewed 30 September 2026.

Graphs, checkpoints, human review, streaming and evaluation, with a small workflow you can run locally.

LangGraph coordinates a program as a graph of steps that read and update shared state. Some steps can call a language model; others can validate data, retrieve documents or ask a person to review a proposed action. This makes the control flow explicit and gives a long-running application a place to save its progress.

This article starts with a maintenance-notice workflow. It prepares a draft, pauses for review, then records approval or rejection. The example uses deterministic Python functions so you can inspect the execution without an API account. The same boundaries apply when a model prepares the draft or proposes the next action.

The downloadable example was tested with Python and LangGraph 1.2.12 plus langgraph-checkpoint-sqlite 3.1.1 on 30 September 2026. It makes no model calls and sends no messages. The production extensions discussed below are design guidance, not features implemented by this demo.

state and control flowlocal runnable examplepersistent reviewno API key

The essentials

  • State is the data a run carries. Nodes return updates to it; edges determine what runs next.
  • A graph can mix fixed and model-driven decisions. Keep checks with a precise answer in ordinary code.
  • A checkpoint records progress. Its thread identifier helps locate a conversation or workflow. Authorization remains an application responsibility.
  • Human review is a pause in execution. Resume the same thread with a validated decision, and expect the interrupted node to start again.
  • External actions need their own retry design. A saved graph does not make an email, payment or database write execute exactly once.
  • Evaluate outcomes and failure behavior. A run can finish successfully while producing the wrong answer.

1 · Choose what belongs in the graph

A workflow specifies its next step in code. An agent delegates some of that choice to a model, usually through tool calls. A useful application may combine both: a model drafts a notice, code checks its fields, a person approves it, and a service sends it.

LangGraph is useful when these steps need explicit state, branching, saved progress or interruption. A single model call followed by a schema check may only need a function. Begin with the smallest control flow that expresses the task; add graph structure when it makes execution easier to inspect or resume.

The library can run without LangChain model integrations. LangChain provides a higher-level agent interface and provider integrations; LangGraph exposes the underlying orchestration primitives. This article uses those primitives directly. See the official overview.

StepWho decides?What to retain
Prepare a draftCode in this demo; optionally a modelInput, draft and configuration version
Validate the proposalApplication codeValidation result and useful error details
Approve or rejectAn authorized reviewerThe exact proposal and decision
Perform an actionA bounded application functionOperation identifier and observed result

2 · State, nodes, edges and reducers

In StateGraph, the state schema defines the fields exchanged by nodes. A node is a function that receives state and returns a partial update. An edge connects two nodes; a conditional edge chooses a destination from the current state. Compiling the builder produces a graph that can be invoked or streamed.

class NoticeState(TypedDict, total=False):
    subject: str
    draft: str
    approved: bool
    outcome: str

def prepare(state: NoticeState):
    return {"draft": f"Maintenance notice: {state['subject']}."}

The full example validates the subject before constructing the draft. A TypedDict documents the shape for Python tooling; it does not itself validate incoming data at runtime.

By default, a field update replaces its previous value. A reducer changes how updates are combined. For example, an annotated list can accumulate results from several workers. The choice matters when parallel nodes write to the same field: define how their outputs combine, or give them distinct fields. Append-only accumulation can also create duplicates during a poorly designed retry.

Message histories commonly use the add_messages reducer, which handles message identifiers as well as additions. Use it when its semantics match the application; keep unrelated business records in their own fields. The Graph API guide documents schemas, reducers and routing.

Check yourself

Must a node return the entire state?

No. Return the fields it updates. The graph applies those updates using each field's configured reducer or replacement behavior.

Does a type annotation validate a user request?

No. Validate external inputs at runtime before accepting them into the workflow.

3 · Run a workflow that survives a process exit

Download approval_demo.py and requirements.txt into an empty directory. Create an environment and install the two pinned packages:

python3 -m venv .venv
source .venv/bin/activate
python -m pip install -r requirements.txt
python approval_demo.py begin demo.sqlite

The command prints a pending review containing the draft, saves the checkpoint to SQLite, and exits. Inspect the saved state from a new process:

python approval_demo.py inspect demo.sqlite
python approval_demo.py resume demo.sqlite --decision approve
python approval_demo.py inspect demo.sqlite

The final state has outcome: recorded_in_demo_state, approved: true and no next node. Nothing has been sent. Try a separate thread with --thread notice-2, then resume that thread with --decision reject. Its outcome becomes rejected, while the first thread retains its own result.

A notice moves through preparation, saved review, a decision and a final state. prepare draft pause and save resume with decision record or reject
  1. The prepare node validates the subject and writes a draft to state.
  2. The review node interrupts. SQLite retains the checkpoint after the Python process exits.
  3. A new process opens the same database and resumes the same thread with a boolean decision.
  4. The conditional edge selects record or reject. The demo saves the outcome and ends.

The example's separate commands make persistence observable. Reusing an in-memory object inside one process would demonstrate a pause, but would not establish that state survives process exit.

4 · Pause for a decision and resume the same thread

interrupt() exposes a JSON-serializable review payload and pauses execution. With the default invocation API, the payload appears under __interrupt__. Resume by passing Command(resume=...) and the same thread configuration:

def review(state: NoticeState):
    decision = interrupt({"draft": state["draft"]})
    if type(decision) is not bool:
        raise ValueError("Expected a boolean decision")
    return {"approved": decision}

config = {"configurable": {"thread_id": "notice-1"}}
result = graph.invoke(Command(resume=True), config)

The interrupted node starts from its beginning on resumption. Keep work before the interrupt free of external effects, and avoid wrapping the interrupt in an exception handler that swallows its control-flow signal. See Interrupts.

A real review interface must authenticate the reviewer, check access to the thread, and show the exact action being approved. If a proposal changes after approval, request a new decision for the new proposal. A client-provided thread identifier or an unvalidated approved field is insufficient evidence of authorization.

5 · Decide what a checkpoint promises

A checkpointer stores graph state and execution metadata for a thread. It enables inspection, resumption and other stateful operations. In-memory storage is useful in tests; a database-backed implementation is needed when the process may exit and the work must continue later. The demo uses SQLite for that narrow local requirement.

Checkpoints occur at graph execution boundaries. They are not arbitrary snapshots of the Python stack or transactions that include every external service call. Inspect the checkpointer documentation when choosing durability behavior, retention and storage.

Use opaque thread identifiers and an application-owned mapping from authenticated users to their threads. Plan how to delete a conversation, limit retention, and migrate saved state when the graph changes. A renamed node or changed field can matter to work that was paused under an earlier version.

For a deployment with multiple workers, also define who may execute a given thread at a time. Two concurrent resumes are an application-level race that the notice demo does not attempt to solve. Test the selected storage and execution arrangement under that failure case.

6 · Make retries safe at the action boundary

Consider a node that sends a notice and then fails before saving its result. On retry, the code may send it again. Moving the send into its own node makes the boundary easier to see, but does not by itself remove the gap between external success and checkpoint persistence.

Give each intended action a stable operation identifier. When the external system supports idempotency keys, reuse the same key for retries of that action. Otherwise, design a durable operation record and reconciliation procedure around the target system. Record pending, succeeded, failed and unknown outcomes distinctly: a timeout does not establish that nothing happened.

The Functional API guide explains durable task boundaries and replay. Choose graph nodes or functional tasks to fit the application, while keeping external idempotency separate from graph persistence.

Check yourself

The request timed out after sending a notice. Should the retry assume it failed?

No. The outcome is unknown until reconciled. Reuse the operation identifier and inspect the target system's result.

Where should an email send go relative to human review?

After a validated approval of that exact action, with an operation identifier and retry handling at the send boundary.

7 · Add a model without handing it every decision

To turn the demonstration into a model-assisted workflow, replace the draft function with a provider call and validate its output before review. Keep the model response separate from the authoritative fields that determine permissions and execution. A generated statement that the action is approved must never become approval.

Tool-calling agents repeat a similar sequence: call the model, inspect proposed tool calls, validate and run permitted tools, append their results, and call the model again. Stop on a final answer, a defined completion condition, or a budget limit. The workflow and agent patterns show how these loops are composed.

Each tool needs a narrow input schema, authorization checks, a timeout and a defined error result. Treat retrieved text and tool responses as untrusted data. They can inform an answer without gaining permission to choose credentials, tenant identity or arbitrary commands.

Set explicit budgets for model calls, tool calls, elapsed time and expensive operations. A graph recursion limit bounds graph steps; it is not a financial budget. A workflow can spend heavily inside one step or loop without improving its answer.

8 · Stream progress without confusing it with completion

Streaming lets a caller observe a run while it is executing. The documented modes include state updates, full state values, message tokens and custom events. For a small graph, updates are often enough:

for update in graph.stream(
    {"subject": "Search maintenance"},
    {"configurable": {"thread_id": "notice-3"}},
    stream_mode="updates",
):
    print(update)

Use a fresh thread when trying this example. It pauses at review just like invoke(). See the streaming reference for mode-specific event shapes and asynchronous interfaces.

A progress event is an observation, not proof that an external action completed. Give the UI distinct states for running, awaiting review, completed, failed and disconnected. If the connection drops, retrieve the saved run state before deciding what to display or retry. Avoid streaming raw internal state to the browser: it may contain information the user should not receive.

9 · Separate conversation state from reusable memory

A checkpointer retains the current thread. A store holds application-defined information that can be retrieved across threads. A user's saved writing preference belongs to a different scope from one notice draft or a temporary tool response. The persistence overview explains this distinction.

For each stored fact, define where it came from, who may retrieve it, how it is corrected and when it expires. Retrieve a small relevant subset rather than adding the entire archive to every model call. Saved text can be wrong, stale or malicious; storing it does not make it authoritative.

Trimming or summarizing a conversation can reduce context size, but the summary becomes another lossy representation to evaluate. Preserve exact identifiers, user decisions and external results where later actions depend on them. Deleting text from a prompt is also different from deleting it from durable storage and logs. The memory guide covers implementation options.

10 · Add multiple agents only for a useful separation

Subgraphs can encapsulate a workflow behind a smaller state interface. This is useful when a document-retrieval step or specialist review has its own local process. A parent can call the component, validate the result and continue. State schemas and persistence choices determine how information crosses that boundary; see Subgraphs.

A second model-driven worker adds another source of latency, cost and error. Use one when it has a distinct responsibility, a useful context boundary or work that can proceed independently. Give it a concrete output contract and a stopping rule. Several workers repeating the same vague request do not provide an independent correctness check.

For parallel research, keep each worker's findings separate until aggregation, retain source references, and decide how conflicting answers will be resolved. Measure whether the composition improves the final result compared with a single worker on the same tasks and budget.

11 · Test state transitions and task outcomes

The local example was checked through separate begin, inspect and resume processes. Approval and rejection finish with the expected state; a separate thread keeps its own result; invalid input and attempts to resume a completed thread fail explicitly. These checks cover the demonstration's behavior, not the quality of any language model.

LayerExample testEvidence
Input boundaryEmpty or excessively long subjectExplicit validation error
Control flowReject a proposed actionAction node does not run
PersistenceExit while review is pendingA fresh process can inspect and resume
Action serviceLose the response after external successReconciliation prevents a duplicate effect
Model behaviorMissing evidence or hostile tool contentExpected answer or refusal to act

The last two rows are extensions to implement and test in a real application. Start a model evaluation set with representative tasks and known failures. Grade the result with deterministic checks where possible, then use human review for the judgments that remain. If a model grades outputs, compare its decisions with reviewed examples before relying on its scores. The testing guide covers graph-level testing.

12 · The service around the graph

A compiled graph is one component of a service. A deployable application still needs authentication, request validation, thread ownership, storage management, secret handling, cancellation, resource budgets and observability. Decide which responsibilities belong to the selected hosting platform and which remain in application code.

Record the graph version, model configuration, input references, durations, review decisions and external operation identifiers needed to diagnose a failure. Minimize sensitive payloads in logs. Test what happens when a worker exits during review, a provider times out, a tool returns malformed data, or a user reconnects after losing the stream.

Before replacing an existing workflow, run the same cases through both versions and compare useful output, failure rate, latency and cost. Keep the earlier version available until saved-state compatibility and rollback behavior are understood. The deployment guide describes the hosted route; a custom service needs its own operational design.

Sources and example scope

Public LangChain documentation was consulted on 30 September 2026. The code and notice scenario were written for this article.

Related articles: reinforcement learning explains how policies are trained; fine-tuning covers model adaptation; the production LLM engineering stack covers the wider serving system.