Rich cards
Render swipeable card carousels into one media message, and wire the agent into them.
Rich cards turn a multi-beat story into a sequence of rendered images the agent delivers as
one message. On iMessage, four or more cards arrive as a swipeable stack; on Telegram, as
an album. Cards come from the official cards capability — the same one the Orchid agent
template installs — and any agent can use them by installing that capability and pointing it
at a rendering service.
What is available
The cards capability (publisher official, version 1.0.0, runtime render-template)
ships with:
- The
render_cardstool. The agent passes an ordered list of slides, a short caption, and a plain-text fallback. Each slide renders to a PNG through the shared UI rendering service, and all of them are delivered as a single media message. - The
storytemplate. A dark 690×920 card with an optional photo, a headline, body text and a slide counter. Its input schema istitleandbody(strings),slideNumberandtotalSlides(numbers), and an optionalphotoUrl(http(s) image URL; the rendering service pre-resolves it onto the card). - Automatic text fallback. If rendering fails, delivery fails, or the conversation is on
a channel that cannot carry media, the platform sends
fallback_textas ordinary text bubbles instead. The user never receives a broken carousel.
The capability also adds a paragraph of system-prompt guidance while installed, so a capable
model uses render_cards correctly without extra wiring from you.
Enabling cards
Two things must be true before render_cards appears on an agent.
1. Install the cards capability on the agent. Use the agent's Capabilities dialog in the
web app, or create the agent from the Orchid template — Orchid installs cards (alongside
its own capability) out of the box, and its daily-digest loop delivers the morning briefing as
a card carousel when the tool is available.
2. Configure a rendering service. The capability calls the shared UI rendering service, which needs an endpoint before any turn can use it:
| Secret | Required | Purpose |
|---|---|---|
RENDER_SERVICE_URL | Yes (or platform default) | The rendering service endpoint. Save it as a developer secret to bring your own; platform deployments provide one otherwise. |
RENDER_SERVICE_SECRET | No | Sent as a Bearer token to the rendering service. |
If RENDER_SERVICE_URL resolves to nothing, the capability is skipped for that turn —
the tool is not offered to the model and turns proceed without it.
Calls through your own endpoint use your own secret — the platform's secret is only ever sent to the platform's own endpoint, and your endpoint must resolve to a public address. When the platform falls back to its
shared instance, the render consumes one unit from the org's plan, and each rendered slide is
recorded as native usage (provider render, feature card-render). Requests are namespaced
per tenant — a namespace derived from your API key, never the key itself — so a shared
rendering service keeps tenants apart.
Channel behavior
Cards are images, so they only go where the transport carries media as media:
| Channel | Delivery |
|---|---|
| iMessage and SMS | One media message; 4+ cards render as a swipeable stack on iMessage. |
| Telegram | One message as an album. |
| Every other channel (WhatsApp, Slack, email, …) | Only fallback_text is sent, through the channel's native sender. |
Agent integration
render_cards has one invocation contract. The agent supplies:
{
"slides": [
{
"title": "Flight options",
"body": "Two nonstops under $400, both landing mid-afternoon.",
"slideNumber": 1,
"totalSlides": 5,
"photoUrl": "https://example.com/cards/flights.jpg"
}
],
"caption": "Your trip, in five cards",
"fallback_text": "The full carousel content as plain text, delivered if rendering fails."
}slides— 1 to 10 cards in order; each renders as one image. Use 4–6: 4 is the minimum for iMessage to show a swipeable stack rather than a static collage. Lead each slide with the bottom line and keep it to about two sentences. Put an image on every slide viaphotoUrlwhen a relevant one exists.caption— optional short text sent alongside the carousel message.fallback_text— required in practice. It must carry the same content as plain text, because it is what the user receives whenever rendering or delivery fails.
Rules the built-in prompt guidance enforces, and that custom prompts should keep:
- Reserve it for stories. A structured briefing, a research roundup, a trip itinerary, a launch recap — a swipeable visual sequence must genuinely beat text. Never for short answers, single facts, confirmations, reminders, or links.
- At most once per turn.
- Always provide
fallback_text. - Trust the result, not the intent. The tool returns a JSON outcome:
{
"success": true,
"cards_sent": 5,
"cards_delivered_as_media": true,
"message_id": "…",
"rendered_dimensions": [{ "slide": 1, "width": 690, "height": 920 }, …]
}rendered_dimensions echoes the intrinsic pixel size of each PNG as reported by the rendering
service, so a card that arrives cropped or letterboxed can be diagnosed from the tool result.
success: false with fell_back_to_text: true (and a render_error) means no images reached
the user — only the fallback text did, and it was already sent. The agent must never say a
carousel went through unless cards_delivered_as_media is true.
Custom card designs
The story template is one template among many. A capability with the render-template
runtime declares its own templates as TSX sources built from the @message-ui/components
primitives (Attachment, Text, Image, …), each with an inputSchema describing the props
the agent may pass per slide:
{
"slug": "menu-cards",
"runtime": "render-template",
"templates": [
{
"name": "dish",
"source": "import React from 'react';\nimport { Attachment, Text } from '@message-ui/components';\n// …a component whose root Attachment declares its own pixel width/height (690×920 when omitted)…",
"inputSchema": {
"fields": [
{ "name": "dish", "type": "string", "required": true },
{ "name": "price", "type": "string", "required": true }
]
}
}
],
"tools": [
{
"name": "render_menu",
"description": "…",
"inputSchema": {
"type": "object",
"properties": {
"slides": {
"type": "array",
"items": {
"type": "object",
"properties": {
"dish": { "type": "string" },
"price": { "type": "string" }
},
"required": ["dish", "price"]
}
},
"fallback_text": { "type": "string" }
},
"required": ["slides"]
},
"run": { "kind": "render-template", "template": "dish" }
}
]
}The runtime reads a slides array and a fallback_text string from the tool's arguments
(textFallbackArg names the fallback argument if you call it something else). Each slide is
rendered through a one-off inline-render call to the service (POST /t/<namespace>/render with the template source and props), with a 60-second timeout per
slide; slides render concurrently and keep their order. See
Capabilities for the full manifest format, and
Governed actions if the cards should carry decisions a person approves.