Skip to content

A2UI (generative UI) — Assessment (Technical)

Status: research spike. Verdict: no adoption now; small isolated prototype included for illustration (see §6). This document evaluates Google's A2UI v0.9 ("Agent-to-UI", a JSON protocol for agent-generated interfaces) against three candidate uses in WorkWingman: dynamic ATS screening questions, the judgement-call/live-run review modal, and documents/study-plan review screens.

Plain-language version: ../plain/a2ui-assessment.md

0. Sources

1. What A2UI actually is

A2UI is a JSON message protocol plus a set of client-side renderer libraries — it is not an agent framework, not a runtime that runs your agent, and not a UI framework you build screens in by hand. Three things exist:

  1. A specification — three JSON Schemas (common_types.json, server_to_client.json, catalogs/basic/catalog.json) defining the message envelope and a fixed, closed "Basic" component catalog: Text, Image, Icon, Video, AudioPlayer, Row, Column, List, Card, Tabs, Divider, Modal, Button, CheckBox, TextField, DateTimeInput, ChoicePicker, Slider. Agents can only reference components in whatever catalog the client registered — this is the protocol's core safety property ("agent output is data, not code").
  2. Renderer libraries — client-side code that turns a stream of A2UI messages into real DOM. Reference implementations exist for Lit (the primary reference, used in the official "Restaurant Finder" quickstart), Flutter, React, and Angular. All are "renderer adapters": the same protocol, one implementation per framework.
  3. Agent SDKs — currently Python and Kotlin only (agent_sdks/python, agent_sdks/kotlin; no C#/.NET SDK). These provide prompt-engineering helpers (schema pruning, few-shot injection), a streaming parser that extracts partial/complete A2UI JSON blocks out of raw LLM token output as it streams, and catalog/version negotiation. None of this is protocol-mandatory — a backend in any language can construct and validate A2UI JSON directly against the schema — but the SDK is doing real, non-trivial work that a .NET backend would have to reimplement by hand (see §3).

How UI generation actually flows

Agent (LLM)                         Client app
    │                                    │
    │  createSurface(surfaceId, root)    │
    │──────────────────────────────────▶│  renderer instantiates an empty surface
    │  updateComponents([...])           │
    │──────────────────────────────────▶│  renderer resolves the flat, ID-addressed
    │                                    │  component list (adjacency-list model —
    │                                    │  components reference parents by ID, so
    │                                    │  messages can arrive/render out of order)
    │  updateDataModel({...})            │
    │──────────────────────────────────▶│  renderer populates bound values via
    │                                    │  JSON-Pointer (RFC 6901) data paths
    │                                    │
    │        ◀── action{name, sourceId,  │  user interacts (click, type, select) —
    │             timestamp, context} ── │  renderer emits an `action` message back
    │                                    │  over whatever transport is wired up

Two-way data binding is real but constrained: input components (TextField, CheckBox, ChoicePicker, DateTimeInput, Slider) write user edits straight into the client-held data model keyed by JSON-Pointer path; a sendDataModel flag controls whether the full data model round-trips to the server on each action, or only the discrete action event does. Client-side logic is limited to a fixed set of ~15 registered functions (required, regex, email, formatDate, formatCurrency, and/or/not, string interpolation via ${/json/pointer} / ${functionName(args)}) — there is no way for the agent to ship arbitrary JS. That's a deliberate security boundary (the InfoQ coverage of the launch quotes a Hacker News skeptic: "why on earth would you trust an LLM to output a UI?" — A2UI's answer is "the LLM outputs data, not code, and the data is schema-validated before it's rendered").

Transport is explicitly agnostic — A2A 1.0, AG-UI, MCP, SSE, WebSockets, or plain REST are all named as valid carriers; the protocol only requires ordered, reliable delivery with message framing.

The Angular renderer, specifically

@a2ui/angular (currently v0.10.2 in the monorepo) is real and reasonably modern:

  • peerDependencies: @angular/core / @angular/common / @angular/platform-browser: ^21.2.5. WorkWingman is on Angular 22.0.5 — one major ahead of the renderer's stated peer range. Nothing observed in the renderer's architecture (see below) looks version-fragile, but this is an untested combination in an "early stage public preview" package; expect to either patch the peer range locally or wait for the renderer to catch up.
  • Components are standalone, use signal-based input() (not legacy @Input decorators), and the root Surface component is ChangeDetectionStrategy.OnPush with computed() signals tracking a processor.version() counter for in-place mutation tracking. This is compatible with WorkWingman's zoneless setup in spirit (OnPush + signals is exactly the pattern zoneless Angular wants) — confirmed by reading renderers/angular/src/v0_8/components/surface.ts and text-field.ts directly. Example (text-field.ts, paraphrased from source): a TextField extends DynamicComponent<TextFieldNode>, exposes label/text/textFieldType as signal inputs, and its onInput() handler either writes straight into the data model via processor.processMessages({ dataModelUpdate: ... }) (when the node has a bound path) or calls sendAction() (when it doesn't) — i.e. the "write" half of two-way binding is a plain method call into an injected processor service, not anything exotic.
  • Caveat: the package's own public-api.ts for v0.9 re-exports ./v0_8/public-api (i.e. the "0.9" entry point currently ships the 0.8 component tree under the hood) — a concrete, literal signal that the Angular renderer's v0.9 layer is mid-migration, not a settled surface to build against today.
  • No file-upload component exists in the Basic catalog at all (see §2.1).

Is there an npm package to try?

Yes: @a2ui/angular (workspace-versioned against @a2ui/web_core), plus @a2ui/markdown-it as an optional peer. Nothing is published as a versioned, independent npm release outside the monorepo's own Yarn workspace tooling (Wireit-orchestrated build, publish-from-dist/) — trying it standalone in a new Angular app means either consuming the git repo directly or waiting for a real npm publish cadence to stabilize.

2. Concrete fit in WorkWingman

Three candidates were named in the task; each is evaluated against the actual code, not the idea of the feature.

2.1 Dynamic ATS screening questions (feature-answer-mapping)

This is the closest thing WorkWingman has to "no fixed schema, per-tenant custom fields" — and it is the strongest candidate on paper. The taxonomy lives in feature-answer-mapping:src/WorkWingman.Core/Models/ApplicationQuestion.cs (not on this branch — read via git show feature-answer-mapping:...): an ApplicationQuestion record with a ~50-value QuestionType enum (LegalFirstName, AuthorizedToWorkInCountry, ScreeningYesNo, CustomFreeText, …) and a ControlKind enum:

public enum ControlKind
{
    Text, TextArea, YesNo, Dropdown, Checkbox, Date, File,
    ConditionalTextAfterYesNo   // "N/A" edge case after a preceding yes/no
}

Mapped onto A2UI's Basic catalog: Text→TextField, TextArea→TextField (multiline is not explicitly confirmed in the spec — see gap below), YesNo→CheckBox or single-ChoicePicker, Dropdown→ChoicePicker (mutually-exclusive variant), Date→DateTimeInput, Checkbox→CheckBox. File has no A2UI Basic-catalog equivalent at all — resumes/cover letters/transcripts, which do appear in real ATS forms, would need a custom catalog component, which is supported (the catalog is an "open registry pattern") but is now bespoke work on top of A2UI rather than "get it for free."

The disqualifying finding, though, isn't schema coverage — it's that this branch's own design doesn't want a generated-UI moment here at all. AnswerResolver.cs (feature-answer-mapping:src/WorkWingman.Infrastructure/Services/AnswerResolver.cs) is a deterministic, offline, no-LLM-call switch over QuestionType that maps straight to IntakeProfile/IntakeProfile.Extended fields and returns a ResolvedAnswer { Value, Confidence, IsGap, RequiresUserConfirmation, Reasoning }. The taxonomy doc (feature-answer-mapping:docs/technical/application-question-taxonomy.md) is explicit that anything the resolver can't confidently answer — knockout questions, gaps — is designed to fall through to the existing judgement-call mechanism (see §2.2), not to a new dynamically-rendered form. In other words: the "questions have no fixed schema" problem is being solved by classifying every question into one of ~50 known types up front (a taxonomy, maintained as code), not by asking an LLM to invent a UI per tenant at runtime. There is nothing in this design that is actually schema-free at the point a UI would need to be shown — by the time a human sees anything, it's already been reduced to "one of a handful of known interaction shapes," which is exactly what today's hand-coded Angular can already render cheaply.

Also material: no dynamic/schema-driven form rendering pattern exists anywhere in this codebase today — no Formly, no dynamically-composed reactive forms, no JSON-schema-form library, on any branch. Every screen, across master, feature-answer-mapping, research-pageagent, is a hand-authored standalone Angular template. Introducing A2UI here would be introducing the entire concept of schema-driven UI to the frontend, not slotting into an existing one.

2.2 Judgement-call / live-run review modal

Read directly: master:frontend/src/app/features/live-run/live-run.html and master:src/WorkWingman.Core/Models/ApplicationRun.cs. The backend shape is deliberately minimal:

public class JudgementCall
{
    public string Id { get; set; } = ...;
    public string FieldLabel { get; set; } = string.Empty;
    public string Question { get; set; } = string.Empty;
    public string Context { get; set; } = string.Empty;
    public List<RankedOption> Options { get; set; } = [];
    public string? ResolvedChoice { get; set; }
}
public class RankedOption { public string Label; public int Confidence; }

The Angular side renders this as a fixed-shape modal: a title, a subtitle, and a @for (opt of call.options; track opt.label; let first = $first) loop over buttons showing {{ opt.label }} and {{ opt.confidence }}%. This is a single, always-the-same interaction shape (ranked-choice buttons) — there is no per-instance structural variation for A2UI to add value by generating. Modeling it in A2UI would mean one Card + one ChoicePicker (or a list of Buttons), which is not materially less code than the ~15-line template that already exists, and it would add a JSON-message round-trip, a client renderer dependency, and a new failure mode (malformed/invalid agent-generated JSON) for a UI that never actually changes shape. A2UI has nothing to offer here.

2.3 Documents / study-plan review

master:frontend/src/app/features/documents/documents.ts and the sibling features/study/ follow the same pattern: fetch a typed DTO (GeneratedDocuments, StudyPlan) from the API, render it through a fixed, hand-authored template (copy-to-clipboard fields, approve/regenerate buttons). The content is agent-generated (Claude writes the resume bullet text / study plan), but the UI structure around that content never varies — it's a known, small set of fields (cover letter body, resume summary, tailoring notes) that doesn't benefit from being redeclared as UI JSON per request. Same conclusion as §2.2: no structural variability for A2UI to justify itself against.

3. Integration cost + fit with the stack

Concern Finding
Angular compatibility Renderer targets ^21.2.5; WorkWingman is on 22.0.5. Architecturally compatible (standalone, signals, OnPush) but untested combination in an early-preview package; v0.9 entry point still re-exports v0.8 components internally.
Zoneless No conflict found — renderer pattern (OnPush + signals + computed()) is exactly what zoneless Angular wants. Not independently verified by running it (no live Node/npm access exercised in this spike beyond registry reads).
.NET backend No official SDK. WorkWingman's backend (ASP.NET Core, Controllers, src/WorkWingman.Api/Controllers/*.cs) would have to hand-construct and validate A2UI JSON envelopes itself, and reimplement the streaming-block-extraction parser the Python SDK provides for pulling partial UI messages out of a live Claude token stream. This is a real, non-trivial chunk of infrastructure (prompt-schema pruning, catalog/version negotiation, partial-JSON streaming parse) to build and maintain for a solo/local-first app.
Agent framework assumption None strictly required (transport-agnostic: REST/SSE/WebSocket all valid) — but the ergonomic path (SDKs, examples, AG-UI/A2A bridges) assumes Python + one of {A2A, AG-UI, MCP}. WorkWingman calls Claude directly from .NET; there's no existing AG-UI/A2A layer to plug into.
Protocol maturity v0.9 is explicitly "Early Stage Public Preview" per the repo README; the spec itself frames v0.9 as a "philosophical shift" from v0.8, with v1.0 only at release-candidate. Building against this now means absorbing breaking changes on the way to v1.0.
Realistic effort (if pursued) Rough estimate for a minimal, single-surface prototype wired end-to-end (Claude → hand-rolled A2UI JSON → @a2ui/angular render → action round-trip → .NET handling): 3–5 days for one screen, most of it schema/prompt plumbing and testing the Angular-22-against-^21-peer-dep question, not counting hardening, the file-upload gap, or catalog extension. This estimate assumes the renderer works as documented; it was not run locally in this spike.

4. Recommendation

No adoption now, for either the Playwright app or the PageAgent variant. Revisit only if a genuinely per-tenant, structurally-unpredictable UI need appears (see §4.3) — none exists in the codebase today.

4.1 Playwright app (current, shipping)

No. Every named candidate use (screening questions, judgement-call modal, documents/study review) turns out, on inspection of the actual code and design docs, to have a small, closed set of interaction shapes that hand-coded Angular already renders cheaply and testably. The feature-answer-mapping design explicitly chose a maintained taxonomy + deterministic resolver over "let an LLM figure out the UI," and routes the genuinely-ambiguous cases through the existing judgement-call mechanism rather than inventing a new generated-UI surface. Adding A2UI here would mean taking on an early-preview protocol, a renderer with an unverified Angular-22 combination, and a from-scratch .NET-side reimplementation of infrastructure the Python SDK provides for free — for UI variability that doesn't actually exist yet. This app's own investment (see docs/technical/testing.md) is in deterministic, heavily-tested hand-built screens; that's the opposite instinct from "let the model draw the form at runtime."

4.2 PageAgent variant (research-pageagent / feature-pageagent-swap)

No, and not obviously applicable even later. PageAgent (research-pageagent:docs/technical/pageagent-feasibility.md) is a client-side, in-page DOM-automation agent — it has no user-facing UI concept at all, and that doc confirms the existing Angular Live Run viewer is unaffected/unchanged if PageAgent were adopted. A2UI solves "how does an agent describe a UI to a human"; PageAgent solves "how does an agent act on a page a human isn't looking at as a form." They don't intersect. If PageAgent is adopted for long-tail ATS fallback, it still doesn't create any new UI-generation need — the judgement-call model would need to be re-implemented as glue code either way, per that doc's own findings, independent of A2UI.

4.3 What would change this recommendation

A concrete, observed need for truly per-instance, structurally unpredictable UI — e.g. if WorkWingman ever let Claude free-form draft an entire settings/config screen whose fields aren't known until runtime, or if the app added a chat-style "ask me anything about my applications" surface where the response shape (table vs. chart vs. form) should vary per query. Nothing in the current roadmap (docs/technical/making-of.md, the four research branches read for this spike) describes such a feature. If one emerges, re-run this assessment against A2UI's then-current maturity (v1.0 status, Angular renderer parity, whether a .NET/community SDK has appeared) before reconsidering.

5. Phased plan (if revisited later)

Not activated now, but recorded for future reference:

  1. Wait for v1.0 stable and confirm @a2ui/angular publishes against an Angular 22+ (or whatever WorkWingman is on then) peer range with a real npm release, not workspace-only.
  2. Prototype the .NET side first — a minimal C# helper that constructs/validates createSurface/updateComponents/updateDataModel messages against the published JSON Schemas (System.Text.Json + a schema validator), independent of any specific screen. This is the highest-risk, least-supported part of the stack for this app specifically. The streaming-block-extraction problem (pulling partial A2UI JSON out of a live Claude /v1/messages stream) is the hardest sub-piece and should be spiked alone before anything user-facing.
  3. Pick the single screen with the most genuine per-instance variability at that time (not necessarily screening questions — re-check whether the taxonomy approach still holds) and build one real surface end-to-end, gated behind a feature flag, isolated from the production route tree the way this spike's prototype is (see §6).
  4. Only fold it into a real feature once it has replaced (not merely duplicated) a hand-coded screen for at least one real ATS/tenant case, with the same test coverage bar the rest of the frontend holds (see docs/technical/testing.md).

6. Prototype: what was built, and why it's isolated

Given the "no" verdict, no production code was touched and no existing screen was modified. A small, self-contained demo lives at frontend/src/app/features/a2ui-lab/, reachable only at the unlisted route /a2ui-lab (not added to any nav menu). It does not depend on the @a2ui/angular npm package (which would require network access to install and an unverified Angular-22 peer-dependency override) — instead it hand-rolls a minimal, honest subset of the A2UI v0.9 message shapes (createSurface/updateComponents/updateDataModel, a JSON-Pointer data model, and an action message emitted back) against a tiny local "renderer" that switches over component type (TextField, ChoicePicker, CheckBox, DateTimeInput) — enough to render one realistic screening-question form (drawn from the feature-answer-mapping taxonomy: work authorization yes/no, visa type dropdown, "why this company" free text, start date) from a static JSON payload standing in for what an agent would emit.

This exists purely to make the assessment's claims checkable — "here is what the message shape looks like, here is what it would render, here is the size of the gap between this and a real integration" — not as a first slice of a real feature. It is intentionally kept out of app.routes.ts's main list and out of the queue/documents/study navigation flow. See frontend/src/app/features/a2ui-lab/README.md for exactly what is and isn't real about it.