Human in the loop
An approval gate is a middleware, and underneath it is interrupt(), which pauses a compiled graph mid-run and waits.
The declaration
from langchain.agents.middleware import HumanInTheLoopMiddleware
HumanInTheLoopMiddleware(
interrupt_on={
"write_file": True,
"execute_sql": {"allowed_decisions": ["approve", "reject"]},
"read_data": False,
},
description_prefix="Tool execution requires approval",
)Three value shapes, and one of them reads backwards from every permission system you have used.
True: pause, with all decisions permitted.- A config object: pause, with only the listed decisions permitted.
False: auto-approve. The tool runs freely.
The boolean answers should this pause, not is this allowed. Reading False as “denied” is the natural error and it produces exactly the wrong policy.
Four decisions, not three
| decision | what happens |
|---|---|
approve |
Execute the tool with the original arguments as proposed. |
edit |
Modify the tool arguments, then execute. |
reject |
Skip the call entirely and return rejection feedback to the loop. |
respond |
Skip execution and return the human’s message as a synthetic ToolMessage with status="success". |
Pausing and resuming
The interrupt fires after the model produces its response and before the tool executes, which is the only point where the arguments exist and nothing has happened yet.
# What interrupt() surfaces:
{
'action_requests': [
{'name': 'execute_sql', 'args': {...}, 'description': '...'}
],
'review_configs': [
{'action_name': 'execute_sql', 'allowed_decisions': ['approve', 'reject']}
],
}
# How you resume, decisions in the same order as the requests:
from langgraph.types import Command
agent.invoke(Command(resume={"decisions": [{"type": "approve"}]}), config=config)The other decision payloads are {"type": "edit", "edited_action": {...}}, {"type": "reject", "message": "..."} and {"type": "respond", "message": "..."}.
The prerequisite that is not optional
You must configure a checkpointer. An interrupt is a paused graph, and a paused graph only exists if something is persisting it.
Try it yourself
How many decisions
The ground school course taught three decision types. The current middleware documents four.
Show answer
Correct answer: D — approve, edit, reject, respond
The fourth is respond, which returns the human's message directly as a synthetic tool result. The first option is the trap for anyone who learned this a version ago, and it is a fair trap: three was correct then and is incomplete now. retry and escalate are invented, and both sound plausible because they name things approval workflows genuinely do.
respond against reject
These two are close enough that the difference is worth stating precisely, because both stop the tool from running.
What is the difference between the respond and reject decisions, in terms of what the model sees next?
Reveal answer
Both prevent the tool call from executing. reject skips the call and feeds rejection feedback back into the loop, so the model learns that its proposed action was refused. respond returns the human's message directly as a synthetic tool result, so the model reads it as though the tool had run and returned that content. reject says no; respond answers on the tool's behalf, which is the stronger and more surprising of the two.
Reading an interrupt_on mapping
Three tools, three different values. What happens to read_data?
interrupt_on={
"write_file": True,
"execute_sql": {"allowed_decisions": ["approve", "reject"]},
"read_data": False,
}Show answer
Correct answer: B — It runs without pausing; False means auto-approve
False means auto-approve, so the tool runs freely and never interrupts. The third option is the tempting misreading, because False reads as denial in almost every other permission system you have used; here the boolean answers should this pause, not is this allowed.
The missing prerequisite
An agent with HumanInTheLoopMiddleware pauses correctly, but the pending approval is gone after a process restart. What is missing?
Show answer
Correct answer: A — A durable checkpointer; the interrupt state is not being persisted
Interrupts persist through the checkpointer, so a durable one is what makes a pending approval survive a restart. The last option is the plausible-sounding wrong answer, and it matters practically: surviving a restart is exactly the property that makes an approval gate usable, because a human deciding hours later is the normal case rather than the exception.
Gate one destructive tool
Small, local, and it proves the property that actually matters, which is that a decision made later still lands.
- Take a two-tool agent where one tool is harmless and one is destructive, and attach InMemorySaver.
- Add HumanInTheLoopMiddleware with the destructive tool set to allow approve and reject only, and the harmless one left ungated.
- Trigger the interrupt and print the payload; note the action_requests and review_configs keys and what each carries.
- Resume with an approve decision, then run it again and resume with a reject, and compare what the model does next in each case.