Interrupts in Multi-Agent Systems
Interrupts work across multi-agent orchestration, so you can pause a swarm or graph for human approval or input the same way you pause a single agent. Raise an interrupt from a BeforeNodeCallEvent hook that runs before each node, or from within the nodes themselves. Session management works here too, so you can persist an interrupted multi-agent run and resume it later.
The interfaces mirror those used for single-agent interrupts. Read that page first if you have not built an interrupt/resume loop before.
Agents as Tools
Section titled “Agents as Tools”An agent used as a tool with Agents as Tools propagates its interrupts to the orchestrator, where they are raised and answered like any other interrupt. The interrupt ids are scoped to the orchestrator’s tool call; treat them as opaque.
A Swarm is a collaborative agent orchestration system where multiple agents work together as a team to solve complex tasks. The following example interrupts a 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)}")import { Agent, Swarm, Status, BeforeNodeCallEvent } from '@strands-agents/sdk'
const cleanupAgent = new Agent({ id: 'cleanup', systemPrompt: 'You clean up resources older than 5 days.',})
const swarm = new Swarm({ nodes: [cleanupAgent], start: 'cleanup' })
swarm.addHook(BeforeNodeCallEvent, (event) => { if (event.nodeId !== 'cleanup') return
const approval = event.interrupt<string>({ name: 'myapp-approval', reason: { resources: 'example' }, }) if (approval.toLowerCase() !== 'y') { event.cancel = 'User denied permission to cleanup resources' }})
let result = await swarm.invoke('Clean up my resources')
while (result.status === Status.INTERRUPTED) { const responses = result.interrupts!.map((interrupt) => ({ interruptResponse: { interruptId: interrupt.id, // In a real app, collect user input here response: 'y', }, }))
result = await swarm.invoke(responses)}
console.log('MESSAGE:', JSON.stringify(result.results, null, 2))Swarms also support interrupts raised from within the nodes themselves, following any of the single-agent interrupt patterns.
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
BeforeNodeCallEvent: orchestrator hook event that exposes the ability to interrupt before a node runsevent.interrupt({ name, reason? }): halts the orchestrator.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 node execution based on the interrupt response. Set totruefor a default message or provide a custom cancellation message string.
MultiAgentResult: returned byinvoke()/stream(), contains interrupt information when the orchestrator pausesresult.status: check if the swarm stopped due toStatus.INTERRUPTEDresult.interrupts: array ofInterruptobjects, each withname,reason, and a uniqueid. Each interrupt’ssourcefield is'multiagent-hook'when raised fromBeforeNodeCallEvent.
InterruptResponseContent: content block type for resuming from an interrupt- Pass an array of these to
swarm.invoke()to resume. The orchestrator routes each response to the node that raised the matching interrupt.
- Pass an array of these to
Strands 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 interrupts a 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)}")import { Agent, Graph, Status, BeforeNodeCallEvent } from '@strands-agents/sdk'
const inspectorAgent = new Agent({ id: 'inspector', systemPrompt: 'You inspect resources.',})const cleanupAgent = new Agent({ id: 'cleanup', systemPrompt: 'You clean up resources older than 5 days.',})
const graph = new Graph({ nodes: [inspectorAgent, cleanupAgent], edges: [['inspector', 'cleanup']],})
graph.addHook(BeforeNodeCallEvent, (event) => { if (event.nodeId !== 'cleanup') return
const approval = event.interrupt<string>({ name: 'myapp-approval', reason: { resources: 'example' }, }) if (approval.toLowerCase() !== 'y') { event.cancel = 'User denied permission to cleanup resources' }})
let result = await graph.invoke('Inspect and clean up my resources')
while (result.status === Status.INTERRUPTED) { const responses = result.interrupts!.map((interrupt) => ({ interruptResponse: { interruptId: interrupt.id, // In a real app, collect user input here response: 'y', }, }))
result = await graph.invoke(responses)}
console.log('MESSAGE:', JSON.stringify(result.results, null, 2))Graphs also support interrupts raised from within the nodes themselves, following any of the single-agent interrupt patterns.
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
BeforeNodeCallEvent: orchestrator hook event that exposes the ability to interrupt before a node runsevent.interrupt({ name, reason? }): halts the orchestrator.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 node execution based on the interrupt response. Set totruefor a default message or provide a custom cancellation message string.
MultiAgentResult: returned byinvoke()/stream(), contains interrupt information when the orchestrator pausesresult.status: check if the graph stopped due toStatus.INTERRUPTEDresult.interrupts: array ofInterruptobjects, each withname,reason, and a uniqueid. Each interrupt’ssourcefield is'multiagent-hook'when raised fromBeforeNodeCallEvent.
InterruptResponseContent: content block type for resuming from an interrupt- Pass an array of these to
graph.invoke()to resume. The orchestrator routes each response to the node that raised the matching interrupt; concurrent nodes already in flight run to completion.
- Pass an array of these to
Strands 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