CopilotKit

Interactive components

Create approval flows where the agent pauses and waits for human input.


"""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?#

Interactive generative UI creates flows where the agent pauses execution and waits for user input before continuing. This enables approval workflows, confirmation dialogs, and any scenario where human judgment is needed mid-execution.

When should I use this?#

Use interactive generative UI when you need:

  • Approval/rejection flows (e.g. "Run this command?")
  • User decisions that the agent should know about
  • Confirmation dialogs with structured responses
  • Any flow where the agent pauses for human judgment

How it works in code#

On the frontend, register an interrupt renderer with useInterrupt. When the agent pauses, your component mounts inline in the chat, captures the user's choice, and resumes the run with that input.

page.tsx
import React, { useRef } from "react";import { CopilotKit } from "@copilotkit/react-core";import {  CopilotChat,  useConfigureSuggestions,  useFrontendTool,} from "@copilotkit/react-core/v2";import { z } from "zod";import { TimePickerCard, TimeSlot } from "./time-picker-card";const DEFAULT_SLOTS: TimeSlot[] = [  { label: "Tomorrow 10:00 AM", iso: "2026-04-25T10:00:00-07:00" },  { label: "Tomorrow 2:00 PM", iso: "2026-04-25T14:00:00-07:00" },  { label: "Monday 9:00 AM", iso: "2026-04-28T09:00:00-07:00" },  { label: "Monday 3:30 PM", iso: "2026-04-28T15:30:00-07:00" },];type PickerResult =  | { chosen_time: string; chosen_label: string }  | { cancelled: true };export default function GenUiInterruptDemo() {  return (    <CopilotKit runtimeUrl="/api/copilotkit" agent="gen-ui-interrupt">      <div className="flex justify-center items-center h-screen w-full">        <div className="h-full w-full max-w-4xl">          <Chat />        </div>      </div>    </CopilotKit>  );}function Chat() {  // Pending-resolver ref: set by the async handler, called by the render  // prop when the user clicks a slot or cancels. This is the CrewAI  // adaptation of the LangGraph `resolve(...)` callback.  const resolverRef = useRef<((result: PickerResult) => void) | null>(null);  useConfigureSuggestions({    suggestions: [      {        title: "Book a call with sales",        message: "Book an intro call with the sales team to discuss pricing.",      },      {        title: "Schedule a 1:1 with Alice",        message: "Schedule a 1:1 with Alice next week to review Q2 goals.",      },    ],    available: "always",  });  useFrontendTool({    name: "schedule_meeting",    description:      "Ask the user to pick a time slot for a meeting via an in-chat " +      "picker. Blocks until the user chooses a slot or cancels.",    parameters: z.object({      topic: z        .string()        .describe("Short human-readable description of the meeting."),      attendee: z        .string()        .optional()        .describe("Who the meeting is with (optional)."),    }),    // Async handler: returns a Promise that resolves only once the user    // acts on the picker. This is the CrewAI Crews shim for    // LangGraph's `interrupt()`/`resolve()` pair.    handler: async (): Promise<string> => {      const result = await new Promise<PickerResult>((resolve) => {        resolverRef.current = resolve;      });      if ("cancelled" in result && result.cancelled) {        return "User cancelled. Meeting NOT scheduled.";      }      if ("chosen_label" in result) {        return `Meeting scheduled for ${result.chosen_label}.`;      }      return "User did not pick a time. Meeting NOT scheduled.";    },    render: ({ args, status }) => {      if (status === "complete") return null;      const topic =        (args as { topic?: string } | undefined)?.topic ?? "a meeting";      const attendee = (args as { attendee?: string } | undefined)?.attendee;      return (        <TimePickerCard          topic={topic}          attendee={attendee}          slots={DEFAULT_SLOTS}          onSubmit={(result) => {            const fn = resolverRef.current;            resolverRef.current = null;            fn?.(result);          }}        />      );    },  });

On the backend, the agent calls into the interrupt primitive and waits for the resumed response before continuing the graph.

interrupt_crew.py
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