Messages & Components

Agents communicate with humans by sending messages on sessions. Each message can include a tree of composable components that render as rich UI—buttons, forms, tables, charts, and more. All message content is encrypted end-to-end; the server never sees plaintext.

How it works

  1. Agent creates a session (a persistent conversation channel)
  2. Agent sends a message with components—a tree of ComponentNode objects
  3. Components render as rich UI on the human side (buttons, forms, tables, charts, etc.)
  4. Human responds via interactive components (button_group, qa, rating)
  5. Agent receives the response

ComponentNode interface

Every component in a message is a ComponentNode. Layout nodes use children to nest other components inside them.

TypeScript
interface ComponentNode {
  type: string;                        // Component type
  data?: Record<string, unknown>;      // Component config
  children?: ComponentNode[];          // Nested components (for layout types)
}

Layout nodes

Layout nodes structure your message. They contain children and control how nested components are arranged.

Type Description
card Bordered container with optional title
group Structural grouping (no visible chrome)
tabs Tabbed content switcher
story Instagram-style swipeable slides
stepper Numbered vertical steps
page Vertical stack with spacing

Content blocks

Content blocks display information. They are leaf nodes—no children.

Type Description
text Rich text content
header Section headers (level 1-3)
divider Horizontal separator
code Code blocks with syntax highlighting
list Ordered/unordered lists
quote Blockquotes with attribution
table Data tables with headers
alert Info/success/warning/error alerts
kv Key-value pairs
stat Single metric with trend
metric_grid Grid of metrics
steps Progress steps (done/active/pending)
accordion Collapsible sections
chart Bar/line charts
image Images with captions
payment_details Payment/invoice display
scheduled_event Calendar event
countdown Countdown timer
file_download File download links

Interactive components

Require response

Interactive components collect input from the human. When a message contains an interactive component, the message status moves through the response lifecycle.

Type Description
button_group Action buttons (primary/danger/default variants)
qa Single select, multi select, or open-ended questions
rating Star rating or NPS
vote Poll/vote with results

Examples

Simple decision message

An agent asks a human to approve or reject a refund. The message combines a header, key-value context, and action buttons.

Python
client.send_message(session.id, components=[
    {"type": "header", "data": {"text": "Approve refund — $149.95", "level": 3}},
    {"type": "kv", "data": {"entries": [
        {"key": "Customer", "value": "Jane Smith"},
        {"key": "Reason", "value": "Item arrived damaged"},
    ]}},
    {"type": "button_group", "data": {"buttons": [
        {"label": "Approve", "action": "approve", "variant": "primary"},
        {"label": "Reject", "action": "reject", "variant": "danger"},
    ]}},
])

Report with metrics

A metrics dashboard sent as a message. No response required—this is purely informational.

TypeScript
await client.sendMessage(session.id, {
  components: [
    { type: "metric_grid", data: { metrics: [
      { label: "ARR", value: "$4.2M", change: "+68% YoY" },
      { label: "Churn", value: "1.8%", change: "↓0.4pp" },
    ] } },
    { type: "chart", data: {
      type: "bar",
      title: "Revenue by Quarter",
      labels: ["Q1", "Q2", "Q3", "Q4"],
      values: [820, 1100, 1340, 1580],
    } },
  ],
});

Tabbed layout

Components nest inside layout nodes using children. Here, a tabs node wraps two group nodes, each with its own content.

TypeScript
await client.sendMessage(session.id, {
  components: [
    { type: "tabs", children: [
      { type: "group", data: { label: "Overview" }, children: [
        { type: "kv", data: { entries: [
          { key: "Status", value: "Healthy" },
          { key: "Uptime", value: "99.97%" },
        ] } },
      ] },
      { type: "group", data: { label: "Metrics" }, children: [
        { type: "metric_grid", data: { metrics: [
          { label: "Requests", value: "1.2M", change: "+12%" },
          { label: "Errors", value: "0.03%", change: "↓0.01%" },
        ] } },
      ] },
    ] },
  ],
});

Response lifecycle

Messages containing interactive components follow a response lifecycle. Set responseRequired: true to mark a message as needing human action.

Status flow

pending viewed responded completed

Optional parameters

Parameter Type Description
responseRequired boolean Mark the message as requiring human action.
deadlineAt string (ISO 8601) Auto-expire the message if no response is received by this time.
priority string "normal", "high", or "critical". Affects notification urgency and sort order.
Tip: Use priority: "critical" for urgent messages—they trigger push notifications and show prominent badges in the app.

See also

  • Getting Started — Create your first agent and send a message in 5 minutes
  • Embed SDK — Drop message components into your own UI
  • SDKs — Python, TypeScript, and Deno client libraries
  • MCP Server — Use Hiloop from Claude, Cursor, or Windsurf
  • API Reference — Full REST API documentation