Today Canvas Kit

Action protocol

Typed widget-to-host actions emitted by the TCK Action component.

The TCK action protocol is the narrow wire contract for user-triggered widget actions. A widget emits an action by calling the generic host RPC:

ctx.invokeHost('today.action', action, payload)

The target is always today.action. The action string selects one of the supported product intents, and payload carries the JSON-serialisable data the host needs to execute it. Hosts must route from this typed pair, not by scraping button text or inspecting DOM structure.

Widget authors should normally use the SDK's <Action> component instead of calling ctx.invokeHost directly. By default it renders a real <button type="button">, so web previews do not expose today://... hrefs on hover. A host can resolve the same action into an <a href>, decide whether that anchor keeps normal navigation or is preventDefault()-handled, intercept the press with metadata, or wrap the rendered node for product chrome. Widget styling should treat Action as element-agnostic: include explicit display, alignment, spacing, text, and state classes instead of relying on native button defaults.

<Action> is the recommended authoring API, not the only supported runtime input. Hosts must continue to support existing widgets that render plain anchors, including today://... schema links and normal https://... links. The bundler emits a warning for bare <a href> so new host-action CTAs migrate to typed actions, but that warning is not a runtime ban and should not be treated as a compatibility break.

This is part of the host/renderer backward-compatibility contract: stricter source validation must never turn an already-published bundle into a render-time failure.

Wire shape

type TodayActionEvent =
  | {
      target: 'today.action'
      action: 'chat.composer.fill'
      payload: { text: string; mode?: 'append' | 'replace'; label?: string; source?: string }
    }
  | {
      target: 'today.action'
      action: 'web.open'
      payload: { url: string; title?: string }
    }
  | {
      target: 'today.action'
      action: 'connector.navigate'
      payload: { connectorId: string; route?: string; label?: string }
    }

Payloads may include additional JSON-serialisable fields when a product host needs more context, but the required fields above are stable ABI surface.

Supported actions

ActionRequired payloadHost behavior
chat.composer.filltextFill or append text in the Today chat composer.
web.openurlOpen a website or external web resource.
connector.navigateconnectorIdNavigate to a connector page, optionally scoped by route.

Every action requires an explicit payload. For chat.composer.fill, payload.text is the composer prefill while the component children remain an independent, concise control label. The payload must not be omitted or inferred from rendered text. Make text self-contained because it replaces the chat composer contents and is the only text the model receives after submission. Integrate every relevant fact into a direct, natural-language request the user could send. Do not refer to a card the model cannot see, append raw widget JSON, or describe protocol fields.

This required payload is the TypeScript authoring contract for new widgets. The generated project runs tsc --noEmit before bundling, so a new build fails when the payload is omitted. The runtime remains backward compatible with previously bundled widgets: if a legacy chat.composer.fill action has no usable payload.text, <Action> emits a migration warning and falls back to the visible label as the composer text. That fallback prevents old content from failing to render; it is not a substitute for a complete payload in new widget source.

Example

import { Action, type TodayActionDescriptor } from '@todayai-labs/tck'

export function CardActions() {
  const summarizeAction: TodayActionDescriptor<'chat.composer.fill'> = {
    action: 'chat.composer.fill',
    label: 'Summarize release status',
    payload: {
      text: 'Summarize the release-readiness status and recommend the next action. Two required checks are still failing, and the platform team owns both blockers.',
    },
  }

  return (
    <footer>
      <Action action={summarizeAction.action} payload={summarizeAction.payload}>
        {summarizeAction.label}
      </Action>

      <Action action='web.open' payload={{ url: 'https://today.ai', title: 'Today AI' }}>
        Open Today
      </Action>

      <Action
        action='connector.navigate'
        payload={{ connectorId: 'gmail', route: 'connect', label: 'Reconnect Gmail' }}
      >
        Reconnect Gmail
      </Action>
    </footer>
  )
}

Preview Playground records every emitted action in its Actions console for both Stage and Canvas renders. The console shows the raw action name, payload, widget id, and render source so widget authors can verify the host event without a production app.

Plain anchors do not appear in the Actions console because they are handled through the host's link-click path rather than the typed today.action protocol. Web hosts should support both paths: Action dispatches typed host events and may be host-rendered as a button or link, while legacy anchors continue through the existing link handler.

On this page