docs/content/docs/integrations/langgraph/generative-ui/a2ui/dynamic-schema.mdx
import { Callout } from "fumadocs-ui/components/callout" import { Steps, Step } from "fumadocs-ui/components/steps"
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.
generate_a2uirender_a2ui tool call with components, data, and layoutTOOL_CALL_ARGS eventsThis LangChain 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"
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.")
from src.a2ui_dynamic_schema import generate_a2ui
agent = create_agent(
tools=[generate_a2ui, ...],
...
)
Enable A2UI in your CopilotRuntime. The middleware auto-detects render_a2ui tool calls:
const runtime = new CopilotRuntime({
agents: { default: myAgent },
a2ui: {
injectA2UITool: true,
},
});
The secondary LLM's render_a2ui tool call streams through LangGraph as TOOL_CALL_ARGS events. The A2UI middleware:
components array as it streams — waits for the full schema before renderingsurfaceId and root from the partial JSONcreateSurface + updateComponentsitems objects progressively and emits updateDataModel for eachCopilotKit 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.
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.
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
)