showcase/shell-docs/src/content/reference/hooks/useInterrupt.mdx
useInterrupt handles agent interrupts and resumes execution with user input. It supports the AG-UI standard interrupt flow — RUN_FINISHED with outcome.type === "interrupt" carrying an interrupts array — and the legacy custom-event flow (on_interrupt). For standard interrupts, your render/handler receive interrupt (the primary one) and interrupts (the full open set); call resolve(payload) to resume or cancel() to cancel.
By default, interrupt UI is rendered inside <CopilotChat> automatically. If you set renderInChat: false, the hook returns the element so you can place it manually.
event.value is typed as any since the interrupt payload shape depends on your agent. Type-narrow it in your callbacks (e.g. handler, enabled, render) as needed.
import { useInterrupt } from "@copilotkit/react-core/v2";
function useInterrupt<
TResult = never,
TRenderInChat extends boolean | undefined = undefined,
>(
config: UseInterruptConfig<any, TResult, TRenderInChat>,
): TRenderInChat extends false
? React.ReactElement | null
: TRenderInChat extends true | undefined
? void
: React.ReactElement | null | void;
<PropertyReference name="handler" type="(props: InterruptHandlerProps) => TResult | PromiseLike<TResult>"
Optional preprocessing callback. Runs before rendering and can return sync or
async data that is exposed as result in render. TResult is automatically
inferred from the handler's return type. If the handler throws/rejects,
result is null.
</PropertyReference>
function ApprovalInterrupt() {
useInterrupt({
render: ({ event, resolve }) => (
<div className="p-3 border rounded">
<p>{event.value.question}</p>
<div className="mt-2 flex gap-2">
<button onClick={() => resolve({ approved: true })}>Approve</button>
<button onClick={() => resolve({ approved: false })}>Reject</button>
</div>
</div>
),
});
return null;
}
function SidePanelInterrupt() {
const element = useInterrupt({
renderInChat: false,
enabled: (event) => event.value.startsWith("approval:"),
handler: async ({ event }) => ({ label: event.value.toUpperCase() }),
render: ({ event, result, resolve }) => (
<aside className="rounded border p-3">
<div className="font-medium">{result?.label ?? ""}</div>
<div className="mt-2">{event.value}</div>
<button className="mt-2" onClick={() => resolve({ accepted: true })}>
Continue
</button>
</aside>
),
});
return <>{element}</>;
}
function ApprovalInterrupt() {
useInterrupt({
render: ({ interrupt, resolve, cancel }) => (
<div className="p-3 border rounded">
<p>{interrupt?.message ?? "Approve this action?"}</p>
<div className="mt-2 flex gap-2">
<button onClick={() => resolve({ approved: true })}>Approve</button>
<button onClick={() => cancel()}>Cancel</button>
</div>
</div>
),
});
return null;
}
RUN_FINISHED (outcome.type === "interrupt"); legacy interrupts from on_interrupt custom events. Standard takes precedence if both appear in the same run.resolve/cancel accumulate one response per open interrupt; the resume run starts once every open interrupt is addressed.expiresAt) are not resumed — the hook logs an error and clears pending state.event.value is any -- type-narrow in your callbacks as needed.render.result is inferred from handler return type and is always TResult | null.handler throws or rejects, result is set to null.useHumanInTheLoop -- structured interactive tool workflowsuseFrontendTool -- client-side tool registrationuseAgent -- access and subscribe to agent events