Skip to main content
A notification is a one-way message to a person. It is the counterpart to an interaction: where ask_user suspends the caller and blocks for an answer, notify_user delivers a message and returns immediately, waiting on nothing. notify_user is a projected operation tool, so an agent or tool with it in scope can send one — and it is the same capability behind POST /api/notifications and the tai notifications notify CLI command.

Two destinations

A notification lands in either or both of two places:
  • The in-app inbox — a per-deployment feed the Studio and the API read back. Every notification is recorded here.
  • An external channel — when channel names a registered channel plugin (Telegram, Slack, WhatsApp, Twilio, web), the message is also pushed to a person on that medium.
Omitting channel records the notification to the inbox only. Naming a channel that is not registered is a loud error, never a silent drop.

What a notification carries

With a named channel, the richer forms deliver only where the channel advertises the matching capability; with no channel, media, location, template, options, and sections are stored on the inbox record as given — schema alone requires a named channel (400 without one): the internal sink has no delivery vehicle to render a form and no submission door for its answers, so a stored form nobody could submit is refused up front, never recorded. The composition rules are:
  • One choice surface. options XOR sections — a flat button list or a sectioned list, never both.
  • One interactive surface. schema (a form) excludes both options and sections — a message carries a form or a choice list, never both.
  • header/footer compose a choice surface. Each requires options or sections present.
  • template stands alone. The out-of-window send is mutually exclusive with every other content and interactive field — media, location, options, sections, schema, and (transitively) header/footer.
  • media and location combine freely with a choice surface or a form: a card with a tappable list beneath it, a form with display media above it, a pin beside a choice.
A form or a choice needs a prompt, so schema, options, and sections each require a non-blank message (only a content-only media/location send may leave the message blank). A channel that does not advertise the matching capability rejects the send with 501 not supported rather than silently downgrading it to text.
The sending identity a channel message leaves from is set only by the conversation bridge, never by a caller — a caller-supplied sender is refused with a 400.

Media kinds and typed options

Media is a list of MediaItems, each a kind + url + optional caption. The file kinds — image, document, video, audio — carry a fetchable body (an absolute https URL, a same-origin /api/interactions/media/<id> served reference, or, for image only, an inline data:image/* URI); a link is a labelled http(s) anchor the person clicks through. A document item may also carry a filename — the suggested display name the medium shows for the download; it is meaningful only on a document and is refused on any other kind. Options are a typed, discriminated list — the clean break from the old text-only list[str]. Each entry is one of two shapes, tagged by kind:
  • A reply is a quick-reply / list-row: tapping it submits text as the person’s next inbound message, exactly as if they had typed it. description is an OPTIONAL secondary line a sectioned-list row renders under its text (a channel that renders flat buttons ignores it — see the matrix). id is an OPTIONAL author-set stable identifier: when set, the channel sends it verbatim on the wire and the person’s tap echoes it back to the inbound turn as params.reply_id (opaque enrichment a conversation tool target reads beside the message); when omitted the channel mints its own id.
  • A link is a call-to-action button: tapping it opens url in the browser and submits no message — distinct from a reply.

Sectioned lists, headers, and footers

sections is the sectioned alternative to a flat options list: titled OptionSection groups, each holding reply rows only (a link is a button, never a list row). Use it to group a longer choice under headings; the rows summed across every section still obey the 10-option cap. A message carries options or sections, never both. An interactive message may also carry a header — a single display-media item (image/document/video/audio, never a link) shown above it — and a footer, a short trailing line beneath it. Each composes a choice surface, so both require options or sections to be present.

Location

location shares a geographic point — latitude/longitude in WGS84 decimal degrees, with an optional place name and street address. A channel that renders maps drops a native pin; one that cannot renders the coordinates (and any name/address) as text. It combines with a choice surface or a form, and a location-only send may leave message blank.

Out-of-window templates

template is a pre-approved ChannelTemplate for delivery outside a medium’s freeform window (e.g. WhatsApp’s 24-hour customer-service window). It is addressed by name + language and parameterised by its named components, never a flat positional list:
  • header_media — the media argument for a media header component (a display item, never a link);
  • body_parameters — the positional body-text values substituted into the body’s placeholders in order (typed values ride as their pre-formatted strings);
  • buttons — the positional per-button arguments (a quick-reply payload or a URL suffix), the i-th entry parameterising the i-th button.
The named-component shape is a clean break: the old flat parameters list is removed. A template’s runtime arguments are header_media, body_parameters, and buttons — nothing else.

Channel capability matrix

Every shipped channel renders the shared vocabulary as far as its medium allows and declines — never silently downgrades — a shape it cannot render: a send of an unadvertised capability is refused loudly (501 not supported) so content is never dropped. Where a medium lacks a native construct but can approximate one, the channel degrades honestly rather than declining. The support each channel advertises: ¹ Slack has no files.upload seam here, so a document/video/audio item degrades to a labelled link line rather than an uploaded file. ² Telegram carries a bridged tap’s author-set id back as params.reply_id and a sectioned row’s description as params.reply_description. ³ Telegram has no native sectioned list, so sections degrade to grouped buttons with each section title rendered as a text header. ⁴ ⁵ ⁶ A template is a vendor construct (a pre-approved WhatsApp template referenced by name/language). Web chat, Slack, and Twilio have no such registry to render one into, so they decline the capability outright. ⁷ On WhatsApp a form notification is a native Flow, and — like every freeform send — only inside the provider’s 24-hour customer-service window; an out-of-window form notification fails loudly, never downgraded. ⁸ Telegram and Slack deliver a form question (ask_user with answer_format="form") but not an ask-less form notification: an ask-less form is ticketless and their form vehicle (a callback page / a Block Kit modal) needs an ask’s ticket machinery to open. SMS has no form vehicle at all. ⁹ A form’s pages render per medium: WhatsApp gives each page its own Flow screen; the web widget and Telegram’s callback-page webview show them as steps with a progress line; Slack, having no native multi-step modal, lays the pages out as titled sections within the one Block Kit modal. A form’s per-send values and options ride the same vehicles — prefilled, and offered in place of the schema’s enum — and a channel that cannot honor them refuses the send, naming the field. reply.description support is channel-dependent: WhatsApp list rows render it and Slack folds it into a muted context line; Telegram never displays it and instead echoes it back as params.reply_description on a bridged tap, and a channel that renders only flat buttons ignores it.

audience versus recipient

These are two orthogonal axes, and both may be set at once:
  • audience is the identity whose scoped in-app inbox surfaces the record.
  • recipient is the delivery address on an external channel.
Setting audience is honored even when a channel recipient also delivers the message — you get both the external push and the in-app entry. audience is the same field, with the same isolation rules, that ask_user uses: a restricted owned key addresses only its own slice and cannot broadcast, and addressing another identity is refused with a 403. See owned keys for the full read/write isolation model.

Ask-less forms

Passing schema — a JSON-schema form in the same channel-deliverable subset a channel-delivered form question uses — turns the notification into an ask-less form: the channel renders message as the form’s prompt and schema as the fillable form, and the send returns immediately, exactly as every notification does.
What comes back is not an answer — nothing is waiting for one. A submission enters the conversation as an ordinary message from the person, riding the same inbound path a tapped option takes: rendered text as the message every consumer sees, with the structured values beside it for a conversation route’s tool target to map deliberately. There is no interaction, no ticket, no expiry, and no validation loop: unlike a form ask, nothing blocks, nothing re-prompts on a bad value, and the platform never checks the submission against the schema — the values are participant-shaped data, bounded as transport (size and nesting) but carrying no more trust than typed participant text. A tool that consumes them validates them itself. When to use which: a form ask (ask_user with answer_format="form") suspends the caller until a validated answer or an expiry — reach for it when the flow cannot proceed without the answer. An ask-less form sends the form and moves on — reach for it when the submission is welcome whenever (and however often) it arrives, as each submission is simply the person’s next message. On the web chat the form renders as a card in the transcript. Like the option chips it has no deadline and no answered state: submitting shows a local “Sent” badge, the card stays fillable (each submission is its own participant message), and an expired form shows an inline “no longer available” line:
The web chat form card rendering a prompt and a schema-driven form with text, choice, and number fields above a send button.The web chat form card rendering a prompt and a schema-driven form with text, choice, and number fields above a send button, in the dark theme.

The web chat's ask-less form card — the prompt, the schema-rendered fields, and the send control.

On WhatsApp the form delivers as a native Flow — and, like every freeform send, only inside the provider’s 24-hour customer-service window. An out-of-window form notification fails loudly (synchronously or as a failed delivery status), never silently downgraded to a template or to text. With an audience, the in-app record stores the schema alongside the message, so the feed shows what was asked for — but the inbox record itself is never submittable, which is why a form notification with no channel is refused.

Reading the inbox

The inbox is read on demand — there is no streaming surface. List the recorded notifications newest-first:
or GET /api/notifications. A restricted caller reads only its own audience slice; an unrestricted operator reads the shared feed. The feed is a bounded, newest-first ring: it retains a fixed maximum number of entries and trims the oldest past it. It rides on the interactions Redis store, so a deployment with that store unconfigured cannot record and lists an empty inbox.
The CLI tai notifications notify exposes --channel, --recipient, --media, --template, --options, --sections, --location, --header, --footer, and --schema (each rich form a JSON string validated into its contract model), but no --audience — addressing a specific identity is available on the tool and the API only.

See also

  • Interactions — the blocking counterpart, ask_user, and the form ask an ask-less form contrasts with.
  • Owned keys — the audience isolation model in full.
  • Client conversations — the channels a notification pushes to, and the structured participant message a form submission arrives as.
  • CLI reference — the full tai notifications surface.