π§© LangGraph Interrupt Workflow Template
β‘ A production-ready, provider-agnostic template for building human-in-the-loop AI workflows with LangGraph v1. Pause an agent mid-execution, ask a human to approve / edit / redirect, then resume exactly where it left off β with a polished Next.js chat UI on top.
Runs with zero configuration. Clone it and go β a built-in mock model lets you explore the full interrupt flow without any API keys. Add a provider when you're ready.
One click β a ready-to-run dev container with Python, Node, and all dependencies installed.
β¨ Why this template?
Most "agent" demos run start-to-finish with no human control. Real-world systems β approvals, content review, high-stakes tool calls β need a human in the loop. This template shows three complementary ways to do that with the latest LangGraph:
| Pattern | File | Best for |
|---|---|---|
| π οΈ Custom multi-step interrupt graph | backend/graph.py |
Workflows with several explicit decision points (approve plan β pick direction β choose format). |
| β Approve / edit / reject workflow | backend/approval_workflow.py |
Draft-then-review flows: the AI drafts, a human approves, edits, or rejects with feedback (redrafts on reject). |
| π€ Prebuilt agent + HITL middleware | backend/agent.py |
Tool-using agents where you want approval only before sensitive actions, with minimal code (create_agent + HumanInTheLoopMiddleware). |
All three use the same primitive: interrupt() pauses the graph, persists state, and waits for Command(resume=...).
π Two engines, one UI
The chat app ships with a live Workflow β Agent toggle so you can compare the two control paradigms on the same screen:
| Engine | Control flow | Human-in-the-loop |
|---|---|---|
Workflow (graph.py) |
Deterministic StateGraph β fixed interrupt points + parallel Send research |
Structured choices at each step |
Agent (agent.py) |
Model-driven create_agent loop β the LLM decides when to call tools |
Approve / edit / reject the tool call before it runs |
Both share the same provider-agnostic LLM, web_search tool, and long-term memory.
π Features
- π§© Human-in-the-loop, done right β multiple interrupt points, resume with approve/edit/redirect.
- π Parallel research (
Send) β a planner fans out concurrent sub-researchers (map-reduce) viaCommand(goto=[Send(...)]), with live progress streaming. - π§ Long-term memory β a LangGraph
Storeremembers a user's topics & preferences across sessions, not just within a thread. - βͺ Time travel β rewind to any past checkpoint and fork a different path; the original run is preserved.
- π Provider-agnostic β OpenAI, Anthropic, Google, Groq, Mistral, IBM watsonx, Ollamaβ¦ via LangChain's
init_chat_model. One env var to switch. - π Zero-config demo β a streaming-capable mock model runs the whole app with no API keys.
- πΎ Durable execution β optional
AsyncSqliteSavercheckpointer; workflows survive server restarts. - π€ Latest agent stack β LangGraph v1.2 + LangChain v1,
create_agent, andHumanInTheLoopMiddleware. - π‘ Streaming β Server-Sent Events stream progress and the final answer to the UI.
- π LangGraph Studio ready β
langgraph.jsonregisters all graphs forlanggraph dev. - π¨ Modern UI β Next.js 15 + React 19 chat interface with live progress and a rewind panel.
- β Tested & CI'd β pytest suite + GitHub Actions for backend and frontend.
ποΈ Architecture
ββββββββββββββββββββ HTTP / SSE ββββββββββββββββββββ ββββββββββββββββββββββ
β Frontend β βββββββββββββββββββΊ β Backend β βββββββΊ β LangGraph β
β Next.js 15 β βββββββββββββββββββ β FastAPI β βββββββ β workflow β
β β’ Chat UI β β β’ /start /resume β β β’ interrupt() β
β β’ Interrupt cardsβ β β’ /stream (SSE) β β β’ checkpointer β
β β’ Live progress β β β’ lifespan graph β β β’ resume via Commandβ
ββββββββββββββββββββ ββββββββββββββββββββ ββββββββββββββββββββββ
β‘ Quick Start (zero config)
# 1. Backend
cd backend
python -m venv .venv && source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -r requirements.txt
cp .env.example .env # works as-is with the built-in mock model
python main.py # http://localhost:8000 (docs at /docs)
# 2. Frontend (new terminal)
cd frontend
npm install
npm run dev # http://localhost:3000
Open http://localhost:3000, ask a question, and watch the workflow pause for your input at each interrupt.
Use a real LLM
Edit backend/.env and set a model + matching API key:
LLM_MODEL=gpt-4o-mini
OPENAI_API_KEY=sk-...
# or: LLM_MODEL=claude-sonnet-4-5 LLM_PROVIDER=anthropic ANTHROPIC_API_KEY=...
That's it β the workflow and agent automatically use it.
π§ How interrupts work
from langgraph.types import interrupt, Command
async def format_selection_interrupt(state):
# Pause and ask the human. State is persisted automatically.
choice = interrupt("How should I format the final answer?")
return {"format_choice": choice}
# Later, from your API:
await graph.ainvoke(Command(resume="executive"), config) # resumes exactly here
- A node calls
interrupt(payload)β execution pauses, state is checkpointed. - The API returns the interrupt payload to the UI (
requires_input: true). - The user picks an option; the API calls
ainvoke(Command(resume=choice)). - The graph continues from the interrupted node with the user's input.
The modern agent pattern (HITL middleware)
from langchain.agents import create_agent
from langchain.agents.middleware import HumanInTheLoopMiddleware
agent = create_agent(
model,
tools=[web_search],
middleware=[HumanInTheLoopMiddleware(interrupt_on={"web_search": True})],
checkpointer=checkpointer,
)
Try it from the CLI:
cd backend && python agent.py "What are the latest advances in solid-state batteries?"
Approve / edit / reject workflow
A second example (backend/approval_workflow.py, UI at /approval) shows the
three canonical human actions on a single interrupt:
- Approve β send the draft as-is
- Edit β send the human's revised version
- Reject + feedback β the AI redrafts using the feedback, then pauses again
response = interrupt({"type": "approval", "draft": draft, "actions": ["approve", "edit", "reject"]})
# resume with: Command(resume={"action": "reject", "feedback": "Make it shorter"})
π§ͺ Advanced LangGraph features
The research workflow is also a tour of LangGraph's more powerful primitives:
Parallel research with Send (map-reduce). A planner node splits the
question into sub-questions and fans them out to concurrent workers, combining
Command (state update + dynamic routing) with Send (one task per item):
return Command(
goto=[Send("sub_researcher", {"sub_query": q}) for q in sub_queries],
update={"sub_queries": sub_queries},
)
Each worker streams a progress event (get_stream_writer) so the UI shows
"Researched: β¦" live; results aggregate through a reset-aware reducer.
Cross-thread long-term memory (Store). Pass a user_id and the assistant
remembers across sessions β a brand-new thread recalls prior topics/preferences:
graph = build_research_graph(checkpointer=saver, store=InMemoryStore())
# in a node: await store.aput(("memories", user_id), key, {"text": note})
Time travel (rewind & fork). List checkpoints and resume from any past one down a different path β without losing the original run:
async for snap in graph.aget_state_history(config): ... # GET /history/{thread_id}
# resume from a past checkpoint -> POST /fork
graph.astream(Command(resume=new_choice),
config={"configurable": {"thread_id": tid, "checkpoint_id": cid}})
π Visualize with LangGraph Studio
pip install "langgraph-cli[inmem]"
langgraph dev # opens LangGraph Studio with the research, approval, and agent graphs
langgraph.json registers all three graphs so you can step through interrupts visually.
π‘ API reference
| Endpoint | Method | Description |
|---|---|---|
/start |
POST | Start a research thread (user_id enables memory) |
/resume |
POST | Resume an interrupted workflow with a choice |
/stream |
GET/POST | Resume and stream progress + the final answer (SSE) |
/continue |
POST | Ask a follow-up on an existing thread (keeps memory) |
/history/{thread_id} |
GET | List checkpoints for time travel |
/fork |
POST | Rewind to a checkpoint and resume a different path |
/get_state/{thread_id} |
GET | Inspect current workflow state |
/agent/start |
POST | Start/continue the agent engine (SSE) |
/agent/decide |
POST | Resume the agent with approve/edit/reject/respond (SSE) |
/approval/start |
POST | Draft content for a task and pause for review |
/approval/decide |
POST | Resume with approve / edit / reject |
/health |
GET | Liveness probe |
βοΈ Configuration
All configuration is via environment variables (see backend/.env.example).
| Variable | Description | Default |
|---|---|---|
LLM_MODEL |
Model id (e.g. gpt-4o-mini) |
mock model |
LLM_PROVIDER |
Provider override (openai, anthropic, ibm, β¦) |
inferred |
LLM_TEMPERATURE |
Sampling temperature | 0.7 |
USE_MOCK_LLM |
Force the offline mock model | false |
TAVILY_API_KEY |
Enables live web search in tools.py |
β |
CHECKPOINT_DB |
Path to enable durable SQLite persistence | in-memory |
CORS_ORIGINS |
Comma-separated allowed origins | * |
PORT |
Backend port | 8000 |
π³ Docker
cp backend/.env.example backend/.env # configure as needed
docker compose up --build
# Frontend β http://localhost:3000 Backend β http://localhost:8000
Compose runs the backend (with a durable SQLite checkpoint volume) and the Next.js frontend as separate services.
π¨ Customizing for your use case
This template ships a research assistant example to demonstrate the patterns. To adapt it:
- Define your state in
backend/graph.py(ResearchState). - Add nodes, calling
interrupt(...)wherever you need a human decision. - Wire edges in
build_research_graph(). - Add tools in
backend/tools.pyand expose them to the agent. - Swap the LLM by changing env vars β no code changes needed.
Great fits: content review & approval, data-processing pipelines, quality control, configuration wizards, customer-support escalation, and any workflow needing human oversight.
π§ͺ Testing
cd backend
USE_MOCK_LLM=true pytest -v # fast, offline, no API keys
π Project structure
langgraph-interrupt-workflow-template/
βββ backend/
β βββ main.py # FastAPI app (lifespan-managed graphs, SSE streaming)
β βββ graph.py # Multi-step human-in-the-loop research workflow
β βββ approval_workflow.py # Approve / edit / reject workflow
β βββ agent.py # create_agent + HumanInTheLoopMiddleware example
β βββ memory.py # Cross-thread long-term memory (Store)
β βββ llm.py # Provider-agnostic LLM factory + offline mock model
β βββ tools.py # Example web_search tool (Tavily / mock)
β βββ test_main.py # Pytest suite
β βββ requirements.txt
β βββ .env.example
βββ frontend/ # Next.js 15 + React 19 UI
β βββ app/page.tsx # Research assistant chat
β βββ app/approval/page.tsx # Approve / edit / reject UI
β βββ Dockerfile
βββ .devcontainer/ # GitHub Codespaces / VS Code dev container
βββ langgraph.json # LangGraph Studio config (research + approval + agent)
βββ .github/workflows/ci.yml
βββ Dockerfile # Backend image
βββ docker-compose.yml # Backend + frontend services
βββ README.md
π€ Contributing
Contributions welcome! See CONTRIBUTING.md. Fork β branch β PR.
π License
MIT β see LICENSE.