CopilotKit

Interactive components

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


"""LangGraph agent for the Interrupt-based Generative UI demo.Defines a backend tool `schedule_meeting(topic, attendee)` that useslanggraph's `interrupt()` primitive to pause the run and surface themeeting context to the frontend. The frontend `useInterrupt` renderershows a time picker and resolves with `{chosen_time, chosen_label}` or`{cancelled: true}`, which this tool turns into a human-readable result."""from __future__ import annotationsfrom typing import Any, Optionalfrom langchain.agents import create_agentfrom langchain_core.tools import toolfrom langchain_openai import ChatOpenAIfrom langgraph.types import interruptfrom copilotkit import CopilotKitMiddlewareSYSTEM_PROMPT = (    "You are a scheduling assistant. Whenever the user asks you to book a "    "call / schedule a meeting, you MUST call the `schedule_meeting` tool. "    "Pass a short `topic` describing the purpose and `attendee` describing "    "who the meeting is with. After the tool returns, confirm briefly "    "whether the meeting was scheduled and at what time, or that the user "    "cancelled.")@tooldef schedule_meeting(topic: str, attendee: Optional[str] = None) -> str:    """Ask the user to pick a time slot for a call, via an in-chat picker.    Args:        topic: Short human-readable description of the call's purpose.        attendee: Who the call is with (optional).    Returns:        Human-readable result string describing the chosen slot or        indicating the user cancelled.    """    # langgraph's `interrupt()` pauses execution and forwards the payload to    # the client. The frontend v2 `useInterrupt` hook renders the picker and    # calls `resolve(...)` with the user's selection, which comes back here.    response: Any = interrupt({"topic": topic, "attendee": attendee})    if isinstance(response, dict):        if response.get("cancelled"):            return f"User cancelled. Meeting NOT scheduled: {topic}"        chosen_label = response.get("chosen_label") or response.get("chosen_time")        if chosen_label:            return f"Meeting scheduled for {chosen_label}: {topic}"    return f"User did not pick a time. Meeting NOT scheduled: {topic}"model = ChatOpenAI(model="gpt-4o-mini")graph = create_agent(    model=model,    tools=[schedule_meeting],    middleware=[CopilotKitMiddleware()],    system_prompt=SYSTEM_PROMPT,)

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 from "react";import {  CopilotKit,  CopilotChat,  useInterrupt,  useConfigureSuggestions,} from "@copilotkit/react-core/v2";import { TimePickerCard, TimeSlot } from "./time-picker-card";const DEFAULT_SLOTS: TimeSlot[] = [  { label: "Tomorrow 10:00 AM", iso: "2026-04-19T10:00:00-07:00" },  { label: "Tomorrow 2:00 PM", iso: "2026-04-19T14:00:00-07:00" },  { label: "Monday 9:00 AM", iso: "2026-04-21T09:00:00-07:00" },  { label: "Monday 3:30 PM", iso: "2026-04-21T15:30:00-07:00" },];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() {  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",  });  useInterrupt({    agentId: "gen-ui-interrupt",    renderInChat: true,    render: ({ event, resolve }) => {      const payload = (event.value ?? {}) as {        topic?: string;        attendee?: string;      };      return (        <TimePickerCard          topic={payload.topic ?? "a call"}          attendee={payload.attendee}          slots={DEFAULT_SLOTS}          onSubmit={(result) => resolve(result)}        />      );    },  });

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

interrupt_agent.py
from __future__ import annotationsfrom typing import Any, Optionalfrom langchain.agents import create_agentfrom langchain_core.tools import toolfrom langchain_openai import ChatOpenAIfrom langgraph.types import interruptfrom copilotkit import CopilotKitMiddlewareSYSTEM_PROMPT = (    "You are a scheduling assistant. Whenever the user asks you to book a "    "call / schedule a meeting, you MUST call the `schedule_meeting` tool. "    "Pass a short `topic` describing the purpose and `attendee` describing "    "who the meeting is with. After the tool returns, confirm briefly "    "whether the meeting was scheduled and at what time, or that the user "    "cancelled.")@tooldef schedule_meeting(topic: str, attendee: Optional[str] = None) -> str:    """Ask the user to pick a time slot for a call, via an in-chat picker.    Args:        topic: Short human-readable description of the call's purpose.        attendee: Who the call is with (optional).    Returns:        Human-readable result string describing the chosen slot or        indicating the user cancelled.    """    # langgraph's `interrupt()` pauses execution and forwards the payload to    # the client. The frontend v2 `useInterrupt` hook renders the picker and    # calls `resolve(...)` with the user's selection, which comes back here.    response: Any = interrupt({"topic": topic, "attendee": attendee})    if isinstance(response, dict):        if response.get("cancelled"):            return f"User cancelled. Meeting NOT scheduled: {topic}"        chosen_label = response.get("chosen_label") or response.get("chosen_time")        if chosen_label:            return f"Meeting scheduled for {chosen_label}: {topic}"    return f"User did not pick a time. Meeting NOT scheduled: {topic}"