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¶
- Spec: a2ui.org/specification/v0.9-a2ui
- Announcement: developers.googleblog.com/a2ui-v0-9-generative-ui
- Coverage: infoq.com/news/2026/07/google-a2ui-genui
- Reference implementation: github.com/a2ui-project/a2ui (Apache-2.0, "early stage public preview", current stable v0.9.1, v1.0 in release candidate)
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:
- 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"). - 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.
- 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@Inputdecorators), and the rootSurfacecomponent isChangeDetectionStrategy.OnPushwithcomputed()signals tracking aprocessor.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 readingrenderers/angular/src/v0_8/components/surface.tsandtext-field.tsdirectly. Example (text-field.ts, paraphrased from source): aTextFieldextendsDynamicComponent<TextFieldNode>, exposeslabel/text/textFieldTypeas signal inputs, and itsonInput()handler either writes straight into the data model viaprocessor.processMessages({ dataModelUpdate: ... })(when the node has a boundpath) or callssendAction()(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.tsfor 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:
- Wait for v1.0 stable and confirm
@a2ui/angularpublishes against an Angular 22+ (or whatever WorkWingman is on then) peer range with a real npm release, not workspace-only. - Prototype the .NET side first — a minimal C# helper that constructs/validates
createSurface/updateComponents/updateDataModelmessages 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/messagesstream) is the hardest sub-piece and should be spiked alone before anything user-facing. - 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).
- 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.