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#
- The primary LLM decides to call
generate_a2ui - Inside the tool, a secondary LLM generates a
render_a2uitool call with components, data, and layout - The tool call arguments stream through LangGraph as
TOOL_CALL_ARGSevents - 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:
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:
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#
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:
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:
- Extracts the
componentsarray as it streams — waits for the full schema before rendering - Extracts
surfaceIdandrootfrom the partial JSON - Once the schema is complete, emits
createSurface+updateComponents - Extracts complete
itemsobjects progressively and emitsupdateDataModelfor each - 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.
