showcase/shell-docs/src/content/docs/human-in-the-loop/useInterrupt.mdx
useInterrupt lets your agent pause mid-run, hand control to the user
through a custom React component, and resume with whatever the user
returns. How that pause is implemented depends on the framework's
runtime.
LangGraph ships a first-class
interrupt()
primitive that lets a running node suspend itself and hand control to
the client. The run is frozen server-side until the client resolves the
interrupt with a payload, at which point the node resumes as if
interrupt() had simply returned that payload.
CopilotKit's useInterrupt is the frontend half of that contract: it
subscribes to the paused run, renders whatever component you give it,
and calls the agent back with the user's answer.
The Microsoft Agent Framework runtime can't pause a run mid-tool the
way LangGraph's interrupt() does, so this demo uses useFrontendTool
with a Promise-based handler instead. The agent calls schedule_meeting
like any other tool; the client-side handler renders the picker, holds
the request open, and only resolves the Promise once the user picks a
slot or cancels. Same UX from the reader's perspective — agent pauses,
user answers, agent resumes — different mechanism underneath.
</WhenFrameworkHas> <InlineDemo demo="gen-ui-interrupt" />Not available on this framework.
useInterruptis only meaningful when the underlying runtime exposes either a nativeinterrupt(...)primitive (LangGraph) or a Promise-resolving frontend tool path (Microsoft Agent Framework). For all other integrations, useuseHumanInTheLoopinstead — it's the standard hook for tool-call-based pause/resume flows and works on every framework that supports tool calls.
Reach for useInterrupt when the pause is a graph-enforced
checkpoint where the code path must stop and wait for a human,
not an LLM-initiated tool call. Typical cases:
interrupt(...) and resume with a payloadFor LLM-initiated pauses where the model decides on the fly to ask
the user, prefer useHumanInTheLoop.
interrupt() inside a toolThe example agent exposes a schedule_meeting tool. When the model
calls it, the tool issues a langgraph.interrupt(...)
with the meeting context. The run freezes here until the client
resolves; the resolution becomes the return value of interrupt(),
which the tool then turns into a final string for the model:
Two things to note:
{"topic": topic, "attendee": attendee}) is what the
frontend receives as event.value. Keep it a plain, serializable
object. It's the "pause-time context" the UI needs to render.{chosen_label, chosen_time} or
{cancelled: true}) is entirely yours. The client can send anything
as the resolve payload; the tool is the one that gives it meaning.useInterrupt render propOn the client you register a useInterrupt hook per agent. When the
paused run arrives, its payload is handed to render as event.value,
and resolve(...) is how you resume the run:
Whatever you pass to resolve is round-tripped back to the agent as
the return value of the matching interrupt(...) call.
useFrontendTool with a Promise-resolving handlerThe handler stores its resolve callback in a ref, returns a Promise
that the user's pick eventually resolves, and renders the picker
inline in the chat. This is the MS Agent equivalent of
useInterrupt's event / resolve pair:
The agent has no local schedule_meeting implementation — the tool is
registered entirely on the frontend. The backend's only job is to
instruct the model to call schedule_meeting whenever the user wants
to book a meeting. AG-UI routes the tool call to the client, where
the Promise-returning handler takes over:
agentId — must match a runtime-registered agent. If omitted, the
hook assumes "default". A mismatch means the interrupt never fires.render — receives { event, resolve }. event.value is the
payload you passed to interrupt(...) on the server.renderInChat — when true (as above), the picker appears inline
in the chat transcript, between the paused assistant turn and the
still-pending continuation.enabledIf your graph issues more than one kind of interrupt (e.g. "ask" vs
"approval"), tag each with a type field on the payload and install
one useInterrupt per shape, each gated by an enabled predicate:
useInterrupt({
agentId: "gen-ui-interrupt",
enabled: ({ eventValue }) => eventValue.type === "ask",
render: ({ event, resolve }) => (
<AskCard question={event.value.content} onAnswer={resolve} />
),
});
useInterrupt({
agentId: "gen-ui-interrupt",
enabled: ({ eventValue }) => eventValue.type === "approval",
render: ({ event, resolve }) => (
<ApproveCard content={event.value.content} onAnswer={resolve} />
),
});
handlerFor cases where the interrupt can sometimes be resolved without user
input (e.g. the current user already has permission), pass a handler
that runs before render. The handler can call resolve(...) itself
to short-circuit the UI, or return a value that render receives as
result:
useInterrupt({
agentId: "gen-ui-interrupt",
handler: async ({ event, resolve }) => {
const dept = await lookupUserDepartment();
if (event.value.accessDepartment === dept || dept === "admin") {
resolve({ code: "AUTH_BY_DEPARTMENT" });
return; // skip render
}
return { dept };
},
render: ({ result, event, resolve }) => (
<RequestAccessCard
dept={result.dept}
onRequest={() => resolve({ code: "REQUEST_AUTH" })}
onCancel={() => resolve({ code: "CANCEL" })}
/>
),
});
useHumanInTheLoop — for
LLM-initiated pauses.useAgent, agent.subscribe, copilotkit.runAgent) to resolve
interrupts outside a chat surface.