Skip to content

AML: Anychat Markup Language

Teach any LLM agent to speak rich messaging in one page.

AML is plain conversational text plus lightweight [[...]] markers. Anychat compiles AML into AMF messages and renders each marker with the richest feature the person's channel supports, such as an Apple time picker, RCS chips, or a numbered SMS list, so the agent never adapts its output to the channel.

Three properties make AML LLM-friendly:

  • Plain text is always valid AML. A model that emits no markers still works everywhere.
  • Markers degrade safely. Unknown or malformed markers are dropped with a warning. A malformed marker never fails hard and never reaches the person.
  • Answers come back as ids. When the user answers a marker (tap or text), the agent receives the option id it chose, on every channel.

Use AML by returning text/anychat-aml from your webhook, by compiling via POST /v4/compile, or by writing it into an Anychat agent's reviewed copy (see below).

Where AML applies

AML is written by people and rendered by Anychat.

In an Anychat agent, you write AML in the copy you review: message templates and Fact answers. The agent does not write markers. It selects your copy by id, and Anychat renders it exactly as written, degraded to what the person's channel supports.

Where What you write
Message Template body Full AML
Fact answer Full AML
Outreach openings and follow-ups Full AML, because these are message templates
Playbook body Presentation markers, as a description of how the reply should look. Not live-data markers
Knowledge Sources None. Sources are retrieved to answer questions, and markup in them is not rendered
Your own agent, via the Messaging API Full AML, returned as text/anychat-aml

A Playbook body is instructions to the agent, not copy that gets sent, so it is never compiled. AML is still the clearest way to say how a reply should look, and the agent reads it that way:

End the turn with [[chips: Boat rentals | Dining | Activities]].
When the user asks about dining, send them [[template: resort-dining-carousel]].

The agent renders those itself, through its own reply fields, and adapts them to the person's channel. Presentation markers (chips, card, carousel, things, collection, template, media) are all fine in a Playbook.

Markers carrying live data are not, because a Playbook cannot know it. A time picker's slot ids exist only after the agent checks availability during the conversation, so ids written into a body are invented and the picker books nothing. Describe the intent instead ("offer times from the calendar"). Behavior lint reports an authored picker as playbook.aml.livedata.

When the exact words matter every time, put them in a message template and name it from the Playbook, as in the second line above. The template's copy is sent verbatim; a description in the body is not.

Two references resolve wherever you write AML:

  • [[template: <id>]] pulls in another message template, one level deep. A template can be composed of other templates; a template pulled in this way is rendered as-is.
  • image:<id> names an item in the agent's media library, valid anywhere media: is.

An id that does not exist is dropped and the rest of the message still sends. The console's Behavior lint reports unknown ids as template.ref.unknown and media.ref.unknown.

The skill

If you run your own LLM agent, paste the speak Anychat skill, the machine-readable cheatsheet below, into your system prompt. It is published as AML_SKILL_PROMPT in the @anychat-ai/messaging-api package so your prompt and Anychat's parser version together.

Marker reference

Quick replies

Do you believe in ghosts?
[[chips: Yes | No | Tell me more]]

Tappable chips (≤5 recommended, short labels). Label = value sets a distinct value to receive when tapped: [[chips: Yes=opt-yes | No=opt-no]].

Ask the user to choose: ask.choice

[[ask.choice #treatment | Which treatment are you interested in?
botox = Botox ~ Smooths fine lines | media: https://example.com/botox.jpg
filler = Dermal filler ~ Restores volume]]

One option per line: id = Label ~ description | media: url. The #id names the intent; the chosen option's id comes back in the result event. Renders as a list picker (AMB), chips or cards (RCS), or a numbered list (SMS). The user can always type their answer instead of picking; unambiguous replies resolve to the same result event. A single option never renders as a picker: it becomes one chip, or a "reply YES to take it" sentence on SMS.

Offer times: ask.time

[[ask.time #booking | When works best?
tue-345 = 2026-06-10T15:45:00-07:00 ~ Tue 3:45 PM
wed-10 = 2026-06-11T10:00:00-07:00 ~ Wed 10:00 AM]]

Slot lines are id = startISO ~ label. The ISO time is authoritative (include the UTC offset). The label is optional, and Anychat generates one per locale and channel when omitted. Renders as an Apple time picker, RCS slot chips, or a numbered SMS list. A single slot renders as one chip (or a "reply YES to take it" sentence on SMS) rather than a one-row picker.

Confirm: confirm

[[confirm #meet-bob | Confirm your meeting with Bob for Tue at 3:45 PM PT?
title = Intro call with Bob
when = 2026-06-10T15:45:00-07:00
tz = America/Los_Angeles
yes = Confirm
no = Pick another time]]

Result is optionIds: ["yes"] or ["no"]. On SMS, "YES" and "NO" replies resolve identically. Supplying when/title lets capable channels attach the event to the confirmation.

Showcase products or options: showcase

[[showcase #pick
prod-1 = Botox ~ From per-unit pricing | media: https://example.com/botox.jpg | price: 12.00 USD
prod-2 = Dermal filler ~ Volume restore | media: https://example.com/filler.jpg]]

Renders a carousel with Select chips, or a numbered digest on SMS. The #id is optional; include it to receive selection results. A single-item showcase is informational: it renders as a plain card (no Select chip), the visual anchor for the thing being discussed rather than a menu.

[[card | Title ~ Short description | media: https://example.com/img.jpg | link: Learn more = https://example.com]]

[[carousel | width: medium
Our clinic ~ Open 9-5 | media: https://example.com/clinic.jpg | link: Directions = https://example.com/map
Our team ~ Board-certified | media: https://example.com/team.jpg | reply: Meet the team
Financing ~ Plans from $99/mo]]

A card is Title ~ description plus optional fields: media: url (the image), and buttons, written as link: Label = url, reply: Label, or dial: Label = +1…, which may repeat and render as the card's chips in order. A carousel is one card per line (up to 10; channels with a smaller carousel limit get it split), with an optional width: narrow|medium|wide in the head. On channels without cards (SMS) each card degrades to "Title: description" text with the image link on its own line.

Agents with a media library write media: image:<id> instead of a URL in any of these markers. The platform substitutes the hosted file at send time and drops the media (never a broken link) if the item has since been removed.

Everything else

[[media: https://example.com/photo.jpg]]
[[link: See pricing | https://example.com/pricing]]
[[dial: Call us | +14155551212]]
[[event: Intro call | 2026-06-10T15:45:00-07:00 | 2026-06-10T16:15:00-07:00 | Optional description]]
[[place: Our clinic | 37.7749,-122.4194 | 123 Main St, San Francisco]]
[[ask.location #loc | What's your ZIP code so I can find the nearest location?]]
[[template: welcome-back | firstName=Ann]]

link and dial attach as chips to the surrounding message. event and place degrade to add-to-calendar / map links on plain channels. template references a message template registered with your agent.

Multiple bubbles

Separate distinct messages with [[next-message]]:

Got it, you're all set for Tuesday.
[[next-message]]
Anything else I can help with?
[[chips: Reschedule | No, thanks]]

Grammar notes

  • Markers are block-level: [[name #id flags | head fields]] with optional newline-separated option lines. Head fields are |-separated.
  • #id names an intent. Omit it and one is generated; supply it whenever your agent wants to recognize the answer.
  • Option lines are id = Label ~ description | key: value | key: value.
  • Inline markdown in text (bold, italic, links, lists) is rendered or stripped per channel.
  • Unknown markers are dropped with a warning; agent runtimes may register extensions (the Anychat runtime adds [[things: ...]], a comma-separated list of Thing ids or names, and [[collection: <id> limit=N]], every Thing in a collection up to the channel's carousel limit; [[products: ...]] still works as the historical spelling of things:). In a Message Template, an entry may be a {…} fill-in, resolved per recipient; one that matches no Thing sends no card.