Blocks

Inline data displays and dashboards in chat

Blocks are inline UI widgets that display live data in your workspace. They come in two flavors:

  • Static blocks — declarative JSX, no imports, automatically laid out across four render sizes. This is the primary and recommended approach.
  • Dynamic blocks — full React with hooks and state, rendered in a sandboxed iframe. Experimental.

Static blocks

Static blocks are self-closing JSX tags. No imports, no functions, no hooks. The system parses the JSX, extracts props, fetches data, and handles all layout and sizing automatically.

Users create them in chat with a block code fence. Apps can also suggest them at install time via suggestedFlowsAndBlocks:

export default createApp({
suggestedFlowsAndBlocks: [
{
explanation: 'Shows the current open issue count.',
block:
'<Number query="type:stat app:github" extract="openIssues" label="Open Issues" />',
},
{
explanation: 'Shows the current open PR count.',
block: '<Number query="type:stat app:github" extract="openPRs" label="Open PRs" />',
},
],
})

Zones

Each component is automatically placed into a layout zone:

ZoneComponentsRenders as
headerStatusstatus indicator in the header row
statsNumber, Currency, Percent, Ratio, Durationmetric cards side by side
chartsChartline/area/bar charts
dataList, Table, Imagelists, tables, and images
actionsButtonhorizontal row of buttons
inputsChoice, Confirm, Input, Form, Selectinteractive user input

Zones are laid out top-to-bottom with dividers between them. The system adapts to four render sizes:

SizeContextBehavior
xsHUD bar, tab chipsCompact: single number, dot, tiny chart
smSmall hover previewLabels, small charts, truncated lists
mdMedium floating paneMid-size charts, partial lists
lgLarge full viewFull titles, axes, table rows, all items

At xs and sm sizes, blocks with no data are automatically hidden.

The query prop

Every block component accepts a query prop to fetch data. It supports three forms:

// string key — exact key lookup
<Number query="github-stats" extract="stars" />
// string shorthand — space-separated field:value filters
<Number query="app:github type:stat" extract="openIssues" />
// object form — for when you need JSX expressions or complex values
<Number query={{ type: "stat", app: "github" }} extract="openIssues" />

The string shorthand is the most concise. If the string contains :, it’s parsed as filters; otherwise it’s treated as a key lookup.

Query fields:

FieldTypeDescription
keystringFilter by data key
typestringFilter by data type
appstringFilter by app id
categorystringFilter by category
kindstringFilter by kind
statusstringFilter by status
limitnumberMax rows to return (default 30)
sortstring'newest' or 'oldest'

The extract prop

When data rows store JSON in their value field, use extract to pull out a nested property using dot-notation:

// if the stored value is { "openIssues": 42, "openPRs": 7 }
<Number query="type:stat app:github" extract="openIssues" />
// nested paths work too
<Number query="type:stat app:myapp" extract="metrics.daily.total" />

The from prop

As an alternative to query, any block component accepts a from prop that runs an app action at render time to get live data. Instead of reading from the data store, the block calls the action and displays whatever it returns.

// fetch live issues from the github app's "issues" action
<List from="/github issues" extract="items" label="Recent Issues" />
// live PR count
<Number from="/github prs" extract="total" label="Open PRs" />

The value is a slash-command string (/appId actionName). The action runs on every render, so from is best for data that should always be fresh. Use query when you want to display data that has already been synced to the data store.

Components

Metric components

Five semantic components for displaying single values. Each sets the appropriate format automatically:

TagUse forExample output
Numbercounts, integers42, 1,234
Currencymoney (auto-formatted)$1,234.56
Percentpercentages42%
Ratiofractions3/5
Durationtime2h 30m
<Number query="type:stat app:github" extract="openIssues" label="Open Issues" />
<Percent value={99.9} label="Uptime" suffix="%" trend="up" delta="+0.1%" />
PropTypeDescription
querystringData query (see above)
extractstringPath to extract from JSON value
valuemixedStatic value (skips query)
labelstringDisplay label
formatstringcurrency, number, percent, compact, duration
prefixstringText before the value
suffixstringText after the value
currencystringCurrency code (for format="currency")
precisionnumberDecimal places
compactbooleanForce compact number formatting
deltastringChange indicator text
trendstringup or down — colors the delta
thresholdsobject{ warn, danger, success } numeric thresholds
trendDirectionstringpositive-up or positive-down

thresholds applies a color theme based on the numeric value: danger (red) takes priority, then warn (yellow), then success (green).

Chart

Line, bar, or area charts with automatic axis scaling.

<Chart query="category:stripe-daily-revenue type:series sort:oldest limit:30" chartType="area" label="Revenue" />
PropTypeDescription
querystringData query (used at lg size)
previewQuerystringKey lookup for xs/sm/md sparkline preview (see below)
extractstring | string[]Path to extract numbers; array enables multi-series
datanumber[]Static data (skips query)
chartTypestringline, bar, or area
labelsstringExtract path for x-axis labels
labelstringChart title
rightScalestring[]Series names to render on the right Y-axis (dual-axis charts)
heightnumberCustom chart height in pixels
formatstringY-axis value formatting: currency, number, percent

At xs/sm/md sizes, Chart renders as a compact sparkline using previewQuery (falls back to query if not set). At lg size, it renders as a full interactive chart with axes and crosshair.

previewQuery best practice: For series data, use a plain key string (e.g. previewQuery="stripe-daily-revenue") that loads the single aggregate row. series() automatically maintains a rolling points array (up to 30 values) on aggregate rows, and Chart auto-discovers this for sparkline rendering. This avoids expensive filter queries at small sizes.

When multiple Chart blocks share the same query.app and labels path, they automatically combine into a single multi-series chart with a legend.

When multiple bar series exist, grouped bar mode is auto-enabled. Line and area charts with only a single data point auto-convert to bar charts.

Status

A colored health indicator dot. Renders in the header zone.

<Status query="key:api-health" extract="status" label="API" />
PropTypeDescription
querystringData query
extractstringPath to extract status string
valuestringStatic status (skips query)
labelstringDisplay label

Status color mapping:

  • Green: up, healthy, ok
  • Red: down, degraded, error
  • Yellow: anything else

List

Bullet or numbered list of items. Supports both simple string lists and rich object rows with auto-detected field types.

// simple string list with extract
<List query="type:issue app:github limit:5" extract="title" label="Recent Issues" />
// static items
<List data={["Deploy API", "Update docs", "Fix login"]} ordered label="Tasks" />

When the query returns objects and no extract is set, List renders rich rows with auto-detected fields — titles, status badges, dates, and numbers:

<List query="app:github category:github-issues sort:newest" label="Recent Issues" limit={5} />

Field detection uses key names: title/name → primary text, status/state → colored badge, author/user → avatar + name, createdAt/date → relative time, amount/count → formatted number, additions/deletions → diff stats. Use fields or columns to override.

For explicit column control, use the columns prop with typed shorthand:

<List query="app:github category:github-prs sort:newest" columns="title state:badge additions:number deletions:number createdAt:date" label="Recent PRs" limit={5} />
PropTypeDescription
querystringData query
extractstringPath to extract per item (omit for rich rows)
dataany[]Static items (skips query)
fieldsstring[]Explicit field selection (auto-detected if omitted)
columnsstringTyped field shorthand like Table ("key:type")
orderedbooleanNumbered list (default: bullet)
limitnumberMax items to show
labelstringList title

At xs size, shows the item count. At sm, shows title + badge. At md, shows title, badge, and truncated fields. At lg, full rows with title, description, date, number, and badge.

Table

Tabular data with column headers, row striping, and typed cell rendering.

// string shorthand for columns — "key" or "key:type"
<Table query="app:github type:issue" columns="title status:badge author createdAt:date" limit={10} label="Issues" />

For full control over labels and formatting, use column definition objects:

<Table query={{ app: 'stripe', category: 'recent-transactions' }} columns={[ { key: 'customer', label: 'Customer' }, { key: 'amount', label: 'Amount', format: 'currency' }, { key: 'status', label: 'Status', type: 'badge' }, { key: 'createdAt', label: 'Date', type: 'date' }, ]} label="Recent Transactions" limit={8} />
PropTypeDescription
querystringData query
extractstringPath to extract row objects
columnsstring | (string | object)[]Column definitions (auto-detected)
limitnumberMax rows (default 10)
labelstringTable title

Column string shorthand: "key" or "key:type" where type is badge, number, date, link, boolean, or a format like currency, percent, compact, duration.

Column object fields:

FieldTypeDescription
keystringField name in the data
labelstringDisplay header (defaults to key)
typestringtext, badge, number, date, boolean, link
formatstringcurrency, number, percent, compact, duration
widthnumberFlex weight (default 1, numbers get 0.6, badges 0.8)

Cell types are auto-detected from key names — status → badge, amount → number, createdAt → date, url → link. Only renders at lg size — hidden at xs, sm, and md.

Button

Interactive button that triggers an app action. On press, inserts the action string as a slash command message in the channel.

<Button label="Sync Revenue" action="/stripe sync revenue" variant="primary" />
<Button label="Delete" action="/stripe delete old-data" variant="destructive" confirm="Delete old data?" />
PropTypeDescription
labelstringButton text
actionstringSlash command to execute (e.g. /stripe sync)
variantstringprimary, secondary, destructive
confirmstringConfirmation dialog text (omit to skip)

Hidden at xs, sm, and md sizes. Only renders at lg size. The action is processed by the messageFlowEffect pipeline — no new mutations needed.

Image

Displays an image with error handling and CDN optimization.

<Image src="https://example.com/dashboard.png" alt="Dashboard" aspectRatio="16/9" />

The image source can also come from a query:

<Image query="latest-screenshot" extract="url" caption="Latest capture" />
PropTypeDescription
srcstringImage URL (or resolve via query + extract)
querystringData query to resolve src
extractstringPath to extract URL from query result
altstringAlt text
aspectRatiostring16/9, 4/3, 1/1, or auto
captionstringText below image (lg size only)

Hidden at xs size. At sm/md, renders cover-fit with 120px max height. At lg, full width with 400px max height and optional caption. Automatically optimizes URLs for imagekit, cloudinary, and imgix CDNs.

Data

A wrapper that runs a single query and shares the result with all child blocks. Useful when multiple blocks need the same data:

<Data query="type:stat app:github">
<Number extract="openIssues" label="Issues" />
<Number extract="openPRs" label="PRs" />
<Chart extract="dailyCounts" chartType="bar" />
</Data>

Full example

A complete dashboard block for a Stripe app:

<Status query="type:stat app:stripe" extract="status" label="Stripe" /> <Currency query="type:stat app:stripe" extract="mrr" label="MRR" prefix="$" /> <Number query="type:stat app:stripe" extract="customers" label="Customers" format="compact" /> <Chart query="type:series app:stripe category:daily-revenue sort:oldest limit:30" chartType="area" label="Revenue" /> <Table query="app:stripe category:recent-transactions" columns="customer amount:currency status:badge createdAt:date" label="Recent Transactions" limit={5} /> <List query="app:stripe type:event limit:5" label="Recent Events" />

The data these blocks display comes from your app’s backend writing to the data table via tools.data. See Data for details.

Interactive blocks

Interactive blocks let the AI (or apps) ask the user for input inline in a message. Unlike dashboard blocks which go in code fences, interactive blocks are written directly in message content and render as interactive UI.

When the user interacts (clicks a choice, fills a field, confirms), their response is automatically sent as a new message in the thread.

Choice

Pick from a set of options. Single-select submits immediately; add multi for multi-select with a submit button.

<Choice label="Which environment?" options="staging,production,canary" />
PropTypeDescription
labelstringQuestion text
optionsstringComma-separated choices
multibooleanAllow selecting multiple

Confirm

Yes/no prompt with optional description and destructive variant.

<Confirm label="Delete 47 stale branches?" description="This cannot be undone" yes="Delete" no="Cancel" variant="destructive" />
PropTypeDescription
labelstringQuestion text (required)
descriptionstringAdditional context
yesstringConfirm button text (default “Confirm”)
nostringCancel button text (default “Cancel”)
variantstring"default" or "destructive"

Input

Single text field with submit button.

<Input label="API key" name="apiKey" placeholder="sk-..." type="text" />
PropTypeDescription
labelstringField label
namestringKey in the response message
placeholderstringPlaceholder text
typestring"text", "number", "url", "email"
requiredbooleanPrevents empty submit

Select

Scrollable option list with optional search. Best for longer lists (7+).

<Select label="Repository" options="repo-a,repo-b,repo-c" searchable />
PropTypeDescription
labelstringField label
namestringKey in the response message
optionsstringComma-separated choices
querystringBlock query to pull options from data
extractstringField to extract from query results
searchablebooleanEnable search filtering

Form

Groups multiple inputs into a single submit. Children can be Input, Choice, or Select.

<Form label="Deploy Config" submit="Deploy">
<Input name="branch" label="Branch" placeholder="main" />
<Input name="replicas" label="Replicas" type="number" />
<Choice name="region" label="Region" options="us-east,eu-west" />
</Form>
PropTypeDescription
labelstringForm title
submitstringSubmit button text (default “Submit”)

Interactive blocks are hidden at xs and sm sizes — they’re conversational components, not dashboard elements.

Dynamic blocks (experimental)

For rich interactive UIs beyond what static blocks can do, you can write full React components with hooks and state. These render in a sandboxed iframe instead of being statically analyzed.

See Dynamic Blocks for the full reference.