Human-in-the-loop: approving a booking
Human-in-the-loop is a pause a deep agent makes before running a tool you name in interrupt_on: the run stops, you see the tool and its arguments, and you resume it with a decision to approve, edit or reject the call.
Last updated: 29 Sep, 2026 · Deep Agents 0.7
The video names interrupts among the parameters of create_deep_agent and leaves them for later; this lesson follows the docs. It gives the trip planner its first action with a cost: a tool that books a hotel. Before it spends the traveller's money, the agent stops and asks.
The interrupt_on syntax
agent = create_deep_agent(..., interrupt_on={"book_hotel": True}, checkpointer=InMemorySaver())
result = agent.invoke(..., config=config) # stops before book_hotel
result["__interrupt__"][0].value["action_requests"] # what it wanted to run
agent.invoke(Command(resume={"decisions": [{"type": "approve"}]}), config=config)A booking tool
Add book_hotel to trip.py under search_travel, the tool from Tools: a travel search the agent can call. It only returns a sentence here; in a real app it would charge a card.
from langchain.tools import tool
CATALOG = {
"paris": {
"flight": ["Return flight Delhi to Paris: 42,000 rupees"],
"hotel": ["Seine Budget Inn, Latin Quarter: 5,200 rupees a night",
"Hotel Lumiere, Montmartre: 7,500 rupees a night",
"Le Grand Opera Hotel: 16,000 rupees a night"],
"sight": ["Eiffel Tower summit: 3,100 rupees", "Louvre Museum: 2,000 rupees",
"Seine river cruise: 1,500 rupees", "Versailles day trip: 2,600 rupees",
"Montmartre walking tour: free"],
"food": ["Cafe breakfast and bistro dinner: 3,000 rupees a day"],
},
}
@tool
def search_travel(city: str, kind: str) -> str:
"""Search the travel catalog. kind is "flight", "hotel", "sight" or "food". Prices are in rupees."""
entries = CATALOG.get(city.lower(), {}).get(kind)
return "\n".join(entries) if entries else f"The catalog has no {kind} entries for {city}."
@tool
def book_hotel(hotel: str, nights: int) -> str:
"""Book a hotel for a number of nights. This spends the traveller's money."""
return f"Booked {hotel} for {nights} nights."An agent that pauses before booking
interrupt_on maps tool names to True. A pause needs saved state, so the agent gets a checkpointer, and every call uses a thread_id.
from deepagents import create_deep_agent
from langchain.chat_models import init_chat_model
model = init_chat_model("groq:openai/gpt-oss-120b", temperature=0, max_retries=6)from langgraph.checkpoint.memory import InMemorySaver
from langgraph.types import Command
agent = create_deep_agent(
model=model,
tools=[search_travel, book_hotel],
interrupt_on={"book_hotel": True}, # pause before every booking
checkpointer=InMemorySaver(), # a pause needs saved state
system_prompt="You book trips. Look up prices with search_travel. Reply in one short sentence.",
)Approving one booking and rejecting another
The loop runs the same request twice on two threads. Each time it reads the paused call, prints it, and resumes with a decision: first approve, then reject with a message for the model.
for decision in [{"type": "approve"},
{"type": "reject", "message": "The traveller wants to wait. Do not book, and do not try again."}]:
config = {"configurable": {"thread_id": decision["type"]}}
result = agent.invoke({"messages": [{"role": "user", "content": "Book the cheapest Paris hotel for 3 nights."}]}, config=config)
request = result["__interrupt__"][0].value["action_requests"][0]
print("paused before:", request["name"], request["args"])
result = agent.invoke(Command(resume={"decisions": [decision]}), config=config)
print(f"{decision['type']:<7} ->", result["messages"][-2].text, "|", result["messages"][-1].text, "\n")paused before: book_hotel {'hotel': 'Seine Budget Inn', 'nights': 3}
approve -> Booked Seine Budget Inn for 3 nights. | Seine Budget Inn booked for 3 nights.
paused before: book_hotel {'hotel': 'Seine Budget Inn', 'nights': 3}
reject -> User rejected the tool call for `book_hotel` with reason: The traveller wants to wait. Do not book, and do not try again. | The cheapest Paris hotel is Seine Budget Inn at 5,200 rupees per night, totaling 15,600 rupees for a 3‑night stay. What approve and reject did
- The pause: both runs stopped before
book_hotel, with the Seine Budget Inn for 3 nights as the arguments, read fromaction_requests. - approve: the tool ran with those arguments, the tool message says "Booked ...", and the agent confirmed.
- reject: the tool never ran. The tool message is the rejection with your reason, and the agent's reply gives the price without claiming a booking.
The four decision types
| Decision | What happens | Use it when |
|---|---|---|
approve | The tool runs as proposed | The call is right |
edit | The tool runs with arguments you changed | The call is almost right |
reject | The tool is skipped; your message goes back to the model | The action should not happen |
respond | Your text becomes the tool's result | The tool asks a person a question |
Limit the choices per tool with {"allowed_decisions": ["approve", "reject"]} instead of True.
Where approval belongs
- Anything that spends money: bookings, purchases, paid API calls.
- Anything that cannot be undone: sending an email, deleting a file.
- Risky file writes: the docs suggest approval on
write_fileandedit_filewhen the backend is your disk.
config, including the same thread_id. A new thread has no paused run, and the decision goes nowhere.Related
- Previous: CompiledSubAgent: your own agent as a subagent
- Next: Edit decisions: changing a tool call before it runs
- Reference: Deep Agents human-in-the-loop
- Change
Trueto{"allowed_decisions": ["approve", "reject"]}and printreview_configsfrom the interrupt. - Ask the agent to book two hotels in one request and count the
action_requests. - Reject without a
messageand read the default text the model gets.
Every expert started right here.