Back to Copilotkit

Dynamic Schema A2UI

showcase/shell-docs/src/content/docs/generative-ui/a2ui/dynamic-schema.mdx

1.57.04.2 KB
Original Source

In the dynamic-schema approach, a secondary LLM generates the entire UI (schema, data, and layout) based on the conversation context. It's the most flexible A2UI flavor; the agent can render any UI for any request without pre-defined schemas.

<InlineDemo demo="declarative-gen-ui" />

How it works

  1. The primary LLM decides to call render_a2ui (the tool the runtime auto-injects when injectA2UITool: true).
  2. The runtime serializes your client-side catalog (component names + Zod prop schemas) into the agent's copilotkit.context so the LLM knows which components it may emit.
  3. The tool call streams through LangGraph as TOOL_CALL_ARGS events.
  4. The A2UI middleware intercepts the stream and renders cards progressively as data items arrive.

The 3-file split

The canonical Bring-Your-Own-Catalog (BYOC) layout keeps three files side-by-side under frontend/src/app/a2ui/:

FileWhat lives there
definitions.tsZod props schema + human-readable descriptions for each custom component. Platform-agnostic, so the runtime can serialise it to the LLM.
renderers.tsxReact implementations keyed by the same names. TypeScript enforces that every definition has a renderer.
catalog.tscreateCatalog(definitions, renderers, { includeBasicCatalog: true }): merges your custom components with CopilotKit's built-in primitives.
<Steps> <Step> ### Declare your custom component definitions

Each entry pairs a Zod prop schema with a description. The description is crucial; the LLM reads it to decide which component to emit. The example below ships a small dashboard catalog (Card / StatusBadge / Metric / InfoRow / PrimaryButton):

<Snippet region="definitions-zod" /> </Step> <Step> ### Implement the React renderers

Every key in myDefinitions must have a matching renderer. Props are statically typed against the Zod schema, so refactors stay safe:

<Snippet region="renderers-react" /> </Step> <Step> ### Wire definitions × renderers into a catalog

createCatalog is what you hand to the provider. Flip includeBasicCatalog: true to merge CopilotKit's built-ins (Column, Row, Text, Image, Card, Button, List, Tabs, …), so the LLM can compose custom + basic components interchangeably:

<Snippet region="create-catalog" /> </Step> <Step> ### Pass the catalog to the provider

A single prop (a2ui={{ catalog }}) is all the frontend needs; the provider registers the catalog and wires up the built-in A2UI activity-message renderer:

<Snippet region="provider-a2ui-prop" /> </Step> <Step> ### Inject the render tool on the runtime

On the TypeScript runtime, injectA2UITool: true tells CopilotKit to add the render_a2ui tool to the agent's tool list at request time and serialise your client catalog into the agent's copilotkit.context. No backend code to write; the agent can be an empty create_agent(tools=[]):

typescript
const runtime = new CopilotRuntime({
  agents: { default: myAgent },
  a2ui: {
    injectA2UITool: true,
  },
});
</Step> </Steps>

Progressive streaming

The secondary LLM's render_a2ui tool call streams through LangGraph as TOOL_CALL_ARGS events. The A2UI middleware:

  1. Waits for the full components array before emitting anything — the schema must be complete before rendering starts.
  2. Extracts surfaceId + root from the partial JSON.
  3. Emits surfaceUpdate + beginRendering once the schema is complete.
  4. Extracts complete items objects progressively and emits a dataModelUpdate for each, so cards appear one by one as data streams in.

A built-in progress indicator shows while the schema is still generating and hides automatically once data items start arriving.

When should I use dynamic schemas?

  • You don't know the UI shape ahead of time; the agent decides what to show based on the user's request.
  • You want to prototype A2UI without committing to a schema file yet.
  • You're building a conversational dashboard where the layout varies per turn.

If the surface is well-known (e.g. a product card, a flight result), prefer a fixed schema; it's faster, cheaper, and the UI is deterministic.

<IntegrationGrid path="generative-ui/a2ui" />