DEV Community

Brian Love
Brian Love

Posted on • Originally published at brianflove.com

Google A2UI: Fixed Schemas, Dynamic Schemas, and a Safe Fallback Strategy

Google's A2UI is one of the more interesting protocol ideas in agentic UI right now.

Not because it lets a model generate arbitrary frontend code.
That would be a fast way to move chaos across a trust boundary.

It is interesting because it does the opposite.

A2UI gives the agent a declarative way to describe UI while keeping rendering inside a trusted client-owned component system.

That is the part that matters.
Especially if you are building a real product instead of a demo.

The practical question is not whether A2UI is good.
The practical question is how to use it without turning your UI contract into a probabilistic event.

For me, that comes down to one decision:

  1. Fixed schema
  2. Dynamic schema

My recommendation is simple:

  • Use fixed schemas by default.
  • Use dynamic schema overlays for the long tail.
  • Validate everything.
  • Retry a few times.
  • Fall back deterministically.

tl;dr

  • A2UI is a protocol for agent-driven interfaces where the client owns rendering and the model emits declarative UI messages.
  • A2UI v0.9 is more model-friendly than earlier versions because it is flatter, more prompt-friendly, and easier to validate.
  • Most teams should start with fixed schemas because they are easier to test, easier to operate, and easier to trust.
  • Dynamic schemas are useful, but only when they stay grounded in renderer-approved catalogs.
  • The right production posture is fixed by default, dynamic overlays for the long tail, and deterministic fallback when validation fails.

Fullstack architecture sketch

Before getting into fixed versus dynamic schema, this is the deployment shape I would use:

Animated fullstack architecture diagram for A2UI showing the trusted web client, the API and agent backend, and the observability layer.

Web Client
  - trusted A2UI renderer
  - advertises supported catalogs
  - may advertise inline catalogs
  - sends actions and data model updates

API / Agent Backend
  - receives client capabilities
  - selects fixed vs dynamic policy
  - builds prompt from selected catalog
  - runs schema repair loop when dynamic
  - validates final payloads
  - streams safe A2UI messages to client

Observability
  - schema validation failures
  - payload validation failures
  - fallback frequency
  - per-intent success rate
Enter fullscreen mode Exit fullscreen mode

Why A2UI matters

A2UI matters because it gives us a cleaner split between model behavior and frontend control.

The model decides what interface should appear.
The client decides how that interface is rendered.

That is a much better contract than shipping arbitrary frontend code across the wire and pretending the security story will work itself out.

Animated diagram of A2UI where the agent emits declarative messages, the backend validates the contract, and the trusted client renderer owns rendering.

Why v0.9 is the interesting version

One caveat up front:
A2UI v0.9 is still a draft, while v0.8 is the stable production release.

So this is not a "ship and forget" protocol.
You should pin versions and test contracts carefully.

But v0.9 is still the version I would watch, because it is the version that feels much more usable for agent systems.

The notable shifts are:

  • a prompt-first design
  • createSurface replacing beginRendering
  • a flat component model with a component discriminator
  • more standardized updateDataModel
  • swappable catalogs instead of a single hardwired shape

That makes the protocol easier to prompt, easier to generate, and easier to validate.
That is the real win.

A quick note on what "schema" means here

When people say "fixed schema" or "dynamic schema" in the context of A2UI, they usually mean something looser than a single JSON Schema file.

In practice, the protocol is made up of separate artifacts:

  • a server-to-client envelope
  • shared common types
  • one or more catalogs that define renderable components

So the real decision is usually this:

  • do you give the model a fixed catalog contract?
  • or do you dynamically select, narrow, or extend that contract for a specific request?

That is the framing that matters.

The protocol shape you are actually building around

For a web app, the v0.9 lifecycle is fairly straightforward:

  1. The server sends createSurface.
  2. The server sends updateComponents.
  3. The server may send updateDataModel.
  4. The client renders the surface and sends actions back.
  5. The server continues streaming updates as needed.

A minimal shape looks like this:

[
  {
    "version": "v0.9",
    "createSurface": {
      "surfaceId": "signup",
      "catalogId": "https://a2ui.org/specification/v0_9/basic_catalog.json",
      "sendDataModel": true
    }
  },
  {
    "version": "v0.9",
    "updateComponents": {
      "surfaceId": "signup",
      "components": [
        {
          "id": "root",
          "component": "Column",
          "children": ["title", "email", "submit"]
        },
        {
          "id": "title",
          "component": "Text",
          "text": "# Create account"
        },
        {
          "id": "email",
          "component": "TextField",
          "label": "Email",
          "value": { "path": "/user/email" }
        },
        {
          "id": "submit_label",
          "component": "Text",
          "text": "Continue"
        },
        {
          "id": "submit",
          "component": "Button",
          "child": "submit_label",
          "action": {
            "event": {
              "name": "submitSignup"
            }
          }
        }
      ]
    }
  }
]
Enter fullscreen mode Exit fullscreen mode

The important implementation detail is that A2UI uses a flat list of components linked by IDs.
That is a good choice because it is easier to emit, easier to diff, and easier to validate for:

  • duplicate IDs
  • missing root
  • dangling references
  • cycles
  • orphaned components

Those are exactly the boring details that determine whether a generative UI system survives contact with production.

Strategy 1: Fixed schema

Fixed schema means your application chooses the catalog ahead of time and the model only emits valid instances of UI that conform to that contract.

This is the default I would recommend for most teams.

Use it when:

  • the workflow is business-critical
  • the UI shape is predictable
  • you want strong testability
  • analytics and event handling need to stay stable
  • you do not want renderer behavior drifting between requests

Examples:

  • onboarding flows
  • support triage
  • approvals
  • payments
  • account management
  • healthcare or compliance-heavy forms

These are not the places where you want model creativity.

Why fixed schema works

A fixed schema gives you a stable protocol boundary.
That buys you:

  • prompts are easier to reason about
  • examples are easier to curate
  • QA can build reusable fixtures
  • regressions are easier to reproduce
  • analytics stay more consistent
  • frontend behavior remains predictable

It also makes debugging much easier.
When something breaks, you are usually debugging a bad payload, not a drifting contract and a bad payload at the same time.

My rule of thumb

If the flow is revenue-critical, compliance-heavy, or operationally sensitive, start fixed.

You can always loosen the system later.
Going the other direction is usually painful.

Strategy 2: Dynamic schema

Dynamic schema is where things get more interesting.
It is also where teams get themselves into trouble.

What I mean by dynamic schema is not "let the model invent whatever UI primitives it wants."

That is not a schema strategy.
That is giving up.

In a production A2UI application, dynamic schema should still be renderer-owned.
The client needs to know how to render every component the model is allowed to reference.

So in practice, dynamic schema usually means one of three things:

  1. Dynamic catalog selection from a trusted set
  2. Dynamic use of client-advertised inline catalogs
  3. Dynamic overlays that narrow or extend a trusted base catalog for a specific request

That third option is the one I would ship most often.

The dynamic pattern I recommend

Do not let the model generate a brand new universe from scratch.

Instead:

  1. Start from a fixed trusted base catalog.
  2. Let the model generate a task-shaped overlay.
  3. Validate that overlay.
  4. Merge it into the base contract for prompting and validation.
  5. Fall back to the base contract if the overlay keeps failing.

This gives you flexibility without losing control.
Most products do not need unbounded UI generation.
They need a safe way to adapt a known renderer contract to the long tail.

Animated diagram of the A2UI dynamic overlay loop showing fixed-by-default policy, overlay validation and repair, and deterministic fallback.

Why validation is non-negotiable

A2UI v0.9 is more prompt-friendly.
It is not a security model.

If you allow a model to shape any part of the UI contract, then validation becomes part of the product, not an implementation detail.

At minimum, a serious pipeline should validate two things:

  1. The schema contract itself
  2. The final A2UI payload generated against that contract

That separation matters because a valid overlay does not guarantee a valid payload.

For dynamic schema in particular, I would validate:

  • overlay structure
  • component and property compatibility
  • child reference types
  • topology integrity
  • final payload instances against the selected catalog

Without this layer, dynamic schema quickly turns into "the backend forwards malformed intent and hopes the renderer is charitable."

The architecture I would actually ship

If I were building a fullstack agent web app around A2UI, this is the high-level policy I would use.

1. Fixed by default

Use a fixed catalog for the majority of flows.
Keep the happy path boring.

That is where stability and predictable UX come from.

2. Dynamic for the long tail

Use dynamic overlays only for workflows that are genuinely exploratory, tenant-specific, or too variable for one static prompt contract.

This keeps the flexible path available without making the whole system probabilistic.

3. Dynamic means task-shaped, not unbounded

Restrict dynamic behavior to things like:

  • narrowing allowed components
  • adding task-specific examples or constraints
  • selecting from trusted custom catalogs
  • merging safe overlays over a base catalog

Do not let the model invent component semantics the client never negotiated.

4. Validate, repair, retry, fallback

The loop should be:

  1. Generate candidate overlay
  2. Validate it
  3. If invalid, feed the error back to the model
  4. Retry a few times
  5. Fall back to fixed if it still fails

I would cap retries at around five attempts.

Past that point, you are not repairing the schema.
You are negotiating with entropy.

At a high level, the control flow should look like this:

user request
  -> choose fixed or dynamic policy
  -> if dynamic:
       generate candidate overlay
       validate overlay
       if invalid:
         return validation error to model
         retry up to N times
       if still invalid:
         fall back to fixed catalog
  -> generate final A2UI payload
  -> validate final payload
  -> stream to client
Enter fullscreen mode Exit fullscreen mode

The implementation should be just as explicit:

MAX_SCHEMA_ATTEMPTS = 5


def resolve_catalog(request_text: str, client_capabilities: dict | None):
    fixed_catalog = get_fixed_catalog(client_capabilities)

    if not should_use_dynamic_schema(request_text):
        return "fixed", fixed_catalog

    last_error = None
    last_overlay = None

    for _ in range(MAX_SCHEMA_ATTEMPTS):
        overlay = llm_generate_overlay(
            request_text=request_text,
            client_capabilities=client_capabilities,
            previous_error=last_error,
            broken_schema=last_overlay,
        )

        try:
            selected_catalog = validate_overlay(overlay, client_capabilities)
            return "dynamic", selected_catalog
        except Exception as exc:
            last_error = str(exc)
            last_overlay = overlay

    return "fallback-fixed", fixed_catalog
Enter fullscreen mode Exit fullscreen mode

5. Validate the final payload too

Even after the schema contract passes, validate the generated A2UI payload before it reaches the client.

Schema validation and instance validation are separate responsibilities.
Treat them that way.

6. Cache successful overlays

If a task-shaped overlay validates and performs well for a repeated intent, cache it by tenant, task family, and capability set.

That reduces latency, prompt cost, and repeated schema thrash.

7. Treat all agent output as untrusted

This should be obvious, but it is still worth stating directly.

Agent messages, UI payloads, and model-shaped schema overlays are all untrusted input.

That means:

  • validation
  • sanitization
  • strict renderer boundaries
  • strong CSP and isolation where relevant
  • no casual trust just because the output happens to be JSON-shaped

A useful mental model

I think the cleanest way to reason about A2UI is this:

  • the renderer is trusted
  • the model is constrained
  • the backend is the enforcer
  • fallback is deterministic

That is the architecture.
Once you see it that way, the fixed-versus-dynamic decision gets much easier.

When to use which approach

My practical rule is simple.

Use fixed schema when you want reliability.
Use dynamic overlays when variability is real and the value justifies the complexity.

In practice:

  • fixed for core trusted workflows
  • dynamic for the long tail

Per surface, not per company.

Conclusion

A2UI v0.9 is a meaningful step forward for agent-driven interfaces.

The flatter structure, prompt-first posture, swappable catalogs, and explicit lifecycle make it much more usable than older "generate a giant nested object and pray" approaches.

But the big lesson is not that everything should become dynamic.
The big lesson is that agent-driven UI still needs architecture.

The posture I would use is:

  • fixed schemas for stable, high-confidence workflows
  • dynamic overlays for the long tail
  • strict validation in the middle
  • repair loops for recoverable failures
  • deterministic fallback when the model cannot produce a safe contract

That gives you flexibility without surrendering control.
And in agent systems, control is the thing that lets creativity survive production.

References

Top comments (0)