Skip to content

Handle interrupts

An action can pause a run by throwing ActionInterrupt — to ask a human to approve a destructive step, say. When that happens the run does not fail: it suspends and hands control back to you. This guide shows how to detect a suspended run, inspect what it is waiting on, and continue it.

For writing the action side of this — throwing the interrupt and consuming its resumption — see Human-in-the-loop with interrupts. For the model behind it, see Interrupts.

When an action throws ActionInterrupt, run() resolves with status: 'interrupted' rather than throwing, and the run emits an interrupted event carrying the interrupt’s message and data:

run.on('interrupted', ({ message, data }) => {
console.log('run paused:', message, data);
});
const result = await run.run();
if (result.status === 'interrupted') {
// the run is waiting for you
}

The data is the payload the action attached to describe what must be satisfied before resuming — for example a question to put to a human.

run.state tells you exactly where the run stands. It is derived from the prompt itself, so it reports the same thing even on a run reconstructed from a persisted prompt after a restart:

const state = run.state;
if (state.status === 'interrupted') {
console.log('suspended tool:', state.toolCall.name);
console.log('waiting on:', state.interrupt.message, state.interrupt.data);
}

When suspended, state is { status: 'interrupted', toolCall, interrupt }:

  • toolCall — the 'tool' output that suspended, including its name and input.
  • interrupt — the pending interrupt: its message, data, and the tool’s private state checkpoint.

Otherwise state is { status: 'ready' }. Calling run() while suspended throws — you must answer the interrupt first.

Answer the pending interrupt with one of two calls. Both continue the loop and return an AgentRunResult, so the run may complete, or suspend again on the next interrupt.

resume(data) re-runs the suspended action, handing your data back to it as context.resumption.data (alongside the tool’s own state checkpoint). Use it once the condition the action was waiting on is met:

const result = await run.resume({ approved: true });
if (result.status === 'completed') {
console.log('finished', result.prompt.output);
}

The action typically checks resumption to decide whether it may now proceed:

execute: async ({ input, resumption }) => {
if (!resumption) {
throw new ActionInterrupt('Approve deleting the record?', { recordId: input.id });
}
// resumed — the human answered
return deleteRecord(input.id);
};

reject(error) declines the interrupt. The action is not re-run; instead the error you pass is recorded as the tool call’s failure and fed back to the model, which continues with the knowledge that the tool was refused:

const result = await run.reject({
name: 'Denied',
message: 'The user declined the deletion.',
});

The model sees the error as the tool’s result and can respond accordingly — for example by explaining to the user that it could not proceed.

Both decisions are recorded on the tool call’s interrupt history before anything else happens, so the transcript preserves the full story: the interrupt, its resolution, and the eventual result. If an action interrupts more than once, each entry keeps its own message, checkpoint state, and resolution — and each resume carries the checkpoint of the interrupt it answered. Because all of this lives on the Prompt, you can persist a suspended run and resume it later from a fresh AgentRun over the same prompts. See The agent run loop.