Learn how to configure human approval for sensitive tool operations
Some tool operations may be sensitive and require human approval before execution. Deep Agents support human-in-the-loop workflows through LangGraph’s interrupt capabilities. You can configure which tools require approval using the interrupt_on parameter.
The allowed_decisions list controls what actions a human can take when reviewing a tool call:
"approve": Execute the tool with the original arguments as proposed by the agent
"edit": Modify the tool arguments before execution
"reject": Skip executing this tool call entirely and return rejection feedback to the agent
"respond": Return the human’s message directly as the tool result, skipping execution, for “ask user” style tools
Use reject when the human denies a proposed action. Use respond only when the human is acting as the tool, such as answering an ask_user prompt. Do not use respond to deny side-effecting tools, because its message may be treated by the model as a successful tool result.You can customize which decisions are available for each tool:
interrupt_on = { # Sensitive operations: allow all options "delete_file": {"allowed_decisions": ["approve", "edit", "reject"]}, # Moderate risk: approval or rejection only "write_file": {"allowed_decisions": ["approve", "reject"]}, # Must approve (no rejection allowed) "critical_operation": {"allowed_decisions": ["approve"]},}
When an interrupt is triggered, the agent pauses execution and returns control. Check for interrupts in the result and handle them accordingly. If the user rejects an action, include a clear message that tells the agent the tool was not executed and what to do next.
from langchain_core.utils.uuid import uuid7from langgraph.types import Command# Create config with thread_id for state persistenceconfig = {"configurable": {"thread_id": str(uuid7())}}# Invoke the agentresult = agent.invoke( {"messages": [{"role": "user", "content": "Delete the file temp.txt"}]}, config=config, version="v2", )# Check if execution was interruptedif result.interrupts: # Extract interrupt information interrupt_value = result.interrupts[0].value action_requests = interrupt_value["action_requests"] review_configs = interrupt_value["review_configs"] # Create a lookup map from tool name to review config config_map = {cfg["action_name"]: cfg for cfg in review_configs} # Display the pending actions to the user for action in action_requests: review_config = config_map[action["name"]] print(f"Tool: {action['name']}") print(f"Arguments: {action['args']}") print(f"Allowed decisions: {review_config['allowed_decisions']}") # Get user decisions (one per action_request, in order) decisions = [ { "type": "reject", "message": "User rejected deleting temp.txt. Do not retry deletion.", } ] # Resume execution with decisions result = agent.invoke( Command(resume={"decisions": decisions}), config=config, # Must use the same config! version="v2", )# Process final resultprint(result.value["messages"][-1].content)
When the agent calls multiple tools that require approval, all interrupts are batched together in a single interrupt. You must provide decisions for each one in order.
config = {"configurable": {"thread_id": str(uuid7())}}result = agent.invoke( {"messages": [{ "role": "user", "content": "Delete temp.txt and send an email to admin@example.com" }]}, config=config, version="v2", )if result.interrupts: interrupt_value = result.interrupts[0].value action_requests = interrupt_value["action_requests"] # Two tools need approval assert len(action_requests) == 2 # Provide decisions in the same order as action_requests decisions = [ {"type": "approve"}, # First tool: delete_file { "type": "reject", "message": "User rejected this action. Do not retry this tool call.", } # Second tool: send_email ] result = agent.invoke( Command(resume={"decisions": decisions}), config=config, version="v2", )
When a reviewer returns a reject decision, Deep Agents skip the tool call and send rejection feedback back to the agent. If you omit message, the default feedback tells the model that the tool was not executed and not to retry the same tool call unless the user asks.For sensitive or side-effecting tools, pass a domain-specific message with the decision. Be explicit about whether the agent should abandon the action, ask a follow-up question, or try a safer alternative.
decisions = [ { "type": "reject", "message": "User rejected deleting this file. Do not retry deletion. Ask which file to archive instead.", }]
When "edit" is in the allowed decisions, you can modify the tool arguments before execution:
if result.interrupts: interrupt_value = result.interrupts[0].value action_request = interrupt_value["action_requests"][0] # Original args from the agent print(action_request["args"]) # {"to": "everyone@company.com", ...} # User decides to edit the recipient decisions = [{ "type": "edit", "edited_action": { "name": action_request["name"], # Must include the tool name "args": {"to": "team@company.com", "subject": "...", "body": "..."} } }] result = agent.invoke( Command(resume={"decisions": decisions}), config=config, version="v2", )
Subagent tools can call interrupt() directly to pause execution and await approval:
from langchain.agents import create_agentfrom langchain_anthropic import ChatAnthropicfrom langchain.messages import HumanMessagefrom langchain.tools import toolfrom langgraph.checkpoint.memory import InMemorySaverfrom langgraph.types import Command, interruptfrom deepagents.graph import create_deep_agentfrom deepagents.middleware.subagents import CompiledSubAgent@tool(description="Request human approval before proceeding with an action.")def request_approval(action_description: str) -> str: """Request human approval using the interrupt() primitive.""" # interrupt() pauses execution and returns the value passed to Command(resume=...) approval = interrupt({ "type": "approval_request", "action": action_description, "message": f"Please approve or reject: {action_description}", }) if approval.get("approved"): return f"Action '{action_description}' was APPROVED. Proceeding..." else: return f"Action '{action_description}' was REJECTED. Reason: {approval.get('reason', 'No reason provided')}"def main(): checkpointer = InMemorySaver() model = ChatAnthropic( model_name="claude-sonnet-4-6", max_tokens=4096, ) compiled_subagent = create_agent( model=model, tools=[request_approval], name="approval-agent", ) parent_agent = create_deep_agent( model="google_genai:gemini-3.5-flash", checkpointer=checkpointer, subagents=[ CompiledSubAgent( name="approval-agent", description="An agent that can request approvals", runnable=compiled_subagent, ) ], ) thread_id = "test_interrupt_directly" config = {"configurable": {"thread_id": thread_id}} print("Invoking agent - sub-agent will use request_approval tool...") result = parent_agent.invoke( { "messages": [ HumanMessage( content="Use the task tool to launch the approval-agent sub-agent. " "Tell it to use the request_approval tool to request approval for 'deploying to production'." ) ] }, config=config, version="v2", ) # Check for interrupt if result.interrupts: interrupt_value = result.interrupts[0].value print(f"\nInterrupt received!") print(f" Type: {interrupt_value.get('type')}") print(f" Action: {interrupt_value.get('action')}") print(f" Message: {interrupt_value.get('message')}") print("\nResuming with Command(resume={'approved': True})...") result2 = parent_agent.invoke( Command(resume={"approved": True}), config=config, version="v2", ) if not result2.interrupts: print("\nExecution completed!") # Find the tool response tool_msgs = [m for m in result2.value.get("messages", []) if m.type == "tool"] if tool_msgs: print(f" Tool result: {tool_msgs[-1].content}") else: print("\nAnother interrupt occurred") else: print("\n No interrupt - the model may not have called request_approval")if __name__ == "__main__": main()
When run, this produces the following output:
Invoking agent - sub-agent will use request_approval tool...Interrupt received! Type: approval_request Action: deploying to production Message: Please approve or reject: deploying to productionResuming with Command(resume={'approved': True})...Execution completed! Tool result: Great! The approval request has been processed. The action **"deploying to production"** was **APPROVED**. You can now proceed with the production deployment.
Beyond interrupt_on, you can pause the built-in filesystem tools by marking a permission rule with mode="interrupt". When the agent calls write_file or edit_file on a path that matches an interrupt-mode rule, create_deep_agent raises the same human-in-the-loop interrupt as a configured tool, using the filesystem tool’s name as the action name.
from deepagents import FilesystemPermission, create_deep_agentfrom langgraph.checkpoint.memory import MemorySaveragent = create_deep_agent( model=model, permissions=[ FilesystemPermission( operations=["write"], paths=["/secrets/**"], mode="interrupt", ), ], checkpointer=MemorySaver(), # Required to pause and resume)
Handle and resume the interrupt the same way as a tool-call interrupt: run until it pauses, inspect the request, then resume with a decision.
from langgraph.types import Commandconfig = {"configurable": {"thread_id": "fs-thread-1"}}result = agent.invoke( {"messages": [{"role": "user", "content": "Save the API key to /secrets/key.txt"}]}, config=config, version="v2",)if result.interrupts: action = result.interrupts[0].value["action_requests"][0] print(f"Approve {action['name']} on {action['args']}?") # Resume with the human decision (approve, edit, or reject). result = agent.invoke( Command(resume={"decisions": [{"type": "approve"}]}), config=config, # Same thread ID version="v2", )
Filesystem-permission interrupts merge with any interrupt_on you pass, so a single review step can cover both custom tools and protected filesystem paths.
The decisions list must match the order of action_requests:
if result.interrupts: interrupt_value = result.interrupts[0].value action_requests = interrupt_value["action_requests"] # Create one decision per action, in order decisions = [] for action in action_requests: decision = get_user_decision(action) # Your logic decisions.append(decision) result = agent.invoke( Command(resume={"decisions": decisions}), config=config, version="v2", )