Skip to main content
Some moments need specific wording: a recording notice at the start of a call, a fraud warning when a card is reported stolen, a legal disclaimer after a transaction. Responses let you declare that text once in responses.yml and have the framework deliver it exactly as written. The LLM never rewrites it.

Defining responses

Response text lives in the skill’s responses.yml:
skills/card_replace/responses.yml
Each name maps to a list of variants. At delivery the engine picks one variant: the first whose condition is true (YAML order), or the single default variant with no condition when none match. Placeholders in braces are filled from memory at the moment the message is sent. Use {session.<skill_id>.<entry>} or {session.project.<entry>} to read any value the skill can see, or the short {entry} form for the skill’s own values. Interpolate only values you know are set by then: a placeholder with no value is left in the text as written.

Conditional wording

Add condition on a variant when the message should change with session memory — for example, different replacement-fee copy for lost vs stolen cards. When any variant is conditioned, exactly one variant must omit condition (the fallback). That fallback may sit anywhere in the list. This is different from a skill utter: trigger with when: in skill.md: when: fires a whole response once when memory first becomes true; condition on a variant picks which wording to deliver each time a reference (action step, collect ask, tool outcome, confirmation prompt, etc.) resolves that response name. Both use the same condition expression grammar. See the responses.yml reference for selection order, train-time validation, and unsupported keys.

Delivering a response

Four things can fire a response, and which one you reach for depends on what the wording is tied to. On skill activation, or when a value becomes true. A frontmatter utter: trigger. This is the one that needs no ordered block:
skills/card_replace/skill.md
on: activate fires once when the skill becomes active. when: fires the first time its condition becomes true after a memory write, so a warning tied to a value lands the moment that value is recorded, wherever in the conversation that happens. Around a tool call. on_success: and on_failure: on a tool_constraints entry deliver a response after the tool runs, and requires_confirmation: names the approval question and the line for a declined one. See Tool Constraints. At a fixed point in a sequence. Ordered-block steps:
action: delivers the response and advances. utterance: on a collect: step is the question asked for that value.

When to use a response

Reach for one when the wording is a business requirement: compliance, legal, brand-mandated disclosures. Use it when the exact words are the point. For everything else, let the LLM phrase things from your instructions. Declaring every message as a response produces an agent that sounds like a scripted phone tree, which is the thing Mantle exists to avoid. If you want declared text that still adapts to context, a response can opt into LLM rewording:
Rephrase applies to whichever variant is selected at delivery, not always the first variant. Do not set this on anything compliance-sensitive.

Dynamic buttons

When the tap targets under a message depend on live data, set buttons: {source: <tool_name>} on the response variant. Examples include the shipping windows a warehouse still has open, or the last four accounts a caller used. The engine calls that local tool at delivery time, and the tool supplies the choices with context.set_buttons([Button(...)]).
skills/delivery/responses.yml
A Button with ref writes its value to that fully qualified memory field on tap, so a collect step advances without a follow-up set_fields call from the LLM. A button without ref sends value (or title when value is omitted) as plain user input. One set_buttons() call can mix both kinds. Source tools are engine-only. They are hidden from the LLM tool inventory and the model cannot dispatch them directly. A source tool cannot come from MCP or carry requires_confirmation, and rasa train rejects any of those cases. If the source tool is missing, a barge-in cancels it, it raises, or it supplies no valid buttons, the engine still delivers the response text without buttons. The engine drops invalid entries individually, so valid siblings survive. See Dynamic buttons for the YAML and Python shape, validation rules, and failure behavior.

Overriding built-in messages

The bundled default skills (wrap-up, resume-after-digression, silence timeout) ship their own responses. Every responses.yml merges into one registry keyed by name, so declaring a name in your project-root file replaces the built-in one:
responses.yml
Session start is different. The bundled skill has no greeting to override. To make the agent speak first, replace the skill itself. See Start a conversation. That shared registry is also why response names are worth prefixing with the skill they belong to.

Reference

For the merge order, both interpolation forms, conditional variants, and every field, see the responses.yml reference.