Interrupts
Some work cannot finish in a single pass. An action might need a human to approve a destructive change, wait for an out-of-band signal, or defer until a condition holds. Rather than give each runtime its own way to handle this, the platform has one mechanism: an action pauses itself by throwing an interrupt, and whichever consumer is running catches it and turns it into something it can resume. Because the mechanism is defined in the core, every layer understands the same thing.
The core contract
Section titled “The core contract”An action signals a pause by throwing ActionInterrupt:
import { ActionInterrupt } from '@grundlag/core';
throw new ActionInterrupt(message, data?, state?);message— a human-readable description of why the action stopped.data— a caller-facing payload describing what must be satisfied before resuming (for example, the question to put to a human).state— the tool’s private checkpoint. It is opaque to everyone else, persisted with the interrupt, and handed back to the action verbatim when it resumes.
The action is written to be re-entrant. On its first call it has no resumption, so it
interrupts; when resumed it receives a resumption and continues from there:
execute: async ({ input, resumption }) => { if (!resumption) { throw new ActionInterrupt('Approve sending the invoice?', { invoiceId: input.id }); } // resumed: resumption.data is the caller's answer, // resumption.state is the checkpoint this call captured earlier return send(input.id);};Because ActionInterrupt lives in the core alongside the action contract, the action does
not know or care which consumer is running it. It just throws.
Each consumer catches it its own way
Section titled “Each consumer catches it its own way”The interrupt propagates up out of execute and is caught by whatever is driving the
action. Each consumer renders it into a resumable form that fits its runtime:
- The agent stops its run loop, records the interrupt on the suspended tool call, and
returns
{ status: 'interrupted' }. You continue withresume(data)— which re-runs the action with your answer — orreject(error)— which records a failure the model sees. See Handle interrupts. - The sandbox stops the script, re-surfaces the pause as a
SandboxInterruptcarrying the accumulated call log, and lets you resume by replaying the log with the answer supplied at the interrupted call’s position. See Wrap a script as an agent tool. - The HTTP API surfaces the interrupt to its caller as the response, so the calling application can gather the answer and call back.
The action wrote the pause once; each runtime knows how to hold it and hand back an answer.
Resumption
Section titled “Resumption”However the pause was caught, resuming reconstructs the resumption the action expects:
type ActionResumption = { data: unknown; state?: unknown };data is the caller’s answer, and state is the checkpoint the action stashed when it
interrupted. The action re-runs — the same execute, from the top — but this time
context.resumption is populated, so its guard clause falls through and it proceeds. An
action can interrupt more than once (each pause captures its own state), and the
resolution of each is kept, so the transcript records the whole negotiation.
Why it is the human-in-the-loop backbone
Section titled “Why it is the human-in-the-loop backbone”Approval flows, confirmations, and “ask the user first” steps all reduce to the same shape:
an action stops, someone decides, the action continues. Because that shape is a single core
primitive rather than a feature bolted onto each runtime, a provider author writes the
pause once and it works everywhere the action runs — inside an agent conversation, inside a
sandboxed script, or behind an HTTP endpoint. And because the pause is persisted (on the
agent’s Prompt, in the sandbox’s call log), a run can be suspended across a restart and
resumed later from reconstructed state.
Where to go next
Section titled “Where to go next”- Human-in-the-loop with interrupts — write an action that interrupts and consumes its resumption.
- Handle interrupts — resume or reject a suspended agent run.
- Wrap a script as an agent tool — how the sandbox captures and replays an interrupt.
- The agent run loop — where interrupts fit in the run.