Barcelona Code School

Since 2015 / 500+ graduates

LangGraph Tutorial: Build a Stateful AI Agent

Stateful agent tutorial

LangGraph Tutorial: Build a Stateful AI Agent

Build a fictional draft-review graph that remembers its state, pauses for a person and resumes safely—without sending anything externally.

Published 13 October 2026 · Barcelona Code School

A stateful LangGraph agent stores shared data across graph nodes and persists checkpoints under a stable thread ID. In this tutorial, a request is validated, a draft is created, execution pauses with interrupt(), and a person approves, edits or rejects it. Resume with Command(resume=...) and the same thread ID. Keep side effects after approval because the interrupted node restarts when resumed.

Key takeaways

  • State stores raw facts needed across nodes; prompts are formatted inside nodes.
  • Nodes return state updates and edges define allowed movement.
  • A checkpointer and thread ID make pause/resume possible.
  • Interrupt should precede non-idempotent side effects.
  • Test duplicate resumes, wrong thread IDs and rejected approval before adding real tools.

What will you build?

The example is a low-risk review assistant. It accepts a fictional request, validates required text, creates a deterministic placeholder draft, pauses for review and stores the final decision. It does not call a model, send a message or modify an external system. That keeps the tutorial focused on LangGraph state and control.

Figure 1. Stateful draft-review execution

No external side effect occurs in this learning graph.

Text equivalent: input is validated and converted into a draft stored in state. The review node pauses through an interrupt and the checkpointer saves the thread. A later call resumes the same thread with a human decision. The graph records an approved, edited or rejected final state and ends without sending data elsewhere.

Step 1: install LangGraph and define state

pip install -U langgraph

from typing import Literal, TypedDict

class ReviewState(TypedDict, total=False): request_id: str request_text: str draft: str status: Literal["new", "waiting", "approved", "rejected"] reviewer_note: str

Store values that must survive across nodes. Keep the original request and draft as raw data; format review messages only when needed.

Step 2: add validation and draft nodes

def validate(state: ReviewState):
    text = state.get("request_text", "").strip()
    if not text:
        raise ValueError("request_text is required")
    return {"status": "new"}

def create_draft(state: ReviewState): # Deterministic placeholder: replace with a tested model call later. draft = f"Draft for review: {state['request_text']}" return {"draft": draft, "status": "waiting"}

Each node receives current state and returns only its updates. Validation remains deterministic. A future model call should return structured output and have separate error handling.

Step 3: pause for human review

from langgraph.types import interrupt

def review(state: ReviewState): # Put interrupt before any non-idempotent side effect. decision = interrupt({ "request_id": state["request_id"], "draft": state["draft"], "allowed": ["approve", "edit", "reject"], }) if decision["action"] == "reject": return {"status": "rejected", "reviewer_note": decision.get("note", "")} return { "status": "approved", "draft": decision.get("edited_draft", state["draft"]), "reviewer_note": decision.get("note", ""), }

On resume, the interrupted node starts again from its beginning. Never place an unguarded email, payment, database write or other irreversible operation before interrupt().

Step 4: compile with a checkpointer

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

builder = StateGraph(ReviewState) builder.add_node("validate", validate) builder.add_node("create_draft", create_draft) builder.add_node("review", review) builder.add_edge(START, "validate") builder.add_edge("validate", "create_draft") builder.add_edge("create_draft", "review") builder.add_edge("review", END)

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

The in-memory saver is suitable for this local tutorial, not a production deployment. Production needs durable storage and explicit retention, privacy and recovery decisions.

Step 5: invoke, inspect and resume

from langgraph.types import Command

config = {"configurable": {"thread_id": "review-case-1042"}} initial = { "request_id": "case-1042", "request_text": "Prepare a short acknowledgement.", }

paused = graph.invoke(initial, config=config)

Inspect paused["interrupt"] in the invoke API.

finished = graph.invoke( Command(resume={"action": "approve", "note": "Checked"}), config=config, ) assert finished["status"] == "approved"

The same thread ID is essential. Treat it as an access-controlled pointer to persisted state, not a guessable public token.

Permissions and recovery boundaries

RiskControlSafe result
Wrong person resumes threadAuthenticate caller and authorize access to the business transactionNo cross-user state access
Code before interrupt repeatsPut side effect after approval or make operation idempotentNo duplicate write
Process crashes while waitingDurable production checkpointer and stable thread IDState can be recovered
Malformed resume payloadValidate action and fields against a schemaGraph remains paused or fails safely
Approval rejectedTerminal rejected status; no external actionDecision is preserved
External action times out laterRead destination state before retryNo blind duplicate action

Test matrix

TestPass condition
Valid inputPauses with draft and request ID
Missing textFails before draft/review
ApproveStatus approved; original draft retained
EditStatus approved; edited draft stored
RejectStatus rejected; no side effect
Wrong thread IDCannot resume target state
Malformed decisionRejected by validation
Duplicate resumeNo external write; terminal state handled predictably

Success criteria

  • State contains only required raw values.
  • Each node has one observable responsibility.
  • The graph pauses and resumes with the same thread ID.
  • Approval, edit and rejection paths are tested.
  • No external action occurs before authorization.
  • Production design names a durable checkpointer, retention policy and recovery owner.

Choose training that matches your stack

This tutorial is for code-first learners. If your goal is Python or JavaScript agent engineering, continue with official LangGraph documentation, software testing and deployment practice. Barcelona Code School's current AI Agent & Automation Bootcamp teaches visual business automation with n8n rather than LangGraph development.

Compare BCS AI learning paths

Frequently asked questions

What makes a LangGraph agent stateful?

Nodes read and update shared graph state, while a checkpointer saves state for a thread. Reusing the same thread ID lets a later invocation load and continue that thread rather than starting with empty state.

Why does LangGraph need a thread ID?

The thread ID identifies which persisted checkpoints belong to a run or conversation. Use a stable unique identifier for the business transaction or session and enforce access so one user cannot resume another user's state.

How does interrupt work in LangGraph?

The interrupt function pauses a node, persists graph state through the configured checkpointer and returns a JSON-serializable request to the caller. The graph continues when invoked again with Command resume and the same thread ID.

Can code before interrupt run twice?

Yes. When a paused graph resumes, the interrupted node restarts from its beginning. Put interrupt before non-idempotent side effects, or protect any earlier operation with an idempotency key and destination-state check.

Is MemorySaver suitable for production?

Use an in-memory saver only for local learning and tests. Production applications need a durable checkpointer selected and configured for their infrastructure, retention, privacy, concurrency and recovery requirements.

Sources

  1. LangGraph overview.
  2. Graph API.
  3. Persistence.
  4. Durable execution.
  5. Interrupts.
  6. BCS: How to Build an AI Agent.
Back to posts