---
title: "Don't Generate the UI. Let AI Compose It."
description: "The tempting way to give an agent a real interface is to let it write the frontend. The better way is to give it a small set of trusted primitives and let it choose. A form, a chart, a map and a table, selected at runtime around the conversation."
canonical: "https://davidgolverdingen.nl/en/insights/dont-generate-the-ui"
last-updated: "2026-09-12"
---

# Don't Generate the UI. Let AI Compose It.

Ask where our vans are around Utrecht, and the model answers with a map. Ask which installations at one of the sites we maintain are getting expensive, and it answers with a table. Ask how energy moves through a building, and a Sankey diagram appears. Say you want to update the assumptions on three of those projects, and it opens a small form with the fields it already knows filled in.

None of those four screens was designed as that workflow. There is no bespoke company assistant here and no screen built for any of those four questions. There is a general model, in a normal chat, with a set of tools behind it, and it picked each representation and configured it around the conversation out of a handful of generic interaction primitives that run behind one MCP server. That is the whole idea I want to argue for, and it starts with a distinction that is easy to skip past.

## Two ways to give an agent a real interface

Chat is a fine interface for ambiguous conversation. It is a poor one for structured input, for dense comparison, for anything spatial. Once an agent has to collect structured input or explain rich data, chat alone starts to be the wrong interface, and there are two ways past it.

The tempting one is to let the model generate the frontend. Hand it a canvas and let it write HTML, or a component, or a whole page. That gives you enormous flexibility, and it moves far too much into a probabilistic system. The model is now on the hook for component correctness, accessibility, validation, responsive behaviour, error handling, write confirmation and browser quirks. None of that is where a model earns its keep.

The other way is to let the model compose the interface from primitives it did not write. It chooses which one fits the intent and how to configure it. A deterministic application still owns rendering, interaction validation and styling, while the capability keeps permissions and the write boundary.

> Generated UI writes the interface. Composed UI chooses it.

The difference is where responsibility sits. A model is good at deciding what kind of interaction would help. Deterministic software is much better at guaranteeing how that interaction behaves. Composed UI puts each on the side of the line it is good at.

## A renderer gives components. A domain-rich app gives a grammar.

Composed UI only works if the primitives carry more than pixels. A generic chart renderer hands the model a `bar` and a `line` and leaves it to guess when each one is right. That is the same mistake as [a thin API wrapper that leaves the model to guess what a field means](https://davidgolverdingen.nl/en/insights/your-data-is-fine): access without meaning, with the guessing moved into the UI layer instead of removed.

A domain-rich app carries the interaction knowledge too. By domain-rich I do not mean business semantics piled into the UI layer. I mean the primitive carries the knowledge required to use it well. For a chart, that is a decision matrix: which shapes of data each visualisation is good at expressing, what fields each one needs, which combinations are misleading. Ours renders fourteen chart types today, and the count is not the point. The rules that sit beside them are. The chart app tells the model that a pie chart answering a ranking question should be a sorted horizontal bar. A radar with mixed units should be a heatmap, and a line chart on a categorical axis is really a bar chart. The model reasons over that designed choice space instead of rediscovering visualisation principles on every query. For a form, the knowledge is the field types, the validation rules, what is required, what is constrained, what can be prefilled. For a table it is the column types that turn a number into a currency or a status into a coloured badge. For a map it is the markers and layers.

![Data table of five HVAC installations with manufacturer, build year, a condition score shown as filled dots, yearly maintenance cost in euros and a fault-trend sparkline, totalling 31,150 euros.](https://davidgolverdingen.nl/images/blog/dont-generate-the-ui-table.png "The same runtime answering with a table. Condition as a six-dot score, costs as currency, a fault sparkline per row. Composed by the model in a chat for this post, on sample data.")

![Sankey diagram of a building's daily energy flow from Electricity and Gas through Heat pump, Boiler, Lighting and Server room into Heating, Cooling and Offices.](https://davidgolverdingen.nl/images/blog/dont-generate-the-ui-sankey.jpg "A flow question, answered as a Sankey diagram, one of fourteen chart types the model can choose. Composed by the model in a chat, on sample data.")

I keep the responsibilities split three ways:

- the **capability** carries business meaning, permissions and operations;
- the **app** carries interaction meaning and presentation constraints;
- the **model** carries intent, reasoning and the choice at runtime.

Domain knowledge does not all move into the UI. The app knows how constrained structured input should be collected and shown. The capability knows what a maintenance request means, which fields matter and who may file one. That stays on the server, where it already lives.

## The old application knew the workflow before you arrived

Traditional enterprise software decides the interaction in advance. A developer chooses which page exists, which fields sit on the form, which columns the table shows, which chart represents the data, and which navigation path gets you there. The application carries the meaning, and the user has to translate their question into its structure. "Where are our vans around Utrecht" becomes: open fleet, find the vehicle screen, locate the map, set the filters, read the result. The interface exists before the intent.

An agent inverts that. The intent arrives first. By the time a representation is needed the model already knows what was asked, which capabilities are relevant, what data came back and whether more input is required. So the flow stops being **screen → controls → intent** and becomes **intent → capability → the interaction that fits**. The application is no longer a fixed sequence of screens. It gets composed around the task while the conversation runs.

![Map of the Utrecht area with five company-van markers placed around the city.](https://davidgolverdingen.nl/images/blog/dont-generate-the-ui-map.jpg "The vans question, answered as a map. The model recognised that location was the point, then selected and configured the map primitive. Composed in a chat for this post, on sample data.")

## The constraint is the feature

The architecture that makes this safe is not `model → arbitrary UI code`. It is `model → constrained interaction schema → trusted renderer`. The model has real freedom, and that freedom sits inside a boundary drawn by engineers. For a chart the boundary is the valid visualisation types and their configuration. For a form it is the supported field types and validation. For a map it is the markers and layers that exist. The model chooses inside that space. It can still propose something the schema does not allow, and the renderer is what refuses to execute it.

That constraint is not a limitation to apologise for. It is the thing that lets deterministic software and probabilistic reasoning each do what they are good at, in the same interaction, without either one being trusted with the other's job.

## Write flows are where this earns the most

Reads are the easy case. Writes are where composed UI stops being a nicety.

Here is one we run. An energy consultant is reviewing a building's consumption, and the model has drafted a handful of weekly observation notes to sit on the graph, each pinned to a point on the time axis. Instead of writing them, it opens a review form. The consultant edits the text of each note, ticks the ones worth keeping, and approves. The time anchors the model chose are shown read-only, so a note pinned to the wrong week gets caught by the person who would know.

Then the mechanics that matter. The model cannot write the notes. Approval in the form is what calls the commit, through a tool the model has no access to, and the form mints a single-use intent that is spent on that one approval. If a week looks unclear the consultant sends it back to chat instead of approving, nothing is written, and the model goes and investigates. The write is create-only and restricted to that group of consultants, and the approval is the only thing that ever authorises it.

> The model decides what information is needed. The app controls how it is collected. The capability controls what may actually be written.

That is the shape under every composed write, and it is a convention the servers share rather than a bespoke flow. Another write tool, the one that attaches a file to a record, is app-only in the same way: only its own app may call it, never the model. The generic form primitive does the milder version of the same thing. It validates client-side as a convenience for the person filling it in, then hands the values back to the model, and it cannot reach a backend at all. Either way the UI is a convenient way to gather and confirm structured input. It is never the guarantee about what reaches the database, and nothing rendered client-side ever should be.

![A form titled Update forecast with read-only Project and Client fields, editable revenue, margin, handover date and risk fields, and Approve and Discuss-in-chat buttons.](https://davidgolverdingen.nl/images/blog/dont-generate-the-ui-form.png "A write, composed. The known fields sit read-only, the editable ones below, with an Approve and a Discuss-in-chat path. The write runs behind a server capability, not this form. Composed in a chat, on sample data.")

## One question, several representations

The version of this that I find most convincing is not one app per conversation. It is a single task moving through several.

Someone asks which projects have margins below five percent. The model queries live ERP data and answers with a table. Then: show me where those are. It reuses the same result and selects a map. Then: update the forecast for these three. It selects a form, prefills the project data it already pulled, and asks only for what it is missing. Table, then map, then form, then a bounded write, with no fixed page flow anywhere in it. The application emerged around the conversation instead of the conversation being routed through the application.

## This is not "no frontend"

The conclusion is not that frontend engineering disappears. It becomes more infrastructural. Instead of building every workflow as its own screen, you build a small number of primitives to a high bar: robust schemas, real validators, design-system integration, accessibility, responsive behaviour, safe action patterns, rendering guarantees. The amount of bespoke screen logic goes down while the quality bar on the primitives goes up.

> The frontend moves from designing every journey to designing the grammar the journeys are composed from.

That is closer to building an interaction runtime than building pages, and it is a stronger claim than "AI can generate forms."

## Where I would not push this

I want to keep this honest, because the failure mode of an idea like this is to oversell it. Composed UI is early here. It is a small set of apps behind one server, not a finished platform, and I am describing a pattern that is emerging rather than a system I have run for years.

It does not mean every enterprise screen should become dynamic. Plenty of workflows are known in advance and deserve a designed page. It does not mean generated UI is always wrong, or that chat should disappear. It does not solve permissions or validation for you; the apps carry presentation constraints, and the real guarantees still have to live in the capability. And it does not make the model a validator. It makes the model a chooser.

The defensible version is narrower and more interesting than "the AI builds the app." For interactions whose shape can be described declaratively, a small set of trusted primitives can give a model substantial runtime flexibility without handing it responsibility for implementation.

## The visual half of the same argument

This sits directly on top of the capabilities argument. [Scale capabilities before you scale agents](https://davidgolverdingen.nl/en/insights/scale-capabilities-before-you-scale-agents) answered what the model should be able to reach, and [the case that most MCP servers are semantically empty](https://davidgolverdingen.nl/en/talks/most-mcp-servers-are-empty) answered why access without meaning is not enough. Composed UI answers the next question. Once the model can reach a capability, who decides how that capability appears to the person using it?

The answer is the same shape as before. Encode the stable knowledge, bind it to the capability, let the model reason over it, and keep the deterministic guarantees in deterministic software. It gives a useful layering:

```text
SYSTEM OF RECORD
      ↓
CAPABILITY        data + action + meaning
      ↓
MODEL             intent + reasoning + selection
      ↓
PRIMITIVE         form | table | chart | map
      ↓
HUMAN
```

The model does not become the application. It becomes the composer between intent, capabilities and trusted primitives. It can decide a Sankey diagram is the right representation without implementing a Sankey renderer. It can decide which fields a maintenance request needs without being the final word on what may be written. It can decide that location is part of the answer without building a mapping library.

We used to design the page first and ask the user to navigate toward their intent. An agent can reverse that. Understand the intent, reach the right capability, then choose the interaction that fits the task. The model does not need to generate the application to do that. It needs a grammar of trusted primitives it can compose. Give the model freedom over the decision, and keep the guarantees in the software.

---

This post is one piece of a longer argument. [*Production MCP: A Practitioner's Guide*](https://davidgolverdingen.nl/en/insights/production-mcp-practitioners-guide) puts all of them in order, from understanding your data through to identity-bound deployment.

Go deeper: read the full practitioner report, [*The Missing Layer*](https://davidgolverdingen.nl/en/the-missing-layer), or explore the [mcp-metadata-demo](https://github.com/DaveGold/mcp-metadata-demo) server, an open-source extract of these patterns, since the production servers run on private business data.
