CopilotKit

Headless Interrupts

Resolve agent interrupts from any UI, without a useInterrupt render slot.


"""CrewAI scheduling crew for the interrupt-adapted demos.Powers both gen-ui-interrupt and interrupt-headless. The LangGraph referenceuses `interrupt()` with checkpoint/resume; CrewAI has no equivalent primitive,so we adapt via Strategy B: the backend crew defines a system prompt thatinstructs the chat LLM to call `schedule_meeting`, and the frontend registersthat tool via `useFrontendTool` with an async handler that renders atime-picker and returns a Promise that only resolves when the user picks aslot (or cancels).No backend tools — `schedule_meeting` is satisfied entirely by the frontend."""from __future__ import annotationsfrom crewai import Agent, Crew, Process, Taskfrom agents._chat_flow_helpers import preseed_system_promptCREW_NAME = "InterruptSchedulingCrew"_SYSTEM_PROMPT = (    "You are a scheduling assistant. Whenever the user asks you to book a call "    "or schedule a meeting, you MUST call the `schedule_meeting` tool. Pass a "    "short `topic` describing the purpose of the meeting and, if known, an "    "`attendee` describing who the meeting is with.\n\n"    "The `schedule_meeting` tool is implemented on the client: it surfaces a "    "time-picker UI to the user and returns the user's selection. After the "    "tool returns, briefly confirm whether the meeting was scheduled and at "    "what time, or note that the user cancelled. Do NOT ask for approval "    "yourself — always call the tool and let the picker handle the decision.\n\n"    "Keep responses short and friendly. After you finish executing tools, "    "always send a brief final assistant message summarizing what happened so "    "the message persists.")preseed_system_prompt(CREW_NAME, _SYSTEM_PROMPT)def _build_crew() -> Crew:    agent = Agent(        role="Scheduling Assistant",        goal="Help users schedule meetings by calling the schedule_meeting tool",        backstory=(            "You are a concise scheduling assistant. You always call "            "schedule_meeting when asked to book or schedule anything."        ),        verbose=False,        tools=[],    )    task = Task(        description=(            "Help the user schedule a meeting by calling the schedule_meeting "            "frontend tool."        ),        expected_output="A confirmation of the scheduled meeting or cancellation.",        agent=agent,    )    return Crew(        name=CREW_NAME,        agents=[agent],        tasks=[task],        process=Process.sequential,        verbose=False,        chat_llm="gpt-4o",    )_cached_crew: Crew | None = Noneclass InterruptScheduling:    """Adapter matching the shape `add_crewai_crew_fastapi_endpoint` expects."""    name: str = CREW_NAME    def crew(self) -> Crew:        global _cached_crew        if _cached_crew is None:            _cached_crew = _build_crew()        return _cached_crew

What is this?#

useInterrupt's render callback is the 80% path: it keeps the UI glued to a <CopilotChat> transcript and handles "when to show the picker" logic for you. This page covers the escape hatch: a render-less interrupt resolver you assemble from the same primitives useInterrupt uses internally — a pattern that lives anywhere in your React tree, takes any shape you like (button grid, form, modal, keyboard shortcut), and resolves the interrupt without mounting a chat at all.

Not available on this framework. Headless interrupts are built on top of useInterrupt / useFrontendTool patterns that require the runtime to expose either a native interrupt(...) primitive (LangGraph) or a Promise-resolving frontend-tool path (Microsoft Agent Framework). For all other integrations, use useHumanInTheLoop instead — it's the standard hook for tool-call-based pause/resume flows and works on every framework that supports tool calls.

When should I use this?#

  • Testing / Playwright fixtures — a deterministic, chat-less button grid is easier to drive than a chat surface where the picker only appears after an LLM call.
  • Non-chat UIs — dashboards, side panels, inspector surfaces, or any place where you want the agent's interrupt without the chat transcript.
  • Custom flow control — when you need to know exactly when the interrupt arrived (e.g. to gate other UI) and when it was resolved.
  • Research / debugging — when you want to observe the raw AG-UI custom events without the abstraction layer.

If you just want "a picker in chat", just use useInterrupt.

Going further#

  • Tool-based HITL with useHumanInTheLoop — for LLM-initiated pauses where the model decides on the fly to ask the user, rather than the runtime forcing the pause itself.
  • useInterrupt — the render-prop version of this page, with enabled gating and handler preprocessing.