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. It's the most flexible A2UI flavor: 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

Frontend / runtime#

Enable A2UI on your CopilotRuntime with injectA2UITool: true:

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

injectA2UITool: true is the single switch: it forwards the decision to the agent so the A2UI tool is injected. Set it to false to turn A2UI off entirely — nothing is injected and no surfaces render.

Register your component catalog on the provider so the LLM knows what it can draw (see Bring Your Own Catalog for the catalog definitions / renderers / createCatalog split):

frontend/src/app/page.tsx
<CopilotKit a2ui={{ catalog: myCatalog }}>
  {/* ... */}
</CopilotKit>

Backend — prebuilt agent#

If you use a prebuilt agent (create_agent), Just add CopilotKitMiddleware — it auto-injects generate_a2ui (built from your agent's own model) when injectA2UITool is on, and executes it:

agent/a2ui_dynamic.py
from copilotkit import CopilotKitMiddleware
from langchain.agents import create_agent
from langchain_openai import ChatOpenAI

graph = create_agent(
    model=ChatOpenAI(model="gpt-5.4"),
    tools=[],
    middleware=[CopilotKitMiddleware()],
    system_prompt=(
        "Whenever a response would benefit from a rich visual — a dashboard, "
        "KPI summary, card layout, or chart — call `generate_a2ui` to draw it. "
        "Keep chat replies to one short sentence and let the UI do the talking."
    ),
)

The system prompt can be used to tell the model when to draw; the middleware supplies and runs the tool.

Backend — graph (node-based) agent#

If you build the graph yourself (StateGraph with your own nodes), wire the get_a2ui_tools tools suite in directly. It returns the generate_a2ui tool bound to a model you provide; add it to your ToolNode like any other tool:

agent/a2ui_dynamic_schema.py
from langchain_openai import ChatOpenAI
from langgraph.graph import StateGraph, END, MessagesState
from langgraph.prebuilt import ToolNode
from ag_ui_langgraph import get_a2ui_tools

base_model = ChatOpenAI(model="gpt-4o")

TOOLS = [
    get_a2ui_tools(
        {
            "model": base_model,
            "default_catalog_id": "https://a2ui.org/demos/dojo/dynamic_catalog.json",
            # guidelines: optional generation/design/composition prompt knobs
            "guidelines": {"composition_guide": COMPOSITION_GUIDE},
        }
    )
]

async def chat_node(state, config):
    model = base_model.bind_tools(TOOLS, parallel_tool_calls=False)
    response = await model.ainvoke([SystemMessage(content=SYSTEM_PROMPT), *state["messages"]], config)
    return {"messages": [response]}

workflow = StateGraph(AgentState)
workflow.add_node("chat_node", chat_node)
workflow.add_node("tool_node", ToolNode(tools=TOOLS))
workflow.set_entry_point("chat_node")
workflow.add_conditional_edges("chat_node", route_after_chat)  # -> tool_node when a tool call is present
workflow.add_edge("tool_node", "chat_node")
graph = workflow.compile()

get_a2ui_tools(params) takes a single A2UIToolParams object — the same factory the middleware uses under the hood:

  • model — the secondary LLM that designs the surface.
  • default_catalog_id — binds generated surfaces to your catalog (BYOC). Catalog ownership stays with the host; the model never picks it.
  • guidelines — optional prompt knobs: composition_guide (which components exist and how to compose them), plus generation_guidelines / design_guidelines to override the built-in generation/design defaults.

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.