Deep AgentsDeep Agents 0.7 · Python 3.11+
0%
1
Curious builder0 XP earned · 300 to level 2
0 daysFinish a lesson to begin
Badge collection0 of 6 unlocked
28 small wins to finish your pathNext lesson →

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.

HumanInTheLoopMiddleware pauses before book_hotel; a person approves, edits or rejects the call.
Approve, edit or reject

The interrupt_on syntax

python
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.

python
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.

python
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)
python
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.

ExampleAPI keytrip.py, continued
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")

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 from action_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

DecisionWhat happensUse it when
approveThe tool runs as proposedThe call is right
editThe tool runs with arguments you changedThe call is almost right
rejectThe tool is skipped; your message goes back to the modelThe action should not happen
respondYour text becomes the tool's resultThe 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_file and edit_file when the backend is your disk.
Watch out. Resume with the same config, including the same thread_id. A new thread has no paused run, and the decision goes nowhere.
Try it yourself
  • Change True to {"allowed_decisions": ["approve", "reject"]} and print review_configs from the interrupt.
  • Ask the agent to book two hotels in one request and count the action_requests.
  • Reject without a message and read the default text the model gets.

Every expert started right here.