Build
Cards and views
An agent's answer can show what it found instead of writing it all out: mail, a week's meetings, a table of invoices, a chart of signups. There are four ways, from the simplest to the most capable:
| What | Who makes it | What it is | Can people use it? |
|---|---|---|---|
| Cards from agents | The agent, as it answers | Typed data, drawn by the workspace | Read; links open; actions buttons ask the agent something |
| Cards from apps | Your app's action | The same cards | The same |
| Apps' own views | Your app: a page on your origin | Your page, framed under the answer | Fully: it can call your actions that only read, open links, ask the agent |
| Connectors' views | An MCP server's tool (MCP Apps) | The server's page, sandboxed in an origin of its own | The same, with its own server's tools |
Cards come first: they look like the rest of the workspace, in both themes, at any width, and cost you nothing to draw. Reach for a view when what you found needs to be used, not just read: opening a thread in place, moving a calendar a week on, filtering a list.
Wherever cards and views can't be drawn (a messaging app, search, notifications, another agent reading the conversation), they come to words. Cards are turned into lines of text; for a view, the agent's own words are what's sent, so it always says what stands out.
Cards from agents
Every agent can put cards under its answer, with its tabs_and_spaces_show_cards tool, and is told to show rather than list: several emails, events, figures or rows, or numbers that change over time, go in cards, and the agent writes only what stands out in a sentence or two. You don't need to do anything for this. It's how the workspace's own agents answer, and how yours will once they run there.
Cards are checked against the rules below. When an agent gets one wrong, it's told what's wrong and tries again. An answer shows at most 8 cards.
Card types
Every card has a type, and may have a title.
| Card | Its fields (? optional) |
|---|---|
emails | items: from, subject, snippet?, at?, url?, unread?, important?, note? (up to 20) |
events | items: title, start, end?, where?, people?, url?, join? (a call's link), status? (accepted, tentative, declined, needs_answer) |
metrics | items: label, value (text), detail?, tone? (good, warning, bad, neutral) (up to 6) |
table | columns (up to 8), rows: one cell per column, text, numbers or null (up to 50) |
list | items: title, detail?, meta? (on the side), url?, tone? (up to 30) |
links | items: label, url, detail? (up to 12) |
note | text, tone? (info, success, warning, danger) |
bars | items: label, value (a number), and unit? for the card (up to 12): one number each |
chart | kind? (line, bar, stacked), labels, series: name, values, and unit?. See Charts. |
actions | items: label, prompt: buttons that send prompt to the agent from whoever clicks (up to 4) |
Times are ISO (2030-01-08T14:00:00Z; without an offset, the asking person's time zone) or a date for all day (2030-01-08). Links are http(s) only and open in a new tab. Cards that don't fit these rules are left out, and the agent reads the words alone.
Charts
A chart shows several numbers per label, or how numbers change: a line for a trend over time (the default), bar for bars side by side, stacked for parts of a whole.
{
"type": "chart",
"kind": "line",
"title": "Signups, last week",
"labels": ["2030-01-06", "2030-01-07", "2030-01-08", "2030-01-09"],
"series": [
{ "name": "Web", "values": [12, 18, null, 30] },
{ "name": "App", "values": [4, 6, 9, 11] }
]
}labelsrun along the x axis: dates (2030-01-08), months (2030-01) or times, shown in the reader's language, or names, shown as written. Up to 60, or 24 for bars.series: up to 6, each with one value per label;nullwhere there's none (a line breaks there).unitfollows each value:h,%,€.- One y axis. Bars start at zero; stacked bars can't go below it. Two measures of different sizes (revenue and orders, say) are two charts, not one.
The workspace draws it: a legend for two or more series, each series in its own colour (always in the same order), a readout of every series where you point or move with the arrow keys, and a button to see the same numbers as a table. For one number per thing, a bars card is simpler.
Cards from apps
An action that changes nothing answers with cards beside its result; they're drawn under the agent's answer, so the agent doesn't retype them. An action that changes something answers with cards too, shown on the approval once the person has said yes. See Showing what you found.
const answer: ActionAnswer = {
result: "Orders rose 40% this fortnight, led by the web shop.",
cards: [
{
type: "chart",
title: "Orders",
labels: ["2030-01-01", "2030-01-08", "2030-01-15"],
series: [
{ name: "Web shop", values: [42, 64, 88] },
{ name: "Wholesale", values: [18, 29, 40] },
],
},
],
};The SDK's ActionCard type is every card above.
Apps' own views
When cards aren't enough, an action that changes nothing can bring its own page, declared as its view in your manifest. The workspace frames it from your origin under the agent's answer, tells it what your action was asked and answered (its data), and lets it call your other reading actions as whoever is looking. The SDK's connectView() does your page's side, and tands dev shows it as a workspace would. See Your action's own view.
Connectors' views (MCP Apps)
Connectors are MCP servers a workspace adds for its agents. A server that speaks the MCP Apps extension (spec 2026-01-26) can show a view under the agent's answer when one of its tools is used. A view written for any MCP Apps host works here.
Declaring it. The workspace says it supports the extension (io.modelcontextprotocol/ui, text/html;profile=mcp-app) when it connects. A tool names its view in _meta.ui.resourceUri (the older _meta["ui/resourceUri"] works too), a ui:// resource whose content has the MIME type text/html;profile=mcp-app, up to 2 MiB. The resource's _meta.ui.csp lists the origins it needs (connectDomains, resourceDomains, frameDomains, baseUriDomains), and _meta.ui.prefersBorder says whether to frame it. A tool with _meta.ui.visibility: ["app"] is for its views only: agents don't see it.
When it's shown. When an agent uses a tool that only reads (readOnlyHint: true, or one the workspace's admins marked as reading) and that has a view, the view goes under its answer, with what the tool was asked and answered (up to 64 KB). At most 3 views show in an answer, counting apps' views.
What it can do. The workspace is a host: it answers ui/initialize with the theme, language, time zone and the workspace's colours (as styles.variables), then sends the tool's input and result, and follows theme changes. The view may:
| It may | How |
|---|---|
| Call its server's tools | tools/call: only tools that only read and are turned on, as whoever is looking (their own account, for connectors each person signs in to), 30 a minute, on the audit trail |
| Open a link | ui/open-link with an http(s) address: a new tab |
| Ask the agent | ui/message with text, as whoever is looking, once they've clicked in the view, and only where they may write |
| Fit its content | ui/notifications/size-changed with its height (up to 720 px) |
Its sandbox. The page is served by the workspace with a content security policy that gives it an origin of its own: no cookies, storage or page of the workspace, nothing but the origins it declared, no forms posted anywhere. Its server learns only what it already knew: the tool calls it answers.
Not yet. Views show inline only (no fullscreen or picture-in-picture). ui/update-model-context, resources/read and ui/download-file from a view, sampling, and calling tools that change something aren't supported; a view can ask the agent instead, which asks the person first. Views aren't shown in approvals of connector tools, in a recurring task's posts, or in messaging apps.