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