# LangGraph and interrupt: making an agent stop and ask before it does something that matters

> At a client site, the hard question is rarely whether the agent can do the job. It is who gets to press the button that lets it, and LangGraph's interrupt function was built for exactly that moment.

Bản gốc: https://fdetimes.net/en/tools/langgraph-interrupt-human-approval/

Picture this: the agent you deployed for a client has drafted a refund and is one step away from sending it. The person responsible on the client side wants to review it before any money leaves the account. That might take a few minutes, or it might wait until the next morning.

The LangChain team stated the reason for this plainly on its official blog on 14 December 2024: agents can be powerful, but they are not perfect. LangGraph's `interrupt` function grew out of that observation, and according to the LangChain team it was designed to run in production.

For an FDE this is not a minor technical detail. When an agent touches a client's money or live systems, you should design a place for a human to say "wait" from the start, rather than waiting for the client to ask for one.

## What LangGraph is, and what it is not

LangGraph describes itself as a low-level orchestration framework for building, managing and deploying stateful, long-running agents. It is built by LangChain Inc, the team behind LangChain, but you can use it without LangChain. The code is released under the MIT licence.

"Low-level" deserves a careful reading. LangGraph does not hand you a ready-made agent. It gives you the parts to assemble one yourself: a graph of steps, the state that flows through each step, and places to stop.

The developers stress two benefits. The first is durable execution: the agent survives failures and can run for a long time. The second is human-in-the-loop: you can inspect and modify the agent's state at any point while it runs.

## Three pieces for one pause point

According to the official documentation, interrupt lets you pause a graph at specific points and wait for external input before continuing. Pausing requires somewhere to store state, so the documentation requires a checkpointer and recommends a durable one for production.

The checkpointer saves each thread's state as checkpoints. The same mechanism serves several purposes: conversation memory, human-in-the-loop, time travel and fault tolerance. The third piece is `thread_id`. Reuse an existing value and you continue from that checkpoint; use a new value and you start an entirely new thread with empty state.

So how does the reviewer's decision get in? Whatever you pass to `Command(resume=...)` becomes the return value of the `interrupt` call itself. From inside the node, `interrupt` looks like an ordinary function call: you call it, it waits, and what comes back is the reviewer's decision.

## A minimal approval node

The documentation says the most common use of interrupt is to pause before an important action and ask for approval. The example below is a runnable illustration: a one-node graph with an in-memory checkpointer. The first call stops at interrupt; the second resumes with the same `thread_id`. The identifiers are Vietnamese: `so_tien` is the amount, `ket_qua` the result, `duyet_hoan_tien` "approve refund", `thuc_hien_hoan_tien` "execute refund", `quyet_dinh` the decision and `dong_y` "approve".

```python
from typing import TypedDict
from langgraph.graph import StateGraph, START, END
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.types import interrupt, Command

class State(TypedDict):
so_tien: int
ket_qua: str

def thuc_hien_hoan_tien(state):
print("Đã hoàn tiền:", state["so_tien"])

def duyet_hoan_tien(state: State):
quyet_dinh = interrupt({
"hanh_dong": "hoàn tiền",
"so_tien": state["so_tien"],
})
if quyet_dinh == "dong_y":
thuc_hien_hoan_tien(state)   # chỉ chạy SAU khi đã được duyệt
return {"ket_qua": quyet_dinh}

builder = StateGraph(State)
builder.add_node("duyet_hoan_tien", duyet_hoan_tien)
builder.add_edge(START, "duyet_hoan_tien")
builder.add_edge("duyet_hoan_tien", END)

# InMemorySaver chỉ để thử trên máy; production cần checkpointer bền vững
graph = builder.compile(checkpointer=InMemorySaver())

config = {"configurable": {"thread_id": "yeu-cau-hoan-tien-001"}}

# Lần chạy đầu: đồ thị dừng tại interrupt, state được lưu theo thread_id
ket_qua = graph.invoke({"so_tien": 100}, config=config)
print(ket_qua["__interrupt__"])   # payload đang chờ người duyệt

# Khi người duyệt bấm nút, gọi lại với CÙNG config (cùng thread_id)
graph.invoke(Command(resume="dong_y"), config=config)
```

The code comments say, in order: the refund only runs after approval; `InMemorySaver` is for local testing only and production needs a durable checkpointer; the first run stops at interrupt and state is saved by `thread_id`; the printed value is the payload awaiting review; and when the reviewer presses the button, call again with the same config, meaning the same `thread_id`.

The approval interface can be whatever the client already uses. Your job is to take the payload from interrupt, show it to the right person, and send the decision back through `Command(resume=...)` with the correct `thread_id`. Send it to a new `thread_id` by mistake and you get an empty thread, not the pending refund.

## The trap in the lines above interrupt

The documentation is explicit: on resume, the node runs again from the beginning, so every line before `interrupt` executes a second time. The rule that follows is never to put a non-idempotent operation before interrupt.

**Điểm mấu chốt:** On resume, the node runs again from the top: anything that must not happen twice belongs after interrupt.

Suppose you send a "your request is awaiting approval" email just before interrupt. The reviewer approves, the node re-runs, and the customer gets a second email. Replace the email with a write to an accounting system and the mistake is enough to cost the whole project its credibility.

The simplest fix is to move the email below interrupt, or take it out of the node altogether and let the approval interface handle notifications. If it has to stay above, make it idempotent: for example, record against the `thread_id` that the email has been sent, and check for that marker before sending again.

## When should you choose LangGraph?

Consider LangGraph when the client's process has clear steps, includes actions that cannot be undone, and has someone accountable for approval who is not sitting there waiting. Long-running execution and persisted state are what let an agent wait overnight without losing its work in progress.

The price is that you decide a lot yourself, because this is a low-level tool: choosing a durable checkpointer for production, managing a `thread_id` tied to each business request, and reviewing every node for lines that will re-run. If the workflow is a single question-and-answer turn that nobody needs to approve, you will not use most of this power.

## What to learn first, and how to put it on a CV

A sensible order has three stages: the checkpointer and `thread_id`, then the `interrupt` and `Command(resume=...)` pair, and only then the lesson about nodes re-running. The first two are mechanics. The last is what separates people who have run this for real from those who have only read the documentation.

When reading job descriptions for FDE or AI engineer roles, watch for phrases such as "human-in-the-loop", "stateful agent" or "long-running workflow". On your CV, rather than writing "used LangGraph", say specifically which action you placed an approval point before, where state was stored, and how you handled resume.

Clients rarely ask how clever your agent is. They ask where it will stop, and who has the authority to let it carry on.

**Thử ngay tuần này:**

- Build a LangGraph graph with exactly one approval node before a simulated 'refund' action, and resume it with Command(resume=...) using two values: approve and reject.
- Deliberately put a log print or file write before interrupt, resume, and count how many times it runs to see node re-execution for yourself.
- Rewrite one line of your CV to describe how you designed a human approval point for an agent, stating which action was gated and why.

## Nguồn

- [langchain-ai/langgraph (GitHub README)](https://github.com/langchain-ai/langgraph)

- [Interrupts – LangGraph docs (docs.langchain.com)](https://docs.langchain.com/oss/python/langgraph/interrupts)

- [Interrupts – LangGraph docs (docs.langchain.com)](https://docs.langchain.com/oss/python/langgraph/human-in-the-loop)

- [Persistence – LangGraph docs (docs.langchain.com)](https://docs.langchain.com/oss/python/langgraph/persistence)

- [Making it easier to build human-in-the-loop agents with interrupt](https://www.langchain.com/blog/making-it-easier-to-build-human-in-the-loop-agents-with-interrupt)
