@canonmsg/rich-cards
v0.8.1
Published
Canon rich runtime card schema, builders, CLI, examples, and bundled agent skill
Readme
Canon Rich Cards
Schema, strict authoring validation, examples, bundled Skill, and canon-card CLI for canon.card.v1 runtime cards.
Canon rich cards are declarative JSON documents that Canon renders natively in chat. They are for structured reports, summaries, tables, charts, action buttons, and small action-bound forms. Canon renders and routes responses; the runtime decides what each action means and enforces the result.
Install
npm install @canonmsg/rich-cards @canonmsg/coreThe package also exposes the canon-card CLI:
canon-card validate examples/report.card.json
canon-card preview examples/report.card.json
canon-card request <conversationId> examples/report.card.json --waitRun canon-card setup to install the bundled canon-rich-cards Codex Skill into $CODEX_HOME/skills.
Card shape
{
"schema": "canon.card.v1",
"title": "Review draft",
"fallbackText": "Review draft: approve or request changes.",
"blocks": [
{ "kind": "summary", "text": "The runtime prepared a draft and needs a decision." },
{
"kind": "actions",
"actions": [
{ "id": "approve", "label": "Approve", "tone": "positive" },
{
"id": "revise",
"label": "Request changes",
"fields": [
{ "id": "note", "label": "What should change?", "type": "textarea", "required": true }
]
}
]
}
]
}Supported block kinds: summary, metricGrid, chart, table, list, callout, mediaPreview, details, and actions.
Supported action field types: text, textarea, select, multiSelect, boolean, date, number, currency, searchSelect, and lineItems.
Authoring limits enforced by this package include 24 blocks, 12 total actions, 8 fields per action, 12 choices per field (100 for searchSelect), 32 rows per details block, 8 columns and 50 submitted rows per lineItems field, and one actions block per card. The backend accepts a more lenient compatibility envelope, but strict validation is the developer-facing contract for predictable rendering.
Document review and richer fields
Two display-only block kinds and five extended field types support inline document review (the AP invoice-review pattern, but the primitives are generic):
| Block kind | Use |
|---|---|
| mediaPreview | Inline image, or a PDF/file thumbnail plus a safe "open" link. media.url and media.thumbnailUrl MUST be Canon Storage URLs — upload the document with POST /media/upload first and use the returned URL. media.mimeType must be image/* or application/pdf (allowlist enforced server-side). Always set the block fallbackText (and the card-level fallbackText) so clients without the block still get the document name and open link. media also accepts fileName, page (1-based), and pageCount. |
| details | Read-only label/value rows for extracted fields. Each row may carry tone, confidence (0–1), and hint (e.g. "OCR low confidence"). Up to 32 rows. |
| Field type | Value shape | Constraints |
|---|---|---|
| date | YYYY-MM-DD string | min/max (also YYYY-MM-DD strings) |
| number | finite number | min, max, step, precision (max decimal places) |
| currency | finite number | currencyCode (ISO-4217 display metadata), precision (default 2), min, max |
| searchSelect | choice value string | filterable single-select; same choices as select, cap raised to 100; degrades to select on older clients |
| lineItems | [{ colId: cell, ... }, ...] | columns (each text/number/currency/date/select, with required and per-type constraints), minRows, maxRows (cap 50, up to 8 columns) |
A required correction/rejection reason is not a new field type — use a textarea field with required: true on the reject/correct action.
These primitives are additive. On clients that do not yet recognize them, an unknown block degrades to its fallbackText and an unknown field type degrades to a plain text input; the approve/reject path never depends on a new block or field, so it always works. See references/examples.md in the bundled skill for a full worked AP invoice-review card and a small example per block/field.
Builder methods
The fluent card(...) builder exposes .mediaPreview(media, { title?, fallbackText? }) and .details(rows, title?) alongside the existing .summary(), .metricGrid(), .chart(), .table(), .list(), .callout(), and .actions():
import { card } from '@canonmsg/rich-cards';
const built = card('Invoice 4471 — review', 'Invoice 4471 from Acme — open the PDF to review.')
.mediaPreview(
{
kind: 'file',
mimeType: 'application/pdf',
url: canonStorageUrl, // from POST /media/upload
thumbnailUrl: thumbStorageUrl,
fileName: 'ACME-4471.pdf',
},
{ title: 'Source invoice', fallbackText: 'ACME-4471.pdf — open to review.' },
)
.details([
{ label: 'Supplier', value: 'Acme Ltd', confidence: 0.98 },
{ label: 'Total', value: '$12,480.00', tone: 'warning', confidence: 0.71, hint: 'OCR low confidence' },
], 'Extracted fields')
.actions([
{ id: 'approve', label: 'Approve', tone: 'positive' },
{
id: 'correct',
label: 'Submit corrections',
fields: [
{ id: 'total', label: 'Total', type: 'currency', currencyCode: 'USD', min: 0 },
{
id: 'lines',
label: 'Line items',
type: 'lineItems',
columns: [
{ id: 'desc', label: 'Description', type: 'text', required: true },
{ id: 'qty', label: 'Qty', type: 'number', min: 0 },
{ id: 'amount', label: 'Amount', type: 'currency', currencyCode: 'USD' },
],
},
{ id: 'note', label: 'Reason for correction', type: 'textarea', required: true },
],
},
{ id: 'reject', label: 'Reject', tone: 'danger', fields: [{ id: 'reason', label: 'Reason', type: 'textarea', required: true }] },
], 'Decision')
.build(); // strictly validated; throws on an invalid cardThe backend also enforces runtime boundaries such as card size, responder identity, expiry, JSON safety, and pending-state consumption. Do not put secrets, tokens, passwords, or hidden raw tool output in cards.
SDK example
import { CanonAgent } from '@canonmsg/agent-sdk';
const agent = new CanonAgent({ apiKey: process.env.CANON_API_KEY! });
agent.on('message', async ({ requestCard, replyFinal }) => {
const result = await requestCard({
card: {
schema: 'canon.card.v1',
title: 'Review draft',
fallbackText: 'Review draft: approve or request changes.',
blocks: [
{ kind: 'summary', text: 'The runtime prepared a draft and needs a decision.' },
{
kind: 'actions',
actions: [
{ id: 'approve', label: 'Approve', tone: 'positive' },
{
id: 'revise',
label: 'Request changes',
fields: [
{ id: 'note', label: 'What should change?', type: 'textarea', required: true },
],
},
],
},
],
},
});
if (result.status === 'submitted' && result.actionId === 'revise') {
await replyFinal(`Revision requested: ${String(result.values?.note ?? '')}`);
return;
}
await replyFinal(result.status === 'submitted' ? 'Approved.' : 'No decision received.');
});Raw REST / CLI flow
Raw REST/SSE runtimes use Canon's runtime-card endpoints:
- Validate the card locally with
canon-card validate card.json. - Send it with
POST /runtime-card/request. - Poll or consume the answer with
POST /runtime-card/consume. - Switch on
actionIdand read action-fieldvalues.
The CLI wraps that flow when Canon agent credentials are available:
canon-card request <conversationId> card.json --waitAction-less cards are display-only. There is no pending response to consume.
Cards vs runtime input vs approval
- Use rich cards for structured display, action buttons, and small action-bound forms. Responses return
{ actionId, values }. - Use runtime input for standalone clarification, sudo, and secret prompts. Structured question responses return
answers. - Use runtime approval for allow/deny decisions before a tool or action.
Card values are ephemeral runtime-control data. They are not written into visible chat text or card metadata. If a value is truly secret, use the runtime-input secret path instead of an action field.
