Skip to documentation
Documentation

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_cards tool. 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 story template. A dark 690×920 card with an optional photo, a headline, body text and a slide counter. Its input schema is title and body (strings), slideNumber and totalSlides (numbers), and an optional photoUrl (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_text as 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:

SecretRequiredPurpose
RENDER_SERVICE_URLYes (or platform default)The rendering service endpoint. Save it as a developer secret to bring your own; platform deployments provide one otherwise.
RENDER_SERVICE_SECRETNoSent 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:

ChannelDelivery
iMessage and SMSOne media message; 4+ cards render as a swipeable stack on iMessage.
TelegramOne 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:

JSON
{
  "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 via photoUrl when 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:

  1. 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.
  2. At most once per turn.
  3. Always provide fallback_text.
  4. Trust the result, not the intent. The tool returns a JSON outcome:
JSON
{
  "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:

JSON with Comments
{
  "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.