Asking a human first
HumanInTheLoopMiddleware is a middleware that pauses an agent before a chosen tool runs and waits for a person to approve or reject the call.
Last updated: 27 Sep, 2026 · LangChain 1.4
Some tools should not run on a model's word alone, such as a refund. The shop adds a second tool to tools.py, and the stand-in model from earlier asks for it when a message mentions a refund.
Pick one to watch it run, step by step.
HumanInTheLoopMiddleware and a checkpointer
from langchain.agents.middleware import HumanInTheLoopMiddleware
from langgraph.checkpoint.memory import InMemorySaver
approval = HumanInTheLoopMiddleware(interrupt_on={"refund_order": True}) # pause on this tool
# a checkpointer is required so the paused run can be resumed later
agent = create_agent(model, tools=tools, middleware=[approval], checkpointer=InMemorySaver())The refund tool
First the new tool. It refunds an order, which cannot be undone, so it is the call a person should confirm.
@tool
def refund_order(order_id: str) -> str:
"""Refund an order in full. This cannot be undone."""
return f"Refunded {order_id}."The agent with approval
Build the agent with both tools and the middleware. interrupt_on names the tools that need a decision; True means every call to that tool pauses. lookup_order is not listed, so it runs as before. The checkpointer is required: a paused agent is saved, and the decision may come minutes or days later.
from langchain.agents import create_agent
from langchain.agents.middleware import HumanInTheLoopMiddleware
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.types import Command
from shop_model import ShopModel
from tools import lookup_order, refund_order
approval = HumanInTheLoopMiddleware(interrupt_on={"refund_order": True})
agent = create_agent(ShopModel(), tools=[lookup_order, refund_order],
middleware=[approval], checkpointer=InMemorySaver())The pause
Ask for a refund. With version="v2", invoke returns an object with the state so far and whatever is waiting for a decision.
thread = {"configurable": {"thread_id": "ravi-refund"}}
ask = {"messages": [{"role": "user", "content": "Please refund A17"}]}
result = agent.invoke(ask, thread, version="v2")
print(result.interrupts[0].value["action_requests"])
print(result.value["messages"][-1].tool_calls[0]["name"])Invoking with version="v2" returns an object with two parts. value is the state so far, ending with the model's request to refund A17. interrupts holds what is waiting for a decision: the tool, its arguments and a description a reviewer can read. The refund has not run.
Approving
Send a decision back on the same thread with Command(resume=...).
decision = Command(resume={"decisions": [{"type": "approve"}]})
result = agent.invoke(decision, thread, version="v2")
for message in result.value["messages"][2:]:
print(f"{message.type:<4} {message.text}")A Command with resume continues the same thread, and decisions holds one decision per paused call. After the approval the refund ran and the model answered.
Rejecting
Reject instead, with a reason for the model to relay.
decision = Command(resume={"decisions": [{"type": "reject", "message": "Refunds need a manager."}]})
result = agent.invoke(decision, thread, version="v2")
for message in result.value["messages"][2:]:
print(f"{message.type:<4} {message.text}")A rejection never runs the tool. The model gets a tool message with the reason instead, so it can tell the customer why. Without a message, the middleware writes a default one telling the model not to try the same call again.
What each decision did
- The run paused before the refund. With
version="v2",invokereturnedvalue(the state so far) andinterrupts(the pending call); the refund had not run. - Approve runs the tool.
Command(resume=...)with{"type": "approve"}continued the thread, the refund ran, and the model confirmed it. - Reject skips the tool. The model got a tool message with the reason instead, so it could explain the refusal.
- The checkpointer holds the pause. The decision can arrive any time later because the paused state is saved.
Approve vs reject
| approve | reject | |
|---|---|---|
| Runs the tool | Yes | No |
| Tool message | The tool's real result | The reason, or a default refusal |
| Model then | Uses the result in its reply | Explains why it could not act |
Where a pause for approval fits
- A refund, a delete, or any action that cannot be undone.
- A step that spends money or emails a customer, held for a person to confirm.
- A review queue where approvals arrive minutes or days after the request.
HumanInTheLoopMiddleware needs a checkpointer. Without one there is nowhere to save the paused run, and the resume has nothing to continue.Related
- Previous: More built-in middleware to reach for
- Next: Edit and respond
- Reference: LangChain agent middleware
- Ask about A17 without a refund and check that the agent does not pause.
- Print
result.interrupts[0].value["review_configs"]to see which decisions are allowed. - After the pause, print
agent.get_state(thread).nextto see where the run is waiting.
You understood something today that you didn't yesterday.