CopilotKit

Dynamic Schema A2UI

LLM-generated A2UI — a secondary LLM creates both the schema and data from any prompt.


In the dynamic-schema approach, a secondary LLM generates the entire UI — schema, data, and layout — based on the conversation context. This is the most flexible approach: the agent can render any UI for any request without pre-defined schemas.

How it works#

  1. The primary LLM decides to call generate_a2ui
  2. Inside the tool, a secondary LLM generates a render_a2ui tool call with components, data, and layout
  3. The tool call arguments stream through LangGraph as TOOL_CALL_ARGS events
  4. The A2UI middleware intercepts these events and renders cards progressively as they stream in

Implementation#

Define the render tool schema (Python)#

This LangGraph tool defines the shape the secondary LLM must produce. It's never actually executed — it's just a schema for bind_tools:

apps/agent/src/a2ui_dynamic_schema.py
from langchain.tools import tool

@tool
def render_a2ui(
    surfaceId: str,
    components: list[dict],
    root: str,
    items: list[dict],
    actionHandlers: dict | None = None,
) -> str:
    """Render a dynamic A2UI surface with progressive streaming."""
    return "rendered"

Define the generation tool#

This is the tool the primary LLM calls. It invokes a secondary LLM that generates the full A2UI:

apps/agent/src/a2ui_dynamic_schema.py
from langchain.tools import tool, ToolRuntime
from langchain_core.messages import SystemMessage
from langchain_openai import ChatOpenAI
from copilotkit.a2ui import a2ui_prompt

A2UI_GENERATION_PROMPT = a2ui_prompt()

@tool()
def generate_a2ui(runtime: ToolRuntime) -> str:
    """Generate dynamic A2UI components based on the conversation."""
    messages = runtime.state["messages"][:-1]

    model = ChatOpenAI(model="gpt-4.1")
    model_with_tool = model.bind_tools(
        [render_a2ui],
        tool_choice="render_a2ui",
    )

    response = model_with_tool.invoke(
        [SystemMessage(content=A2UI_GENERATION_PROMPT), *messages],
    )

    tool_call = response.tool_calls[0]
    args = tool_call["args"]
    return f"Rendered A2UI surface '{args.get('surfaceId')}'."

The a2ui_prompt() helper builds a system prompt with the A2UI JSON schema reference and design guidelines. You can customize it:

# Custom design guidelines
prompt = a2ui_prompt(design_guidelines="Use a minimal, monochrome aesthetic.")

Register the tool#

apps/agent/main.py
from src.a2ui_dynamic_schema import generate_a2ui

agent = create_agent(
    tools=[generate_a2ui, ...],
    ...
)

Configure the runtime (TypeScript)#

Enable A2UI in your CopilotRuntime. The middleware auto-detects render_a2ui tool calls:

app/api/copilotkit/route.ts
const runtime = new CopilotRuntime({
  agents: { default: myAgent },
  a2ui: {
    injectA2UITool: true,
  },
});

Progressive streaming#

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

  1. Extracts the components array as it streams — waits for the full schema before rendering
  2. Extracts surfaceId and root from the partial JSON
  3. Once the schema is complete, emits createSurface + updateComponents
  4. Extracts complete items objects progressively and emits updateDataModel for each
  5. Cards appear one by one as data streams in

Built-in progress indicator#

CopilotKit includes a built-in progress indicator that shows while the schema is being generated. It appears automatically and hides once data items start streaming.

To replace it with a custom component, see the Advanced — Custom A2UI Progress Renderer guide.

Action handlers in dynamic schemas#

With dynamic schemas, the secondary LLM can generate action handlers as part of the render_a2ui tool call. The actionHandlers parameter is optional — if the LLM includes buttons with actions in the component tree, it can also include matching handlers:

# The render_a2ui tool schema includes actionHandlers
@lc_tool
def render_a2ui(
    surfaceId: str,
    components: list[dict],
    root: str,
    items: list[dict],
    actionHandlers: dict | None = None,  # LLM can generate these
) -> str:
    """Render a dynamic A2UI surface with progressive streaming."""
    return "rendered"

The a2ui_prompt() generation guidelines instruct the LLM on how to produce valid action handlers alongside buttons. You don't need to configure anything extra — if the LLM generates them, they work automatically.

For frontend-side action handling with useA2UIActionHandler, see Advanced — Action Handlers.

Customizing the generation prompt#

The a2ui_prompt() function accepts two optional arguments:

from copilotkit.a2ui import a2ui_prompt

prompt = a2ui_prompt(
    generation_guidelines="...",  # How to call the tool, path rules, data format
    design_guidelines="...",       # Visual design rules, component hierarchy
)

The generation prompt includes the full A2UI JSON schema reference and guidelines for producing valid A2UI output. The secondary LLM uses this to generate components, data bindings, and action handlers.