> ## Documentation Index
> Fetch the complete documentation index at: https://docs.tai42.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Notifications

> Send a one-way message to a person — to an in-app inbox, an external channel, or both.

A notification is a **one-way** message to a person. It is the counterpart to an
[interaction](/concepts/interactions): 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`](/reference/cli/notifications) 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](/plugins) (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

| Field               | Effect                                                                                                                                                                                                                                                                                                                                                                                        |
| ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `message`           | The text shown to the person. Required; it may be blank only when `media` or `location` carries the content (a content-only send).                                                                                                                                                                                                                                                            |
| `channel`           | A registered channel to deliver on. Omit to record to the inbox only.                                                                                                                                                                                                                                                                                                                         |
| `recipient`         | The per-call **delivery address** on that channel — a chat id, a phone number. Omit to use the channel's operator-default recipient.                                                                                                                                                                                                                                                          |
| `audience`          | The **identity** whose in-app inbox shows the record (see below).                                                                                                                                                                                                                                                                                                                             |
| `media`             | Display media sent with the message — `image`, `document`, `video`, and `audio` file items plus labelled `link` anchors (see [media kinds](#media-kinds-and-typed-options)). Requires a channel that advertises media support.                                                                                                                                                                |
| `location`          | A shared geographic point — latitude/longitude with an optional place `name` and `address`. Requires a channel that advertises location support.                                                                                                                                                                                                                                              |
| `template`          | A pre-approved out-of-window [template](#out-of-window-templates) send, addressed by its `name` + `language` with **named components** (a media header, positional body values, per-button arguments). Requires a channel that advertises template support.                                                                                                                                   |
| `options`           | A flat list of tappable [typed options](#media-kinds-and-typed-options) (at most 10) — a `reply` (its text becomes the person's next message) or a `link` (opens a URL). Requires a channel that advertises interactive support.                                                                                                                                                              |
| `sections`          | The sectioned alternative to `options` — titled groups of reply rows (rows summed across sections stay within 10). Requires interactive support.                                                                                                                                                                                                                                              |
| `header` / `footer` | A single media `header` above, and a short text `footer` below, an interactive message. Each requires `options` or `sections`.                                                                                                                                                                                                                                                                |
| `schema`            | An [ask-less form](#ask-less-forms)'s answer schema. The channel renders `message` as the form's prompt and `schema` as the fillable form; the submission enters the conversation as an ordinary message from the person. Requires a named channel that advertises form support.                                                                                                              |
| `data` / `pages`    | Per-send [prefill, choice lists, and step layout](/concepts/interactions#prefilling-per-send-options-and-pages) over `schema` for this one send — `data.values` opens the form filled in, `data.options` replaces a string property's choices for this send, and `pages` lays the form out in titled steps. Both ride ONLY a form send (a `schema` present) and are cross-checked against it. |

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.

<Note>
  The sending identity a channel message leaves **from** is set only by the
  [conversation bridge](/reference/conversation-bridge), never by a caller — a
  caller-supplied sender is refused with a `400`.
</Note>

## Media kinds and typed options

**Media** is a list of `MediaItem`s, 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`:

```json theme={null}
{"kind": "reply", "text": "Yes, book it", "description": "Confirms the 3pm slot", "id": "confirm-3pm"}
{"kind": "link",  "label": "Open the docs", "url": "https://docs.example.com"}
```

* 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](#channel-capability-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](/concepts/client-conversations)
  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.

<Note>
  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.
</Note>

## 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:

| Shape                                      |  [WhatsApp](/plugins/tai42/channel-whatsapp) | [Web chat](/plugins/tai42/channel-web) |                [Telegram](/plugins/tai42/channel-telegram)                | [Slack](/plugins/tai42/channel-slack) |   [Twilio](/plugins/tai42/channel-twilio)   |
| ------------------------------------------ | :------------------------------------------: | :------------------------------------: | :-----------------------------------------------------------------------: | :-----------------------------------: | :-----------------------------------------: |
| `media` — image                            |                    native                    |                 inline                 |                                `sendPhoto`                                |              image block              |                MMS `MediaUrl`               |
| `media` — document/video/audio             |               native, per item               |     download card / native players     |                       `sendDocument`/`Video`/`Audio`                      |          labelled link line¹          | `MediaUrl`; document → `filename: url` line |
| `media` — link                             |               appended to body               |             outbound anchor            |                              inline with body                             |           labelled link line          |                  body line                  |
| `options` (reply / link)                   | reply buttons or list; link → CTA URL button |      tappable chips; link → button     |                              inline keyboard                              |           Block Kit buttons           |                 **declined**                |
| `reply.description`                        |            list-row secondary line           |             secondary line             |                        `params.reply_description`²                        |      folded into a context block      |                      —                      |
| `reply.id` → `params.reply_id`             |                      yes                     |                   yes                  |                                    yes                                    |                  yes                  |                      —                      |
| `sections`                                 |              multi-section list              |              titled groups             |                   grouped buttons, title as text header³                  |     section header + reply buttons    |                 **declined**                |
| `header` / `footer`                        |          media header / text footer          |       media header / muted footer      | header as caption or a separate message; footer as a trailing italic line |   image block / muted context block   |                 **declined**                |
| `location`                                 |                  native pin                  |      map-pin + OpenStreetMap link      |                        `sendLocation` / `sendVenue`                       |      section + OpenStreetMap link     |                 **declined**                |
| `template`                                 |           native (named components)          |              **declined**⁴             |                                **declined**                               |             **declined**⁵             |                **declined**⁶                |
| `schema` (ask-less form)                   |           native Flow (in-window)⁷           |                form card               |                               **declined**⁸                               |             **declined**⁸             |                **declined**⁸                |
| `form` **question** delivery (`ask_user`)⁹ |                     Flow                     |               web widget               |                     webview of the callback form page                     |            Block Kit modal            |                 **declined**                |

¹ 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`](/concepts/interactions#prefilling-per-send-options-and-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](/concepts/owned-keys#audience--one-concept-across-interactions-and-notifications)
addresses only its own slice and cannot broadcast, and addressing another identity
is refused with a `403`. See
[owned keys](/concepts/owned-keys#audience--one-concept-across-interactions-and-notifications)
for the full read/write isolation model.

## Ask-less forms

Passing `schema` — a [JSON-schema form](/concepts/interactions#delivering-to-a-channel)
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.

```bash theme={null}
tai notifications notify "fill this in" --channel whatsapp \
  --schema '{"type": "object", "properties": {"name": {"type": "string"}}}'
```

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](/concepts/client-conversations#structured-participant-messages)
to map deliberately. There is **no interaction, no ticket, no expiry, and no
validation loop**: unlike a form [ask](/concepts/interactions), 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:

<Frame caption="The web chat's ask-less form card — the prompt, the schema-rendered fields, and the send control.">
  <img className="block dark:hidden" src="https://mintcdn.com/tai42/xdHOAbW9fRf-H_xp/images/channel-web/form-card-light.png?fit=max&auto=format&n=xdHOAbW9fRf-H_xp&q=85&s=87b584a0a302e871d70a1954a7d5f620" alt="The web chat form card rendering a prompt and a schema-driven form with text, choice, and number fields above a send button." width="770" height="914" data-path="images/channel-web/form-card-light.png" />

  <img className="hidden dark:block" src="https://mintcdn.com/tai42/xdHOAbW9fRf-H_xp/images/channel-web/form-card-dark.png?fit=max&auto=format&n=xdHOAbW9fRf-H_xp&q=85&s=544ed5f95418dd127eee77fa971452ef" alt="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." width="770" height="914" data-path="images/channel-web/form-card-dark.png" />
</Frame>

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:

```bash theme={null}
tai notifications list
```

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](/concepts/interactions) Redis store,
so a deployment with that store unconfigured cannot record and lists an empty
inbox.

<Note>
  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.
</Note>

## See also

* [Interactions](/concepts/interactions) — the blocking counterpart, `ask_user`, and
  the form **ask** an ask-less form contrasts with.
* [Owned keys](/concepts/owned-keys#audience--one-concept-across-interactions-and-notifications) — the `audience` isolation model in full.
* [Client conversations](/concepts/client-conversations) — the channels a notification
  pushes to, and the structured participant message a form submission arrives as.
* [CLI reference](/reference/cli/notifications) — the full `tai notifications` surface.
