Advanced

Custom progress rendering, frontend action handlers, and advanced A2UI configuration.


Custom A2UI Progress Renderer#

When using Dynamic Schema A2UI, a secondary LLM generates the UI schema and data. This takes a few seconds — during which CopilotKit shows a built-in progress indicator.

You can replace the built-in indicator with your own component using useRenderTool.

How it works#

The dynamic schema flow calls a tool named render_a2ui under the hood. While the tool call is in progress (status === "inProgress"), your custom renderer is shown. Once the A2UI surface starts rendering (status === "complete"), your component is hidden and the actual surface takes over.

Implementation#

Create a progress component#

src/components/a2ui-progress.tsx
"use client";

import { memo } from "react";

interface A2UIProgressProps {
  parameters: Record<string, unknown>;
}

export const A2UIProgress = memo(function A2UIProgress({
  parameters,
}: A2UIProgressProps) {
  // You can inspect `parameters` to show partial progress.
  // As the LLM streams, `parameters.components` and `parameters.items`
  // will progressively populate.
  const componentCount = Array.isArray(parameters?.components)
    ? parameters.components.length
    : 0;
  const itemCount = Array.isArray(parameters?.items)
    ? parameters.items.length
    : 0;

  return (
    <div className="rounded-lg border border-gray-200 bg-gray-50 p-4">
      <div className="flex items-center gap-2 text-sm text-gray-600">
        <div className="h-4 w-4 animate-spin rounded-full border-2 border-gray-300 border-t-gray-600" />
        <span>Building interface...</span>
      </div>
      {componentCount > 0 && (
        <p className="mt-2 text-xs text-gray-500">
          {componentCount} components, {itemCount} items
        </p>
      )}
    </div>
  );
});

Register the renderer#

Use useRenderTool to intercept the render_a2ui tool call and show your component while it's in progress:

src/hooks/use-a2ui-progress.tsx
"use client";

import { useRenderTool } from "@copilotkit/react-core/v2";
import { z } from "zod";
import { A2UIProgress } from "@/components/a2ui-progress";

export function useA2UIProgress() {
  useRenderTool(
    {
      name: "render_a2ui",
      parameters: z.any(),
      render: ({ status, parameters }) => {
        // Hide when complete — the A2UI surface renderer takes over
        if (status === "complete") return <></>;
        return <A2UIProgress parameters={parameters ?? {}} />;
      },
    },
    [],
  );
}

Call the hook in your page#

src/app/page.tsx
"use client";

import { useA2UIProgress } from "@/hooks/use-a2ui-progress";

function Chat() {
  useA2UIProgress();

  return <CopilotChat className="flex-1" />;
}

The parameters object updates progressively as the LLM streams the render_a2ui tool call. You can use this to show a skeleton that fills in as components and data arrive.

What's in parameters?#

As the secondary LLM generates the A2UI surface, the parameters object accumulates:

FieldTypeDescription
surfaceIdstringUnique ID for this surface
componentsarrayThe component tree (schema) — arrives first
rootstringRoot component ID
itemsarrayData items — arrive after the schema

A common pattern is to show a skeleton layout once components has data, then show item count as items streams in.

Action Handlers#

Action handling in this section is client-side. The current Python SDK's a2ui.render does not support the action_handlers= keyword; the fixed-schema guide preserves the button schema and documents that server-side handlers are not supported. The current React API exposes createA2UIMessageRenderer with an onAction interceptor.

This section covers the current frontend interceptor and the v0.9 schema button format.

Schema button with action context#

The v0.9 A2UI schema defines buttons with an event and data-bound context fields. Components inside a repeating list use relative paths, so the context values resolve from the clicked item:

{
  "id": "book-button",
  "component": "Button",
  "child": "book-label",
  "variant": "primary",
  "action": {
    "event": {
      "name": "book_flight",
      "context": {
        "flightNumber": { "path": "flightNumber" },
        "price": { "path": "price" }
      }
    }
  }
}

The resulting A2UIUserAction will include the resolved context:

{
  name: "book_flight",
  surfaceId: "flight-search-results",
  sourceComponentId: "book-button",
  timestamp: "2025-01-01T00:00:00.000Z",
  context: { flightNumber: "AA100", price: "$350" },
}

onAction interceptor#

Pass onAction to the exported renderer factory to handle an action in the browser. Return null after handling the action locally; return undefined to forward it unchanged to the agent:

import {
  createA2UIMessageRenderer,
  type A2UIUserAction,
} from "@copilotkit/react-core/v2";
import { theme } from "./theme";

const A2UIMessageRenderer = createA2UIMessageRenderer({
  theme,
  onAction: (action: A2UIUserAction) => {
    if (action.name === "book_flight") {
      console.info("Booking requested", action.context);
      return null;
    }
    return undefined;
  },
});

Pass the returned renderer through the provider's renderActivityMessages prop when you need this custom onAction behavior. User-provided renderers take precedence over the provider's built-in A2UI renderer.

Forward a modified action#

An interceptor can return a modified exported A2UIUserAction; the renderer forwards that action to the agent:

const A2UIMessageRenderer = createA2UIMessageRenderer({
  theme,
  onAction: (action) => {
    if (action.name !== "book_flight") return;

    return {
      ...action,
      context: {
        ...(action.context ?? {}),
        source: "a2ui",
      },
    };
  },
});

Current action flow#

The current renderer applies onAction before forwarding an action:

  1. Returning null handles the action in the browser and skips the agent.
  2. Returning an A2UIUserAction forwards that modified action.
  3. Returning undefined forwards the original action.

The current Python SDK does not create server-side action handlers from an action_handlers= argument. The frontend interceptor is the supported custom-handling path in this documentation. The current React bridge does not include dataContextPath in the action passed to onAction; use context for values needed by the callback.

Types reference#

TypeDescription
A2UIUserActionDispatched action: { name, surfaceId, sourceComponentId, timestamp, context? }
A2UIActionInterceptor(action, forward) => A2UIUserAction | null | void | Promise<void | A2UIUserAction | null> — exported interceptor type
createA2UIMessageRendererExported factory that accepts theme and optional onAction