Cards & approvals
How your app asks the user for things — approval cards, form cards from MCP elicitation, and result cards — and the rule that anything with consequences waits for a tap.
A card is a small piece of UI Chataway draws on your app's behalf: an approval, a form, a gallery of results, a notice. Chataway renders every card itself, in its own design, with your app's icon and name on it — so the user always knows who is asking, and no app can fake another app's (or Chataway's) prompt.
#The one rule: consequences need a gesture
Anything that can't be undone by turning your app off — spending money, uploading, posting, sending, deleting something on your service — happens only after the user taps a button on a card or in your panel. The agent can propose; only the user can approve.
You get this with one line of manifest: confirm on the tool. The review team checks that tools with side effects have it (see Review guidelines).
| Your tool… | Needs confirm? |
|---|---|
| Searches, lists, reads, previews | No |
| Creates a draft only the user can see | Usually no — say "draft" in the description |
| Uploads, publishes, sends, posts, deletes | Yes |
| Spends money or credits | Yes, and say how much in body |
#Approval cards (confirm)
{
"id": "publish_post",
"name": "Publish post",
"description": "Publish a draft post to the user's blog.",
"confirm": {
"title": "Publish this post to your blog?",
"body": "It will be public immediately and emailed to 1,204 subscribers.",
"action": "Publish"
}
}| Field | |
|---|---|
title | Required, ≤ 120 characters. {{count}} is replaced with the number of files handed over. |
body | Optional Markdown under the title. Say what will happen, where, and what it costs. |
action | Label of the approve button (default Continue). Use a verb: Upload, Publish, Send. |
The card blocks the tool call. Approve → the call proceeds to your server. Cancel → your server is never called and the agent gets "The user did not approve … — nothing was sent". For a tool with file params, the card shows thumbnails of the (already prepared) files; {{count}} works in body too.
#Forms (elicitation)
When you need an answer you can't get from the arguments, ask for it with MCP elicitation from inside your tool handler. Chataway renders the request as a form card; the call continues when the user submits.
app.tool('import_contacts', { input: { csv: fileParam() } }, async ({ csv }, ctx) => {
const answer = await ctx.elicit('Which workspace should the contacts go into?', {
type: 'object',
properties: {
workspace: { type: 'string', title: 'Workspace', enum: ['sales', 'support', 'ops'], enumNames: ['Sales', 'Support', 'Ops'] },
tag: { type: 'string', title: 'Tag (optional)' },
notify: { type: 'boolean', title: 'Notify the team', default: false },
},
required: ['workspace'],
});
if (answer.action !== 'accept') return 'The user cancelled the import.';
// answer.content = { workspace: 'sales', tag: '', notify: false }
// …
});ctx.elicit is the server helper; with the raw MCP SDK, send an elicitation/create request from your handler.
How the schema maps to form fields:
| JSON Schema | Form field |
|---|---|
string | Text field (description becomes the placeholder when there's also a title) |
string with enum (or oneOf of consts) | Select; labels from enumNames (or each option's title) |
number / integer | Number field (minimum / maximum enforced) |
boolean | Switch |
required | Required fields must be filled before Submit |
title | Field label (falls back to description, then the key) |
default | Pre-filled value |
Only flat objects of those types are supported — no nested objects or arrays, per the MCP elicitation spec. The card's buttons are Send and Decline; the message is its title (or its body, when longer than 120 characters). The result is { action: 'accept', content } (values coerced to the schema's types), { action: 'decline' }, or { action: 'cancel' } if the card was dismissed or timed out.
#Result cards (_meta)
A tool result can ask Chataway to show a card next to it by setting _meta["chataway/card"]. Two kinds are available to cloud apps:
// A notice: Markdown text with a title.
_meta: { 'chataway/card': { kind: 'notice', title: 'Deployed', body: 'Preview: https://preview.example.com/p/42' } }
// A gallery: your own title and text for the files this result returned.
_meta: { 'chataway/card': { kind: 'gallery', title: 'Your stickers', body: 'Die-cut, 1024 px, transparent.' } }| Field | |
|---|---|
kind | notice or gallery |
title | Short headline |
body | Markdown |
items | Accepted by the SDK types but not rendered yet — a gallery card shows the files your result returned. |
Result cards never block. Files you return are always shown as a gallery — titled "{App}: 3 files" — with Add to Media library and Save to project buttons; a gallery _meta card just gives that gallery your own title and text. To show images you host without returning them, return resource_links on your domains (Chataway downloads them) or put links in a notice.
#Connect and consent cards
Two cards are raised by Chataway itself, on your behalf:
- Connect — a tool that declares
accountwas called but the account isn't connected (or its refresh failed). The card has your icon and a Connect {label} button that starts the OAuth flow. The tool call waits. - File hand-off consent — the first time files move from one owner to another in a project. See Files & hand-off.
#Where cards appear
| Chat | Cards |
|---|---|
| Chataway chat | Inline, on the tool call that raised them |
| Claude Code, Codex, Grok, Cursor | In the pending-asks strip above the composer |
| Any | In your panel, if it lists them with the bridge |
On the phone, cards appear the same way and can be answered there — useful for approving a long-running agent's upload from the couch.
#Answering cards from your panel
Your panel can list your app's cards (listCards), watch for new ones (on('card')) and answer them (respondCard) — for example a queue view of pending uploads. The panel can only see and answer your cards; answering from the panel is a user gesture by definition, because it happened in your UI.
#Writing good cards
- Say what will happen, not what the tool is called. "Upload 3 images to Acme?" beats "Run upload_images?".
- Put consequences in the body. Public or private, reversible or not, how much it costs.
- One card per decision. Don't chain confirm → form → confirm; ask everything in one elicitation, or make the arguments carry it.
- Use verbs on buttons. Upload, Publish, Send.