Whetstone.
Production-Ready AgentHuman in the loop
Module 3, Lesson 415 min

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

Per-tool approval policy
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.

The payload, and the resume
# 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.

Practice

Try it yourself

Quiz

How many decisions

The ground school course taught three decision types. The current middleware documents four.

  1. Aapprove, edit, reject
  2. Bapprove, edit, reject, retry
  3. Capprove, edit, reject, escalate
  4. Dapprove, edit, reject, respond
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.

Recall

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.

Quiz

Reading an interrupt_on mapping

Three tools, three different values. What happens to read_data?

A mixed approval policy
interrupt_on={
    "write_file": True,
    "execute_sql": {"allowed_decisions": ["approve", "reject"]},
    "read_data": False,
}
  1. AIt pauses with the default decision set, same as write_file
  2. BIt runs without pausing; False means auto-approve
  3. CIt is blocked entirely and can never be called
  4. DIt raises at construction, because False is not a valid value
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.

Quiz

The missing prerequisite

An agent with HumanInTheLoopMiddleware pauses correctly, but the pending approval is gone after a process restart. What is missing?

  1. AA durable checkpointer; the interrupt state is not being persisted
  2. BA description_prefix on the middleware, so the approval is addressable
  3. CThe respond decision in allowed_decisions, which persists the pending call
  4. DNothing; interrupts are always in-process and cannot survive a restart
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.

Do

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.
Done whenThe harmless tool never pauses, both resume paths behave differently, and you can point at the part of the payload that told you which decisions were permitted.
Sign in to track your progress →