Interrupts
The interrupt system enables human-in-the-loop workflows by allowing users to pause agent execution and request human input before continuing. When an interrupt is raised, the agent stops its loop and returns control to the user. The user in turn provides a response to the agent. The agent then continues its execution starting from the point of interruption. Users can raise interrupts from either hook callbacks or tool definitions. The general flow looks as follows:
flowchart TD A[Invoke Agent] --> B[Execute Hook/Tool] B --> C{Interrupts Raised?} C -->|No| D[Continue Agent Loop] C -->|Yes| E[Stop Agent Loop] E --> F[Return Interrupts] F --> G[Respond to Interrupts] G --> H[Execute Hook/Tool with Responses] H --> I{New Interrupts?} I -->|Yes| E I -->|No| DUsers can raise interrupts within their hook callbacks to pause agent execution at specific life-cycle events in the agentic loop.
Currently, only the BeforeToolCallEvent is interruptible. Interrupting on a BeforeToolCallEvent allows users to intercept tool calls before execution to request human approval or additional inputs.
import jsonfrom typing import Any
from strands import Agent, toolfrom strands.hooks import BeforeToolCallEvent, HookProvider, HookRegistry
@tooldef delete_files(paths: list[str]) -> bool: # Implementation here pass
@tooldef inspect_files(paths: list[str]) -> dict[str, Any]: # Implementation here pass
class ApprovalHook(HookProvider): def __init__(self, app_name: str) -> None: self.app_name = app_name
def register_hooks(self, registry: HookRegistry, **kwargs: Any) -> None: registry.add_callback(BeforeToolCallEvent, self.approve)
def approve(self, event: BeforeToolCallEvent) -> None: if event.tool_use["name"] != "delete_files": return
approval = event.interrupt(f"{self.app_name}-approval", reason={"paths": event.tool_use["input"]["paths"]}) if approval.lower() != "y": event.cancel_tool = "User denied permission to delete files"
agent = Agent( hooks=[ApprovalHook("myapp")], system_prompt="You delete files older than 5 days", tools=[delete_files, inspect_files], callback_handler=None,)
paths = ["a/b/c.txt", "d/e/f.txt"]result = agent(f"paths=<{paths}>")
while True: if result.stop_reason != "interrupt": break
responses = [] for interrupt in result.interrupts: if interrupt.name == "myapp-approval": user_input = input(f"Do you want to delete {interrupt.reason['paths']} (y/N): ") responses.append({ "interruptResponse": { "interruptId": interrupt.id, "response": user_input } })
result = agent(responses)
print(f"MESSAGE: {json.dumps(result.message)}")Both BeforeToolCallEvent and BeforeToolsEvent are interruptible. Interrupting on a BeforeToolCallEvent allows users to intercept individual tool calls before execution, while BeforeToolsEvent allows intercepting the entire batch of tool calls before any execute.
BeforeToolCallEvent
Section titled “BeforeToolCallEvent”const deleteFiles = tool({ name: 'delete_files', description: 'Delete files at the given paths', inputSchema: z.object({ paths: z.array(z.string()) }), callback: (input) => { // Implementation here return true },})
const inspectFiles = tool({ name: 'inspect_files', description: 'Inspect files at the given paths', inputSchema: z.object({ paths: z.array(z.string()) }), callback: (input) => { // Implementation here return {} },})
const agent = new Agent({ systemPrompt: 'You delete files older than 5 days', tools: [deleteFiles, inspectFiles],})
agent.addHook(BeforeToolCallEvent, (event) => { if (event.toolUse.name !== 'delete_files') return
const approval = event.interrupt<string>({ name: 'myapp-approval', reason: { paths: (event.toolUse.input as { paths: string[] }).paths }, }) if (approval.toLowerCase() !== 'y') { event.cancel = 'User denied permission to delete files' }})
const paths = ['a/b/c.txt', 'd/e/f.txt']let result = await agent.invoke(`paths=<${JSON.stringify(paths)}>`)
while (result.stopReason === 'interrupt') { const responses = result.interrupts!.map((interrupt) => ({ interruptResponse: { interruptId: interrupt.id, // In a real app, collect user input here response: 'y', }, }))
result = await agent.invoke(responses)}
console.log('MESSAGE:', JSON.stringify(result.lastMessage))BeforeToolsEvent
Section titled “BeforeToolsEvent”const agent = new Agent({ tools: [/* ... */],})
agent.addHook(BeforeToolsEvent, (event) => { const dangerousTools = event.message.content .filter((block) => block.type === 'toolUseBlock') .filter((block) => ['delete_files'].includes(block.name))
if (dangerousTools.length > 0) { const response = event.interrupt<{ approved: boolean }>({ name: 'batch_approval', reason: `Approve ${dangerousTools.length} dangerous tool calls?`, }) if (!response.approved) { event.cancel = 'Batch cancelled by user' } }})Components
Section titled “Components”Interrupts in Strands are comprised of the following components:
event.interrupt- Raises an interrupt with a unique name and optional reason- The
namemust be unique across all interrupt calls configured on theBeforeToolCallEvent. In the example above, we demonstrate usingapp_nameto namespace the interrupt call. This is particularly helpful if you plan to vend your hooks to other users. - You can assign additional context for raising the interrupt to the
reasonfield. Note, thereasonmust be JSON-serializable.
- The
result.stop_reason- Check if agent stopped due to “interrupt”result.interrupts- List of interrupts that were raised- Each
interruptcontains the user provided name and reason, along with an instance id.
- Each
interruptResponse- Content block type for configuring the interrupt responses.- Each
responseis uniquely identified by their interrupt’s id and will be returned from the associated interrupt call when invoked the second time around. Note, theresponsemust be JSON-serializable.
- Each
event.cancel_tool- Cancel tool execution based on interrupt response- You can either set
cancel_tooltoTrueor provide a custom cancellation message.
- You can either set
For additional details on each of these components, refer to the Python API Reference.
BeforeToolCallEvent/BeforeToolsEvent— hook events that expose the ability to interrupt via theinterruptmethodevent.interrupt({ name, reason? })— halts the agent loop.nameis a string identifier andreasonis an optional JSON-serializable value providing context for why the interrupt was raised.- The
namemust be unique across all interrupt calls configured on the same event. In the example above, we demonstrate using a namespace prefix for the interrupt call. This is particularly helpful if you plan to vend your hooks to other users. event.cancel— cancel tool execution based on the interrupt response. Set totruefor a default message or provide a custom cancellation message string.
AgentResult— returned byinvoke()/stream(), contains interrupt information when the agent pausesresult.stopReason— check if agent stopped due to'interrupt'result.interrupts— array ofInterruptobjects, each containing the user-providednameandreason, along with a uniqueid
InterruptResponseContent— content block type for resuming from an interrupt- Pass an array of these to
agent.invoke()to resume. Each response is keyed by the interrupt’sidand will be returned from the associatedinterrupt()call when the tool/hook re-executes. Theresponsemust be JSON-serializable.
- Pass an array of these to
Strands enforces the following rules for interrupts:
- All hooks configured on the interrupted event will execute
- All hooks configured on the interrupted event are allowed to raise an interrupt
- A single hook can raise multiple interrupts but only one at a time
- In other words, within a single hook, you can interrupt, respond to that interrupt, and then proceed to interrupt again.
- All tools running concurrently are interruptible
- All tools running concurrently that are not interrupted will execute
- All hooks configured on the interrupted event will execute
- All hooks configured on the interrupted event are allowed to raise an interrupt
- A single hook can raise multiple interrupts but only one at a time
- In other words, within a single hook, you can interrupt, respond to that interrupt, and then proceed to interrupt again.
- When an interrupt fires from
BeforeToolCallEvent,AfterToolCallEventdoes not fire for that tool — butAfterToolsEventalways fires - When an interrupt fires mid-batch, completed tool results are preserved so the agent skips the model call on resume and only executes remaining tools
- Both assistant and tool result messages are appended only after tool execution completes, preventing dangling
toolUseblocks without matching results
Users can also raise interrupts from their tool definitions.
from typing import Any
from strands import Agent, toolfrom strands.types.tools import ToolContext
class DeleteTool: def __init__(self, app_name: str) -> None: self.app_name = app_name
@tool(context=True) def delete_files(self, tool_context: ToolContext, paths: list[str]) -> bool: approval = tool_context.interrupt(f"{self.app_name}-approval", reason={"paths": paths}) if approval.lower() != "y": return False
# Implementation here
return True
@tooldef inspect_files(paths: list[str]) -> dict[str, Any]: # Implementation here pass
agent = Agent( system_prompt="You delete files older than 5 days", tools=[DeleteTool("myapp").delete_files, inspect_files], callback_handler=None,)
...⚠️ Interrupts are not supported in direct tool calls (i.e., calls such as
agent.tool.my_tool()).
The tool callback receives a context parameter (the second argument) which provides the interrupt method.
const deleteFiles = tool({ name: 'delete_files', description: 'Delete files at the given paths', inputSchema: z.object({ paths: z.array(z.string()) }), callback: (input, context) => { const approval = context.interrupt<string>({ name: 'myapp-approval', reason: { paths: input.paths }, }) if (approval.toLowerCase() !== 'y') return false
// Implementation here
return true },})
const inspectFiles = tool({ name: 'inspect_files', description: 'Inspect files at the given paths', inputSchema: z.object({ paths: z.array(z.string()) }), callback: (input) => { // Implementation here return {} },})
const agent = new Agent({ systemPrompt: 'You delete files older than 5 days', tools: [deleteFiles, inspectFiles],})
// ...Components
Section titled “Components”Tool interrupts work similarly to hook interrupts with only a few notable differences. For more on tool context, see ToolContext.
tool_context- Strands object that defines the interrupt calltool_context.interrupt- Raises an interrupt with a unique name and optional reason- The
namemust be unique only among interrupt calls configured in the same tool definition. It is still advisable however to namespace your interrupts so as to more easily distinguish the calls when constructing responses outside the agent.
- The
ToolContext— the second argument passed to the tool callback, providing access to theinterruptmethodcontext.interrupt({ name, reason? })— halts the agent loop.nameis a string identifier andreasonis an optional JSON-serializable value.- The
namemust be unique only among interrupt calls configured in the same tool definition. It is still advisable however to namespace your interrupts so as to more easily distinguish the calls when constructing responses outside the agent.
Strands enforces the following rules for tool interrupts:
- All tools running concurrently will execute
- All tools running concurrently are interruptible
- A single tool can raise multiple interrupts but only one at a time
- In other words, within a single tool, you can interrupt, respond to that interrupt, and then proceed to interrupt again.
- A single tool can raise multiple interrupts but only one at a time
- In other words, within a single tool, you can interrupt, respond to that interrupt, and then proceed to interrupt again.
- When an interrupt fires mid-batch, completed tool results are preserved so the agent skips the model call on resume and only executes remaining tools
Session Management
Section titled “Session Management”Users can session manage their interrupts and respond back at a later time under a new agent session. Additionally, users can session manage the responses to avoid repeated interrupts on subsequent tool calls.
##### server.py #####
import jsonfrom typing import Any
from strands import Agent, toolfrom strands.agent import AgentResultfrom strands.hooks import BeforeToolCallEvent, HookProvider, HookRegistryfrom strands.session import FileSessionManagerfrom strands.types.agent import AgentInput
@tooldef delete_files(paths: list[str]) -> bool: # Implementation here pass
@tooldef inspect_files(paths: list[str]) -> dict[str, Any]: # Implementation here pass
class ApprovalHook(HookProvider): def __init__(self, app_name: str) -> None: self.app_name = app_name
def register_hooks(self, registry: HookRegistry, **kwargs: Any) -> None: registry.add_callback(BeforeToolCallEvent, self.approve)
def approve(self, event: BeforeToolCallEvent) -> None: if event.tool_use["name"] != "delete_files": return
if event.agent.state.get(f"{self.app_name}-approval") == "t": # (t)rust return
approval = event.interrupt(f"{self.app_name}-approval", reason={"paths": event.tool_use["input"]["paths"]}) if approval.lower() not in ["y", "t"]: event.cancel_tool = "User denied permission to delete files"
event.agent.state.set(f"{self.app_name}-approval", approval.lower())
def server(prompt: AgentInput) -> AgentResult: agent = Agent( hooks=[ApprovalHook("myapp")], session_manager=FileSessionManager(session_id="myapp", storage_dir="/path/to/storage"), system_prompt="You delete files older than 5 days", tools=[delete_files, inspect_files], callback_handler=None, ) return agent(prompt)
##### client.py #####
def client(paths: list[str]) -> AgentResult: result = server(f"paths=<{paths}>")
while True: if result.stop_reason != "interrupt": break
responses = [] for interrupt in result.interrupts: if interrupt.name == "myapp-approval": user_input = input(f"Do you want to delete {interrupt.reason['paths']} (t/y/N): ") responses.append({ "interruptResponse": { "interruptId": interrupt.id, "response": user_input } })
result = server(responses)
return result
paths = ["a/b/c.txt", "d/e/f.txt"]result = client(paths)print(f"MESSAGE: {json.dumps(result.message)}")const deleteFiles = tool({ name: 'delete_files', description: 'Delete files at the given paths', inputSchema: z.object({ paths: z.array(z.string()) }), callback: (input) => { // Implementation here return true },})
const inspectFiles = tool({ name: 'inspect_files', description: 'Inspect files at the given paths', inputSchema: z.object({ paths: z.array(z.string()) }), callback: (input) => { // Implementation here return {} },})
// Server function — creates a fresh agent with session management each callasync function server( prompt: string | { interruptResponse: { interruptId: string; response: unknown } }[]) { const agent = new Agent({ systemPrompt: 'You delete files older than 5 days', tools: [deleteFiles, inspectFiles], sessionManager: new SessionManager({ sessionId: 'myapp', storage: { snapshot: new FileStorage('/path/to/storage') }, }), })
agent.addHook(BeforeToolCallEvent, (event) => { if (event.toolUse.name !== 'delete_files') return
// Check if user already trusted this approval if (event.agent.appState.get('myapp-approval') === 't') return
const approval = event.interrupt<string>({ name: 'myapp-approval', reason: { paths: (event.toolUse.input as { paths: string[] }).paths }, }) if (!['y', 't'].includes(approval.toLowerCase())) { event.cancel = 'User denied permission to delete files' }
event.agent.appState.set('myapp-approval', approval.toLowerCase()) })
return agent.invoke(prompt)}
// Client functionasync function client(paths: string[]) { let result = await server(`paths=<${JSON.stringify(paths)}>`)
while (result.stopReason === 'interrupt') { const responses = result.interrupts!.map((interrupt) => ({ interruptResponse: { interruptId: interrupt.id, // In a real app, collect user input here response: 'y', }, }))
result = await server(responses) }
return result}
const paths = ['a/b/c.txt', 'd/e/f.txt']const result = await client(paths)console.log('MESSAGE:', JSON.stringify(result.lastMessage))Components
Section titled “Components”Session managing interrupts involves the following key components:
session_manager- Automatically persists the agent interrupt state between tear down and start up- For more information on session management in Strands, please refer to here.
agent.state- General purpose key-value store that can be used to persist interrupt responses- On subsequent tool calls, you can reference the responses stored in
agent.stateto decide whether another interrupt is necessary. For more information onagent.state, please refer to here.
- On subsequent tool calls, you can reference the responses stored in
sessionManager- Automatically persists the agent interrupt state between tear down and start up- For more information on session management in Strands, please refer to here.
agent.appState- General purpose key-value store that can be used to persist interrupt responses- On subsequent tool calls, you can reference the responses stored in
appStateto decide whether another interrupt is necessary. For more information onappState, please refer to here.
- On subsequent tool calls, you can reference the responses stored in
MCP Elicitation
Section titled “MCP Elicitation”Similar to interrupts, an MCP server can request additional information from the user by sending an elicitation request to the connecting client. Currently, elicitation requests are handled by conventional means of an elicitation callback. For more details, please refer to the docs here.
Multi-Agents
Section titled “Multi-Agents”Interrupts are supported in multi-agent patterns, enabling human-in-the-loop workflows across agent orchestration systems. The interfaces mirror those used for single-agent interrupts. You can raise interrupts from BeforeNodeCallEvent hooks executed before each node or from within the nodes themselves. Session management is also supported, allowing you to persist and resume your interrupted multi-agents.
A Swarm is a collaborative agent orchestration system where multiple agents work together as a team to solve complex tasks. The following example demonstrates interrupting your swarm invocation through a BeforeNodeCallEvent hook.
import json
from strands import Agentfrom strands.hooks import BeforeNodeCallEvent, HookProvider, HookRegistryfrom strands.multiagent import Swarm, Status
class ApprovalHook(HookProvider): def __init__(self, app_name: str) -> None: self.app_name = app_name
def register_hooks(self, registry: HookRegistry) -> None: registry.add_callback(BeforeNodeCallEvent, self.approve)
def approve(self, event: BeforeNodeCallEvent) -> None: if event.node_id != "cleanup": return
approval = event.interrupt(f"{self.app_name}-approval", reason={"resources": "example"}) if approval.lower() != "y": event.cancel_node = "User denied permission to cleanup resources"
swarm = Swarm( [ Agent(name="cleanup", system_prompt="You clean up resources older than 5 days.", callback_handler=None), ], hooks=[ApprovalHook("myapp")],)
result = swarm("Clean up my resources")while result.status == Status.INTERRUPTED: responses = [] for interrupt in result.interrupts: if interrupt.name == "myapp-approval": user_input = input(f"Do you want to cleanup {interrupt.reason['resources']} (y/N): ") responses.append({ "interruptResponse": { "interruptId": interrupt.id, "response": user_input, }, })
result = swarm(responses)
print(f"MESSAGE: {json.dumps(result.results['cleanup'].result.message, indent=2)}")// Multi-agent interrupts are not yet available in TypeScript SDKSwarms also support interrupts raised from within the nodes themselves following any of the single-agent interrupt patterns outlined above.
Components
Section titled “Components”event.interrupt- Raises an interrupt with a unique name and optional reason- The
namemust be unique across all interrupt calls configured on theBeforeNodeCallEvent. In the example above, we demonstrate usingapp_nameto namespace the interrupt call. This is particularly helpful if you plan to vend your hooks to other users. - You can assign additional context for raising the interrupt to the
reasonfield. Note, thereasonmust be JSON-serializable.
- The
result.status- Check if the swarm stopped due toStatus.INTERRUPTEDresult.interrupts- List of interrupts that were raised- Each
interruptcontains the user provided name and reason, along with an instance id.
- Each
interruptResponse- Content block type for configuring the interrupt responses.- Each
responseis uniquely identified by their interrupt’s id and will be returned from the associated interrupt call when invoked the second time around. Note, theresponsemust be JSON-serializable.
- Each
event.cancel_node- Cancel node execution based on interrupt response- You can either set
cancel_nodetoTrueor provide a custom cancellation message.
- You can either set
// Multi-agent interrupts are not yet available in TypeScript SDKStrands enforces the following rules for interrupts in swarm:
- All hooks configured on the interrupted event will execute
- All hooks configured on the interrupted event are allowed to raise an interrupt
- A single hook can raise multiple interrupts but only one at a time
- In other words, within a single hook, you can interrupt, respond to that interrupt, and then proceed to interrupt again.
- A single node can raise multiple interrupts following any of the single-agent interrupt patterns outlined above.
A Graph is a deterministic agent orchestration system based on a directed graph, where agents are nodes executed according to edge dependencies. The following example demonstrates interrupting your graph invocation through a BeforeNodeCallEvent hook.
import json
from strands import Agentfrom strands.hooks import BeforeNodeCallEvent, HookProvider, HookRegistryfrom strands.multiagent import GraphBuilder, Status
class ApprovalHook(HookProvider): def __init__(self, app_name: str) -> None: self.app_name = app_name
def register_hooks(self, registry: HookRegistry) -> None: registry.add_callback(BeforeNodeCallEvent, self.approve)
def approve(self, event: BeforeNodeCallEvent) -> None: if event.node_id != "cleanup": return
approval = event.interrupt(f"{self.app_name}-approval", reason={"resources": "example"}) if approval.lower() != "y": event.cancel_node = "User denied permission to cleanup resources"
inspector_agent = Agent(name="inspector", system_prompt="You inspect resources.", callback_handler=None)cleanup_agent = Agent(name="cleanup", system_prompt="You clean up resources older than 5 days.", callback_handler=None)
builder = GraphBuilder()builder.add_node(inspector_agent, "inspector")builder.add_node(cleanup_agent, "cleanup")builder.add_edge("inspector", "cleanup")builder.set_entry_point("inspector")builder.set_hook_providers([ApprovalHook("myapp")])graph = builder.build()
result = graph("Inspect and clean up my resources")while result.status == Status.INTERRUPTED: responses = [] for interrupt in result.interrupts: if interrupt.name == "myapp-approval": user_input = input(f"Do you want to cleanup {interrupt.reason['resources']} (y/N): ") responses.append({ "interruptResponse": { "interruptId": interrupt.id, "response": user_input, }, })
result = graph(responses)
print(f"MESSAGE: {json.dumps(result.results['cleanup'].result.message, indent=2)}")// Multi-agent interrupts are not yet available in TypeScript SDKGraphs also support interrupts raised from within the nodes themselves following any of the single-agent interrupt patterns outlined above.
Components
Section titled “Components”event.interrupt- Raises an interrupt with a unique name and optional reason- The
namemust be unique across all interrupt calls configured on theBeforeNodeCallEvent. In the example above, we demonstrate usingapp_nameto namespace the interrupt call. This is particularly helpful if you plan to vend your hooks to other users. - You can assign additional context for raising the interrupt to the
reasonfield. Note, thereasonmust be JSON-serializable.
- The
result.status- Check if the graph stopped due toStatus.INTERRUPTEDresult.interrupts- List of interrupts that were raised- Each
interruptcontains the user provided name and reason, along with an instance id.
- Each
interruptResponse- Content block type for configuring the interrupt responses- Each
responseis uniquely identified by their interrupt’s id and will be returned from the associated interrupt call when invoked the second time around. Note, theresponsemust be JSON-serializable.
- Each
event.cancel_node- Cancel node execution based on interrupt response- You can either set
cancel_nodetoTrueor provide a custom cancellation message.
- You can either set
// Multi-agent interrupts are not yet available in TypeScript SDKStrands enforces the following rules for interrupts in graph:
- All hooks configured on the interrupted event will execute
- All hooks configured on the interrupted event are allowed to raise an interrupt
- A single hook can raise multiple interrupts but only one at a time
- In other words, within a single hook, you can interrupt, respond to that interrupt, and then proceed to interrupt again.
- A single node can raise multiple interrupts following any of the single-agent interrupt patterns outlined above
- All nodes running concurrently will execute
- All nodes running concurrently are interruptible