Using cards
- Inputs update instantly. Moving a slider or changing a number recomputes the card on your device. Nothing is sent to the model.
- Buttons and numbered choices send their label to the assistant as your next message.
- What you set is remembered. Reopen the chat and the card is as you left it, on any device you’re signed in on: what you set is saved with the message. The device you used keeps a copy too, so it also works offline and on older servers.
- A card that can’t be drawn still says what it meant. If part of a card can’t be shown, the rest still renders and a short note lists what was left out. If nothing can be drawn, the card’s text appears instead (its title, numbers, rows and options), never a blank box or raw code.
- Live data comes from your connected apps (Settings → Connectors). A card that reads data loads it when it opens. A card that changes data only does so when you press its button, and shows an Allow bar with the exact tool and arguments first. Nothing changes until you approve. Cancel leaves everything as it was.
- Numbers say where they come from. Every metric carries a note: your input, an estimate, or a named source.
Turning cards off
Settings → Response style → Layout and visuals:
The setting is saved on each device, like the Memory switches. On phones it’s in the same Settings section.
In Slack, Telegram, email and other channels
Channels can’t show cards, so a card in a bot’s reply is sent there as readable text: the title, numbers (worked out from the card’s starting values), table rows, chart values, steps, and numbered options with “Reply with a number or the option.” Voice calls read the card aloud the same way. If a card can’t be read, it becomes one line saying to open the reply in Allternit. An inline app always becomes one line naming it. The raw card code and app source are never posted. This covers Slack, Discord, Telegram, WhatsApp, Teams, SMS, email and voice (allternit-apiopenui_text.rs).
For developers
Source:allternit-ai/src/lib/openui/:
Inline apps:
allternit-ai/src/lib/apps/inline-app.ts (document, theme variables, manual) and src/components/ai-elements/InlineAppFrame.tsx (the frame, previews and watchdog). Server side: allternit-api openui_text.rs (card text, check_reply/check_card), card_meta.rs (checks and card state per message, migration V251__message_card_meta.sql) and the card_check frame in the chat bridge (v1_routes.rs).
Built on OpenUI (@openuidev/react-lang, MIT).
Format
The model writes OpenUI Lang in a fenced block taggedopenui (openui-lang is accepted too), anywhere in its markdown:
root = … line has arrived, the card’s shape holds its place. Unknown components and broken lines are skipped; the rest of the card still renders.
Live data: Query and Mutation
When the user has connectors, the manual lists their tools as<connector>__<tool> (for example crm__list_deals), read-only tools first, at most 24. A card can then use them:
Query(tool, args, defaults, refreshSeconds?)runs when the card opens and again when a$variablein its args changes. Built-ins such as@Count,@Filter,@Sortand@Sumwork on its results.Mutation(tool, args)runs only from aButtonaction (Action([@Run(m), @Run(q), @Set($x, v), @Reset($x)])).- Calls go through the MCP Apps host bridge (
POST /api/mcp/apps,tools/call) with the user’s own connector credentials, never to a provider directly. The model never sees the results; they go straight into the card.
card-tools.ts, not only in the prompt:
- A
Query()may only use a tool its connector marks read-only (readOnlyHint). Any other tool is refused before it runs. - A tool that changes something must be declared as a
Mutation()in the card. Every call shows the Allow bar with the tool and its arguments, whatever the connector’s install permission mode. - The install permission mode still applies to reads: Always ask asks before reads too.
Table rows may be arrays (in header order) or objects (cells picked by header, ignoring case, spaces and underscores), so a Query result’s rows can be passed straight in.
Design
Cards follow common inline-card guidelines for chat apps, on Allternit’s neutral polish tokens: system type within body and body-small sizes, a title, at most two actions in the footer, no scrolling inside the card, WCAG AA contrast, light and dark themes, and 44 px touch targets on phones. Each card has two zones: a controls panel (a Section holding inputs, shown as a soft inset with white fields) and the result, where the first Metric in a row is shown large as the answer and the rest are listed beside it. Number fields show grouped values (10,000) and whole currency amounts drop the cents. Result numbers animate only right after you change an input; the first paint and data refreshes show the value at once. While a card streams in, each new component slides in, text fades in, bars grow, lines draw and the card’s height eases to fit, with a mono working beam after 1.5 s; a card loaded from history appears at once. Nothing animates with reduced motion on. Spacing, radius and chart colours are design tokens (gap: "xs" | "sm" | "md" | "lg", --iu-series-1…6), so the model never writes pixels or colours. The manual asks for one short sentence before the card, and tells the model not to repeat the card’s numbers in its text.
What runs where
- On the page (deterministic):
$namestate, expressions (+ - * /, comparisons,?:) and@built-ins are evaluated by the OpenUI runtime. Noeval, no network, no model call. - Through the user’s connectors:
Query()andMutation(), under the rules above. - Back to the model: a
Buttonwithout an action, andChoice, send their label as the next user message. - Links open only when they are
http(s);ImageandLinkcomponents takehttpsURLs only, and images load with no referrer. No other card code runs. - Inline apps (below) are the one place model-written code runs, and only in the sandbox.
Server check
At the end of every chat turn, allternit-api checks each card the reply wrote against the catalog (openui_catalog.json, a copy of the app’s catalog.json): unknown components, too many or too few values, names used but never defined, a missing root, lines that can’t be read. It sends the result to the app as a card_check frame (per card: its key, the diagnostics and the card as text) and stores it with the message, so a reload shows the same.
What the user sets on a card is saved with the message through PUT /api/v1/agent-sessions/:id/messages/:messageId/card-state (merged per card, at most 32 cards and 16 KB per card), and comes back with the history from GET /api/v1/agent-sessions/:id/card-meta. Rows belong to the user who wrote them.
Inline apps
When a card’s components can’t do it, the model writes a fenced block taggedapp with a title:
- It runs inline in the reply through the MCP sandbox proxy: another site (so another process under site isolation), an opaque origin,
connect-src 'none', scripts and styles only inline or from the library CDNs pages may use. - It gets the app’s theme as CSS variables (
--viz-text,--viz-text-2,--viz-text-3,--viz-card,--viz-panel,--viz-panel-2,--viz-border,--viz-border-strong,--viz-accent, the status colours,--font-sans,--font-mono, and the MCP Apps--color-*names), updated live when you switch light or dark. HTML artifacts that use these variables follow the theme in the artifact window too. - The frame grows with the app. While the reply streams, its markup and styles show as a preview with scripts removed; the app starts once the fence closes.
- A heartbeat watchdog closes an app that stops responding (an endless loop) and offers Restart. It only judges an app that’s on screen in a visible tab.
- Model-made JSX previews and the older
sandboxreply parts run the same way, never in the app’s own page.
Components
Model instructions
The app, not gizzi-code, sends the manual. On every chat turn it buildsinteractiveAnswersPrompt({ tools }) and adds it to the request’s systemPrompt (the /api/agent-chat and Desktop /api/v1/ai/chat bridges) or, for direct gizzi sessions, to the message’s system field with the + prefix, which appends to gizzi’s own system prompt instead of replacing it. Because of this:
- Each build only asks for the components it can render, and older Desktop builds never get instructions they can’t show.
- The terminal (
gizziTUI) and channels never get the manual. - Text only sends nothing (no card manual and no inline-apps manual).
- With the same tools, the text is byte-identical turn to turn, so it stays in the provider’s prompt cache. A send waits at most 1.5 s for the connector tool list, then goes without tools.
Design mode
Design mode’s preview tab shows the latest card in the reply, re-skinned with the project’s DESIGN.md:background, text, primary and fontFamily set the card’s surface, text and accent colours and its font. Only tokens DESIGN.md actually names are applied.
Privacy
OpenUI’s renderer runs entirely in the app. Its observability stream is turned off (publishObservability={false}), and its runtime telemetry is opt-in and never runs in a browser. Its package install script is blocked by pnpm’s build-script allow-list, so it doesn’t run either.