Documentation

Card Types

The full reference of every card type, with example payloads.

Card Types (48)

TailStats supports 48 card types β€” display primitives for numbers, charts, timers, KPIs, schedules, and rich media, plus a full set of 13 standalone form inputs, recursive group containers, and a chat-style userInput composer designed for AI agent integration.

Basic Types

Type Description Example
string Text display with optional formatting "value": "Hello"
number Numeric with formatting (currency, compact, percent) "value": 1234.56
progress Horizontal progress bar (0-100% or 0-1 range) "value": 0.75
status Auto-colored indicator (success/warning/error/info). Use level to override auto-detection. "value": "Online"
array Sparkline or bar chart visualization "value": [10, 20, 15, 30]

Rich Media Types

image

Display an image from a URL or base64-encoded data. Use title for a caption and subtitle for a description.

// Remote image with caption
{
  "type": "image",
  "value": "https://example.com/dashboard-screenshot.png",
  "title": "Dashboard Overview",
  "subtitle": "Captured at 2:30 PM"
}

// Base64-encoded image
{
  "type": "image",
  "value": "data:image/png;base64,iVBORw0KGgo..."
}

Supports https:// URLs and data:image/png;base64, or data:image/jpeg;base64, data URIs. Images are centered with rounded corners and a max height of 200pt.

message

Chat bubble with sent/received modes. Supports avatars via icon or emoji, sender name via title, and timestamp via subtitle.

// Received message with avatar and sender info
{
  "type": "message",
  "value": "Hey, the deploy just finished πŸš€",
  "displayMode": "received",
  "title": "Alice",
  "subtitle": "2:34 PM",
  "icon": "person.circle.fill",
  "color": "green"
}

// Sent message (right-aligned, no avatar)
{
  "type": "message",
  "value": "Nice! Any errors in the logs?",
  "displayMode": "sent",
  "subtitle": "2:35 PM"
}

// Received with emoji avatar
{
  "type": "message",
  "value": "All clear βœ… Zero errors in the last 10 minutes",
  "displayMode": "received",
  "title": "Alice",
  "subtitle": "2:36 PM",
  "emoji": "πŸ‘©β€πŸ’»"
}

// Image message (image-only)
{
  "type": "message",
  "displayMode": "received",
  "title": "Alice",
  "subtitle": "2:37 PM",
  "icon": "person.circle.fill",
  "image": "https://example.com/photo.jpg"
}

// Image with caption
{
  "type": "message",
  "value": "Look at this dashboard!",
  "displayMode": "sent",
  "subtitle": "2:38 PM",
  "image": "https://example.com/screenshot.png"
}

displayMode: sent (right-aligned, filled bubble) or received (left-aligned with avatar). color overrides the default bubble color (blue for sent, gray for received). image: URL or base64 data URI — renders inline in the bubble, with value as caption text below. URLs in message text are auto-detected and clickable.

map

Interactive MapKit map with customizable markers. Auto-fits the camera to show all markers.

{
  "type": "map",
  "value": {
    "markers": [
      {"lat": 37.7749, "lng": -122.4194, "label": "SF Office", "icon": "building.2", "color": "blue"},
      {"lat": 37.3861, "lng": -122.0839, "label": "Data Center", "icon": "server.rack", "color": "red"}
    ]
  }
}

Marker fields: lat, lng (required), label (tooltip text), icon (SF Symbol, default "mappin"), color (per-marker, or set color on the item for a default).

Advanced Types

timer

Live countdown/elapsed/duration display. Two visual styles: compact (default) or big event-style tiles.

{"target": 1705000000, "mode": "countdown", "format": "auto"}
{"type": "timer", "displayMode": "tiles", "value": {"target": "2026-12-01T09:00:00Z", "mode": "countdown", "pastLabel": "Launched!"}}

Mode (behavior): countdown (to a Unix timestamp or ISO-8601 date), elapsed (since a timestamp), duration (fixed). Format (compact only): auto, hh:mm:ss, mm:ss, days, compact. displayMode: tiles shows big DAYS/HRS/MIN/SEC cards β€” great for event countdowns. Tiles options: units (pick which to show, e.g. ["days","hours"]), hideZeroUnits (collapse leading zero tiles), pastLabel (text shown when elapsed). Live-updates every second.

gauge

Circular arc dial with auto-coloring based on value thresholds

{"value": 75, "min": 0, "max": 100, "unit": "%"}

Fields: value (required), max (default 100), min (default 0), unit (%, GB, ms, etc.). Auto-colors by percentage: green 0-50% → blue 50-75% → orange 75-90% → red 90%+. Use color on the item to override.

link

Clickable URL button that opens in the default browser

{"url": "https://grafana.internal/d/api-latency", "label": "Open Dashboard"}

Fields: url (required), label (display text, defaults to showing the URL). Shows an external link icon and underlines on hover.

list

Vertical item list (max 10 visible). Items with url are clickable.

[{"text": "Item 1", "subtitle": "Details", "icon": "circle", "color": "blue", "url": "https://..."}]

Fields: text (required), subtitle, icon, color, url (or link)

qrcode

Generated QR code from any string, URL, or WiFi config

// URL "value": "https://example.com" // WiFi network (auto-connect on scan) "value": "WIFI:T:WPA;S:MyNetwork;P:secret123;;"

Accepts any text. Use title for a heading and subtitle for a description (auto-shows a preview of the data if no subtitle is set).

code

Scrollable code block with line numbers, language badge, and copy-to-clipboard button

{"source": "func main() {\n fmt.Println(\"hello\")\n}", "language": "go"}

Fields: source (required), language (swift, python, json, javascript, go, etc. — shown as a badge). Displays with line numbers, monospaced font, and a max height of 200pt with scrolling.

log

Scrollable log entries with level coloring, tags, and conversation-style avatars

[{"message": "Started", "level": "info", "timestamp": "12:00:00", "tag": "DEPLOY", "tagColor": "blue", "from": {"id": "CI", "color": "green"}, "to": {"id": "QA", "color": "purple"}}]

Fields: message (required), level (info/warn/error/debug), timestamp, tag, tagColor, from/to avatars: {id, color, avatar}

Dashboard Types

Advanced types for building rich dashboard displays.

statusGroup

Multi-service status display with up/down/degraded indicators.

{
  "type": "statusGroup",
  "value": {
    "services": [
      {"label": "API", "status": "up", "message": "45ms"},
      {"label": "DB", "status": "degraded", "message": "High latency", "url": "https://..."},
      {"label": "Cache", "status": "down"}
    ],
    "showLabels": true,
    "compact": false
  }
}

Status values: up, down, degraded, maintenance, unknown. Also auto-maps: online/healthy/ok/active → up, offline/dead/stopped/error → down, slow/partial/warning → degraded. Service fields: label, status, message, url

avatarGroup

Team presence display with avatars and online status.

{
  "type": "avatarGroup",
  "value": {
    "users": [
      {"name": "Alice", "status": "active", "avatar": "https://..."},
      {"name": "Bob", "status": "away", "initials": "B", "color": "blue"},
      {"name": "Carol", "status": "busy", "role": "On-call"}
    ],
    "maxDisplay": 5,
    "showNames": false,
    "showPresence": true,
    "size": "medium"
  }
}

Status values: active, away, busy, offline

metricDelta

Metric with comparison to previous value, showing change percentage.

{
  "type": "metricDelta",
  "value": {
    "current": 1250,
    "previous": 1100,
    "format": "number:compact",
    "period": "vs last week",
    "showPercent": true,
    "invertColors": false,
    "suffix": "users",
    "sparkline": [1000, 1050, 1100, 1150, 1250]
  }
}

Automatically shows green for increase, red for decrease. Use invertColors: true when decrease is positive (e.g., errors). Use delta or deltaPercent directly instead of previous if you already have the change value.

table

Data table with columns and rows, supporting colored cell values.

{
  "type": "table",
  "value": {
    "columns": [
      {"key": "name", "label": "Name"},
      {"key": "status", "label": "Status", "align": "center"}
    ],
    "rows": [
      {"cells": {"name": "Build", "status": {"value": "Pass", "color": "green", "icon": "checkmark.circle", "badge": true}}},
      {"cells": {"name": "Tests", "status": {"value": "Fail", "color": "red"}}}
    ],
    "showHeader": true,
    "striped": true,
    "compact": false,
    "maxRows": 10
  }
}

checklist

Task checklist with completion tracking, progress bar, and completion badge.

{
  "type": "checklist",
  "value": [
    {"text": "Write tests", "completed": true},
    {"text": "Update docs", "completed": false},
    {"text": "Deploy to staging", "completed": false}
  ]
}

Item fields: text (required), completed (bool). Shows a progress bar and "N/M" completion badge (turns green when all done). Completed items are shown with strikethrough. Max 10 items visible. Use color on the item to customize the progress bar color.

pipeline

CI/CD or task pipeline with stage progress.

{
  "type": "pipeline",
  "value": {
    "tasks": [
      {"id": "build-1", "title": "Build", "status": "completed"},
      {"title": "Test", "status": "running", "progress": 0.6, "meta": "12 agents", "eta": "5m"},
      {"title": "Deploy", "status": "queued", "url": "https://...", "assignees": [{"id": "CI", "color": "green"}]}
    ],
    "showProgress": true,
    "showEta": true,
    "maxDisplay": 10,
    "summary": "Build #42"
  }
}

Status values: queued, running, completed, failed, blocked. Also auto-maps: in_progress/active → running, pending/waiting → queued, done/finished/success → completed

menu

xbar-style menu with title/value pairs and optional icons.

{
  "type": "menu",
  "value": {
    "items": [
      {"title": "CPU", "value": "45%", "color": "green"},
      {"title": "Memory", "value": "8.2 GB", "icon": "memorychip", "href": "https://..."},
      {"title": "---", "isSeparator": true},
      {"title": "Disk", "value": "120 GB free", "icon": "internaldrive", "submenuLevel": 1}
    ],
    "headerItemCount": 1,
    "compact": false
  }
}

headerItemCount controls how many items show in the collapsed menu bar view. Item fields: title, value, icon, color, href (URL), font, size, length (truncate), submenuLevel (0-N), isDisabled, isSeparator, isAlternate, showInMenuBarOnly

stages

Workflow stages visualization with step indicators.

{
  "type": "stages",
  "value": {
    "stages": [
      {"name": "Plan", "status": "completed", "label": "2m"},
      {"name": "Build", "status": "in_progress", "label": "ETA 5m"},
      {"name": "Test", "status": "pending", "icon": "checkmark.circle", "color": "blue"},
      {"name": "Ship", "status": "pending"}
    ],
    "showConnectors": true,
    "showLabels": true,
    "vertical": false,
    "summary": "Release v2.1"
  }
}

Status values: pending, in_progress (pulsing), completed, failed, blocked, skipped, cancelled, warning. Also accepts aliases: active/running/current, done/success/passed, error/failure, waiting/queued, paused, skip/ignored

habit

GitHub-style contribution heatmap grid for habit tracking, with optional streak badge.

{
  "type": "habit",
  "title": "Play Drums",
  "subtitle": "30 min daily",
  "icon": "music.note",
  "color": "green",
  "value": {
    "entries": [
      {"date": "2026-04-09", "value": 1.0},
      {"date": "2026-04-08", "value": 0.75},
      {"date": "2026-04-07", "value": 0.5}
    ],
    "weeks": 20,
    "streak": 5
  }
}

Renders a 7-row (days of week) x N-week grid of colored cells. Entry fields: date (YYYY-MM-DD, required), value (0.0–1.0 intensity). Value fields: entries (array), weeks (grid width, default 20), streak (shown as flame badge), color (overrides card color). Cell intensity is mapped to 4 opacity levels; zero/missing days render as empty cells. Works with card-level and item-level actions.

calendar

Calendar with week or month view. Each day shows a colored ring based on entry status (completed, partial, missed).

{
  "type": "calendar",
  "title": "Meditate in the morning",
  "subtitle": "Every day",
  "icon": "figure.mind.and.body",
  "color": "green",
  "displayMode": "week",
  "value": {
    "entries": [
      {"date": "2026-04-09", "status": "completed"},
      {"date": "2026-04-08", "status": "completed"},
      {"date": "2026-04-07", "status": "partial"},
      {"date": "2026-04-06", "status": "missed"}
    ]
  }
}

Use displayMode: "week" (default) for a compact 7-day strip ending today, or displayMode: "month" for a full month grid with prev/next navigation. Entry status values: completed (card color ring), partial (orange ring), missed (no ring). Optional month and year fields in the value object set the initial month for month view. Works with card-level and item-level actions.

kpi

Executive dashboard tile with hero value, delta %, sparkline, target progress, and optional additions/deletions breakdown.

{
  "type": "kpi",
  "title": "Monthly Recurring Revenue",
  "icon": "dollarsign.circle",
  "color": "green",
  "value": {
    "current": 127450,
    "previous": 117800,
    "format": "currency:USD",
    "periodLabel": "vs last month",
    "sparkline": [110000, 115000, 118000, 121000, 124000, 127450],
    "target": 150000,
    "diff": {
      "additions": 18200,
      "deletions": 7800,
      "additionsLabel": "new",
      "deletionsLabel": "churn"
    }
  }
}

Unifies number, metricDelta, progress, and sparkline into one polished tile. Required: current. Optional: previous (enables delta badge), format (currency:USD, percent, compact), sparkline, target (shows progress bar), diff (additions/deletions breakdown), periodLabel. Supports displayMode: "compact" for smaller tiles.

notification

Inbox of recent notifications with read/unread state, relative timestamps, and per-entry actions. Perfect for mentions, alerts, deploys, CI results.

{
  "type": "notification",
  "title": "Inbox",
  "icon": "bell",
  "color": "blue",
  "value": {
    "maxVisible": 5,
    "entries": [
      {
        "id": "n1",
        "title": "Deploy successful",
        "body": "production.api β€’ v2.8.1",
        "level": "success",
        "timestamp": "2026-04-09T15:58:00Z",
        "read": false,
        "openURL": "https://grafana.internal/..."
      },
      {
        "id": "n2",
        "title": "@sarah mentioned you",
        "body": "#incidents",
        "level": "info",
        "emoji": "πŸ’¬",
        "timestamp": "2026-04-09T15:00:00Z",
        "read": false
      }
    ]
  }
}

Entry fields: id (required, stable), title (required), body, level (success/error/warning/info/pending), icon/emoji, timestamp (ISO-8601 or Unix seconds), read, openURL (tap row to open). Value-level: maxVisible (default 5, extras collapse into "+N more" expander). Auto-sorted newest first. Unread indicator dot + bold title; read entries dim to 70% opacity. Empty state shows "✨ All caught up". Relative timestamps live-update every minute.

quote

Styled quote with serif italic text and author attribution line. Perfect for motivational quotes, daily tips, word of the day.

{
  "type": "quote",
  "title": "Daily Stoic",
  "icon": "quote.bubble",
  "color": "purple",
  "value": {
    "text": "The obstacle is the way.",
    "author": "Marcus Aurelius",
    "source": "Meditations"
  }
}

Required: text. Optional: author, source (book, speech, etc.). Renders with a large opening quotation mark, serif italic body text, and a dash-separated attribution line. Also accepts a bare string as the value for quick use.

feed

Activity/social feed with avatar + author + message + timestamp rows. Perfect for GitHub activity, Slack digest, CRM events, social media feeds.

{
  "type": "feed",
  "title": "Activity Feed",
  "icon": "bubble.left.and.text.bubble.right",
  "color": "#6366F1",
  "value": {
    "maxVisible": 5,
    "compact": false,
    "entries": [
      {
        "id": "1",
        "author": "Sarah Chen",
        "avatar": "https://example.com/sarah.png",
        "message": "Merged PR #247: Add feed card type",
        "body": "into main from feature/feed-card",
        "timestamp": "2026-04-10T14:30:00Z",
        "level": "success",
        "read": false,
        "openURL": "https://github.com/org/repo/pull/247",
        "media": "https://example.com/preview.png"
      }
    ]
  }
}

Each entry requires: id, author, message, timestamp. Optional: avatar (image URL, falls back to initials), body (secondary text), level (success/error/warning/info/pending), icon, emoji, read (boolean), openURL, media (image preview URL), actions (inline action buttons). Set compact: true for minimal rows.

nowPlaying

Audio player widget that streams mp3 / HLS / icecast / internet-radio URLs directly inside the card (macOS). Push a streamUrl and the card renders track metadata + local play/pause + Β±15s skip; live streams auto-detect and swap the scrubber for a pulsing LIVE pill. System volume, no autoplay. On iOS/Android the card still follows the legacy remote-info model with server-pushed position/actions.

{
  "type": "nowPlaying",
  "title": "Radio",
  "value": {
    "title": "BBC Radio 6 Music",
    "artist": "Live",
    "artwork": "https://example.com/bbc6.png",
    "streamUrl": "http://stream.live.vc.bbcmedia.co.uk/bbc_6music",
    "source": "bbc",
    "color": "purple"
  }
}

macOS fields: streamUrl (any URL AVPlayer accepts β€” mp3, HLS, icecast; live streams detected automatically). Optional metadata: title, artist, album, artwork (image URL), source (badge text), color (accent), compact. Deprecated on macOS (still decoded, ignored at render): position, positionTimestamp, isPlaying, actions β€” local AVPlayer owns playback state and controls.

form

Multi-field form as a card. Submit data via HTTP or script without opening a modal. Supports all input types, validation, file upload with drag & drop (macOS), and inline/multi-column layouts.

{
  "type": "form",
  "title": "Report Bug",
  "icon": "ladybug",
  "value": {
    "submitLabel": "Submit",
    "submitStyle": "primary",
    "exec": {
      "type": "http",
      "url": "https://api.example.com/bugs",
      "method": "POST",
      "body": {"title": "${input.title}", "severity": "${input.severity}"}
    },
    "result": {"message": "Bug reported!"},
    "fields": [
      {"id": "title", "label": "Title", "type": "text", "required": true, "validation": {"minLength": 5}},
      {"id": "severity", "label": "Severity", "type": "select", "value": "medium", "options": ["Low", "Medium", "High"]},
      {"id": "description", "label": "Description", "type": "textarea"},
      {"id": "file", "label": "Attachment", "type": "file", "accept": "png,jpg,pdf"}
    ]
  }
}

Each field uses the same types as action inputs (text, textarea, select, number, toggle, file, directory, etc.). Optional: validation ({regex, minLength, maxLength, pattern, message} where pattern is email/url/phone), width (full/half/third for multi-column), layout ("inline" for single-line + button), submitStyle (primary/destructive), submitIcon. File fields send as multipart/form-data when submitted via HTTP. Form resets after successful submit.

form

Multi-field form as a card. Submit data via HTTP or script without opening a modal. Supports all input types, validation, file upload with drag & drop (macOS), and inline/multi-column layouts.

{
  "type": "form",
  "title": "Report Bug",
  "icon": "ladybug",
  "value": {
    "submitLabel": "Submit",
    "submitStyle": "primary",
    "submitIcon": "paperplane.fill",
    "exec": {
      "type": "http",
      "url": "https://api.example.com/bugs",
      "method": "POST",
      "body": {"title": "${input.title}", "severity": "${input.severity}"}
    },
    "result": {"message": "Bug reported!"},
    "fields": [
      {"id": "title", "label": "Title", "type": "text", "required": true,
       "validation": {"minLength": 5}},
      {"id": "severity", "label": "Severity", "type": "select",
       "value": "medium", "options": ["Low", "Medium", "High"],
       "width": "half"},
      {"id": "notify", "label": "Notify team", "type": "toggle",
       "value": "true", "width": "half"},
      {"id": "desc", "label": "Description", "type": "textarea"},
      {"id": "file", "label": "Attachment", "type": "file",
       "accept": "png,jpg,pdf"}
    ]
  }
}

Fields: Each field uses the same types as action inputs (text, textarea, select, number, toggle, file, directory, slider, date, color, password, radio, checkbox). directory opens a folder picker on macOS (no accept filter). Validation: validation object with regex, minLength, maxLength, pattern (email/url/phone), message. Layout: width per field (full/half/third) for multi-column, layout: "inline" for single-line input + button. Submit: exec supports http (with ${input.fieldId} interpolation in URL/headers/body) and script (file path in args). File fields send as multipart/form-data when submitted via HTTP. submitStyle: primary/destructive. Form resets after successful submit.

codeDiff

Inline unified diff viewer with colored add/delete lines. Perfect for code review notifications, AI agent diffs, commit previews.

{
  "type": "codeDiff",
  "title": "PR #247",
  "value": {
    "filename": "auth.swift",
    "language": "swift",
    "openURL": "https://github.com/org/repo/pull/247",
    "diff": "--- a/auth.swift\n+++ b/auth.swift\n@@ -10,3 +10,5 @@\n let token = getToken()\n-validate(token)\n+guard validate(token) else {\n+  throw AuthError.invalid\n+}\n return token"
  }
}

Accepts either diff (raw unified diff string) or lines (array of {type, content} where type is add/delete/context). Optional: filename, language (badge), additions/deletions (override counts), openURL (tap to open), expanded (boolean, default false β€” diff content is collapsed by default, tap chevron to toggle).

schedule

Daily agenda/timeline with time labels, event blocks, and a "now" marker. Perfect for calendar events, appointments, class schedules, meeting agendas.

{
  "type": "schedule",
  "title": "Today's Agenda",
  "icon": "calendar",
  "color": "blue",
  "value": {
    "showNowMarker": true,
    "showGaps": false,
    "overlapMode": "stack",
    "entries": [
      {
        "id": "1",
        "title": "Sprint Planning",
        "startTime": "2026-04-10T09:00:00Z",
        "endTime": "2026-04-10T10:00:00Z",
        "location": "Room 4B",
        "color": "blue",
        "icon": "person.3",
        "attendees": [
          {"name": "Alice", "avatar": "https://..."},
          {"name": "Bob"}
        ],
        "joinURL": "https://meet.google.com/xyz"
      },
      {
        "id": "2",
        "title": "Lunch",
        "startTime": "12:00",
        "endTime": "13:00",
        "emoji": "πŸ•"
      }
    ]
  }
}

Each entry requires: id, title, startTime (ISO-8601, epoch, or bare "HH:mm"). Optional: endTime, description, location, color, icon, emoji, status (scheduled/in_progress/completed/cancelled), attendees (array of {name, avatar?}), joinURL, allDay (boolean, renders as banner). Value-level options: showNowMarker (default true), showGaps (proportional time blocks), overlapMode ("stack" or "columns"), compact (minimal list), date (ISO date, default today), timeRange ({start, end} in "HH:mm"), maxVisible.

Standalone Inputs & Groups

Individual form input primitives that live as their own card items β€” no surrounding form wrapper. Live values are held per-card in a runtime state store and can be read by any button on the same card via the new ${state.<itemId>} interpolation token.

How it differs from form: A form card is a single unit with its own submit button β€” one form, one submit. Standalone inputs are loose items on a card that any action (item-level, group header, or card-level) can reference. Use them when you want multiple actions reading the same fields, or when you need mixed displays + inputs on the same card (e.g. a dashboard with a search box, filters, and a result list, or a prompt composer alongside a conversation feed).

Runtime-only state. Values are session-scoped β€” they survive card refresh but are wiped on app quit, logout, workspace switch, and card deletion. Never synced to the backend. password fields render via SecureField. Per-card isolation is enforced at the view layer: one card's state can never be read by another card's actions.

Input Types (13)

Each input stores its live value in the card's state store keyed by the item's id. The value in the card's value object is the schema (placeholder, default, options, validation, constraints) β€” not the live value.

Type Renders Schema fields
textField Single-line text input placeholder, defaultValue, required, validation
textArea Multi-line text editor placeholder, defaultValue, required, validation
password Secure text field (masked) placeholder, required
numberField Text field + stepper min, max, step, defaultValue
slider Range slider with min/max labels min, max, step, defaultValue
toggle On/off switch defaultValue ("true"/"false")
checkbox Single checkbox or multi-select list options, defaultValue (comma-separated)
radio Radio group options, defaultValue
select Pop-up picker options, defaultValue, placeholder
date Native date picker (ISO-8601 round-trip) defaultValue (ISO date)
color System color picker + hex swatch defaultValue (#RRGGBB)
file File picker button accept (comma-separated extensions)
directory Folder picker button β€”

Every input type supports item-level actions, so you can put a Save/Send/Run button directly next to the field.

Example: card with inputs + a save button

{
  "columns": 1,
  "items": [
    {
      "id": "username",
      "title": "Username",
      "type": "textField",
      "value": { "placeholder": "alice", "required": true }
    },
    {
      "id": "priority",
      "title": "Priority",
      "type": "select",
      "value": {
        "defaultValue": "medium",
        "options": [
          {"label": "Low",    "value": "low"},
          {"label": "Medium", "value": "medium"},
          {"label": "High",   "value": "high"}
        ]
      }
    },
    {
      "id": "save_note",
      "title": "Notes",
      "type": "textField",
      "value": { "placeholder": "Optional note" },
      "actions": [
        {
          "id": "save",
          "label": "Save",
          "icon": "checkmark",
          "style": "primary",
          "exec": {
            "type": "http",
            "url": "https://api.example.com/save",
            "method": "POST",
            "body": {
              "user":     "${state.username}",
              "priority": "${state.priority}",
              "note":     "${state.save_note}"
            }
          }
        }
      ]
    }
  ]
}

group

Recursive container for organizing card items into collapsible sections with their own header actions. Depth capped at 4 levels.

{
  "id": "advanced",
  "title": "Advanced",
  "type": "group",
  "value": {
    "layout": "section",
    "collapsible": true,
    "collapsed": false,
    "countBadge": 2,
    "children": [
      { "id": "dark_mode", "title": "Dark mode", "type": "toggle",
        "value": { "defaultValue": "false" } },
      { "id": "accent",    "title": "Accent color", "type": "color",
        "value": { "defaultValue": "#007AFF" } }
    ],
    "headerActions": [
      {
        "id": "reset",
        "label": "Reset",
        "icon": "arrow.counterclockwise",
        "style": "destructive",
        "exec": {
          "type": "http",
          "url": "https://api.example.com/prefs/reset",
          "method": "POST",
          "body": { "user": "${state.username}" }
        }
      }
    ]
  }
}

Layouts: vertical (default), horizontal, section (boxed visual with subtle fill). Collapsible: collapsible: true adds a chevron; collapsed is the initial state. Collapse persists across card refresh within session. countBadge: small number pill next to the title. headerActions: array of CardAction that can read any ${state.*} from the whole parent card β€” not just the group's children. children: any other card item type, including nested group (max depth 4; deeper payloads are rejected at decode time).

userInput

Chat-style composer: auto-sizing text area + optional attachments + send button. Designed as a drop-in for AI agent integration (prompt composer, support chat). Renders identically in popover, desktop widget, and notch panel β€” the text area measures its own content height and stays cleanly compact when empty.

{
  "id": "composer",
  "title": "Ask Claude",
  "type": "userInput",
  "value": {
    "placeholder": "Type a prompt…",
    "rows": 1,
    "maxRows": 14,
    "allowAttachments": true,
    "accept": "png,jpg,pdf,md,txt",
    "submitLabel": "Send",
    "submitIcon": "paperplane.fill",
    "clearOnSend": true,
    "textFieldName": "message",
    "exec": {
      "type": "http",
      "url": "https://api.anthropic.com/v1/messages",
      "method": "POST",
      "headers": {
        "x-api-key":         "${param.anthropicKey}",
        "anthropic-version": "2023-06-01",
        "content-type":      "application/json"
      },
      "body": {
        "model":      "claude-opus-4-6",
        "max_tokens": 1024,
        "messages": [
          { "role": "user", "content": "${state.composer}" }
        ]
      }
    },
    "result": {
      "message":    "Sent",
      "updateCard": true
    }
  }
}

Sizing: rows sets the initial visible line count (default 1), maxRows caps growth before internal scroll. For pixel-exact control use minHeight / maxHeight in points (takes precedence over rows). Attachments: set allowAttachments: true to show a paperclip button + enable drag-drop onto the composer. accept is a comma-separated extension filter. On submit, a payload with attachments is automatically sent as multipart/form-data (text goes into the field named by textFieldName, default message; attachments go as attachment0, attachment1, …). A payload without attachments uses the normal JSON body path with full ${state.*} / ${input.*} / ${param.*} interpolation. Submit behaviour: the text is available as ${state.<id>} everywhere on the card; on success the composer clears if clearOnSend is true (default). Set result.updateCard: true to have the response body parsed as new card data and swapped in without a refresh β€” useful for threading the model's reply into a sibling feed/message item on the same card.

Interpolation: ${state.x} vs ${input.x}

Three input flows coexist on a single card and their values compose at action execution time:

  • Flow A β€” form card: the form's own submit button fires with ${input.<fieldId>} resolved to the form draft.
  • Flow B β€” action with modal inputs: any button with inputs: [...] presents a transient modal form; values resolve as ${input.<id>} for that single exec.
  • Flow C β€” standalone inputs: values persist in the card's state store; any action on the card reads them via ${state.<itemId>}.

Composition rules:

  • ${state.x} β€” strictly reads from the parent card's state store. Never falls through to modal values, never searches other cards.
  • ${input.x} β€” reads card state, then overlays modal/form values on top. Modal wins on key collision; if no modal, it's equivalent to ${state.x}.
  • Other tokens stay unchanged: ${item.id}, ${item.title}, ${item.value}, ${card.id}, ${card.name}, ${param.*}.

To avoid surprises when mixing Flows B + C on the same card, use distinct ids for modal inputs vs standalone items.

Events & cross-card reactions (macOS; iOS/Android forthcoming)

Actions and card-internal sources (e.g. the nowPlaying player) can fire named events onto an in-process bus. Other cards subscribe via an on: block in their push payload and react with a full CardAction. Use this to: refresh a dashboard when a form submits, fetch lyrics when a track starts playing, chain actions without a server orchestrator, or mirror state from one card into another without cross-card state writes.

Emitting from an action result β€” four new optional fields on result, plus exec is now optional on any action:

{
  "id": "save",
  "label": "Save",
  "exec": {"type": "http", "url": "https://api.example.com/save", "method": "POST"},
  "result": {
    "refresh":      true,
    "refreshCards": ["Dashboard", "Recent Activity"],
    "emit":         {"event": "settings.saved", "data": {"by": "${card.name}"}},
    "onFailure":    {"notification": {"title": "Save failed", "body": "Try again"}}
  }
}

refreshCards refreshes other cards by display name (additive with the self-refresh from refresh: true). Missing names log + no-op; duplicate names fan out. emit fires on success only; its data values interpolate in the current card's context. onFailure is a sibling branch run only when exec fails and supports refreshCards / notification / emit. When exec is omitted the action runs no HTTP/script and no auto-refresh β€” it just runs the post-exec side effects. Useful for emit-only buttons (list item clicks, menu taps) and for subscribers that only want to patch their own fields from an event payload.

Patching the owning card β€” result.patch is a partial card-data object merged into the card's existing items by id. Every string inside the patch resolves ${event.*} / ${state.*} / ${input.*} / ${card.*} like any other interpolated field. Nested dict values deep-merge; arrays and scalars replace. Unknown ids log + drop; missing items are not added; items you don't name are left alone.

{
  "card": "Player",
  "items": [
    {"id": "np", "type": "nowPlaying", "value": {"title": "Nothing playing"}}
  ],
  "on": {
    "track.selected": {
      "id":    "load-track",
      "label": "",
      "result": {
        "patch": {
          "items": [{
            "id": "np",
            "value": {
              "streamUrl": "${event.streamUrl}",
              "title":     "${event.title}",
              "artist":    "${event.artist}"
            }
          }]
        }
      }
    }
  }
}

Pair with an emit-only button on a track list (exec omitted, result.emit set) to build "click a list row β†’ replace fields on another card" flows without any HTTP round-trip.

Subscribing from a card β€” add an on block to the push payload:

{
  "card": "Lyrics",
  "items": [...],
  "on": {
    "nowPlaying.played": {
      "id": "fetch-lyrics",
      "label": "",
      "exec": {
        "type":   "http",
        "method": "GET",
        "url":    "https://api.example.com/lyrics?q=${event.title}"
      },
      "result": {"updateCard": true}
    }
  }
}

Each key under on is an event name; each value is a full CardAction so reactions support HTTP/scripts, interpolation, updateCard, notifications, nested refreshCards / emit, etc. Inside a reaction, the triggering payload is read via ${event.<key>}. Every event also carries ${event.sourceCardId} + ${event.sourceCardName} so you can branch on who emitted.

Built-in emitters β€” the nowPlaying card automatically fires nowPlaying.loaded / nowPlaying.played / nowPlaying.paused / nowPlaying.ended with a payload of title, artist, album, streamUrl, source, currentTime, duration, isLive. More built-in emitters (form submit, timer elapsed, input change) are planned.

Rules & limits

  • Addressing is by card display name β€” keep names unique if you rely on refreshCards.
  • Event data is a flat string map. Nested path reads like ${event.track.artist} do not resolve β€” flatten at emit time.
  • Depth cap 8 hops, per-dispatch reaction budget 256 β€” runaway emit chains log once and drop.
  • Cross-card state writes always go through an event and a subscriber that writes its own state; there is no direct "write into card X's state store" primitive.
  • Reactions inherit their own card's auth. Events do not carry credentials.
  • Scope: in-process, macOS only. No Supabase sync, no webhook fan-out.