Skip to main content
responses.yml holds named text templates the framework delivers verbatim. Use it for wording that must be exact: legal disclosures, compliance notices, regulated confirmations. Everything else should stay prose so the agent sounds natural.
skills/card_replace/responses.yml
The top-level key is responses:. Each name maps to a list of variants. At delivery the engine selects one variant from that list (see Conditional variants below). A response with a single variant and no condition always selects that single variant. An entry is either a mapping with a text key, or a plain string:

Conditional variants

Add an optional condition on a variant to gate its wording on current memory. Conditions use the same expression grammar as skill requires: and prose if: lines.

How a variant is selected

When a response is delivered, the engine evaluates variants in YAML order:
  1. Skip variants with no condition during this pass.
  2. For each conditioned variant, evaluate its condition against readable memory. The first variant whose condition is true is delivered.
  3. If none match, deliver the default variant — the one that omits condition.
  4. If every variant is unconditioned, deliver the first variant.
If evaluating a condition raises an error at delivery time (for example a comparison the current memory type cannot satisfy), that variant is skipped and evaluation continues. When no conditioned variant matches, the default is used. If there is no default and nothing can be selected (every variant is conditioned and none match, or every condition fails evaluation), delivery fails closed the same way as a missing response name: the engine utters the response name as literal text and does not rephrase. metadata.rephrase applies to whichever variant is selected, not always the first variant. Other keys under metadata are delivered with the bot message on the tracker (see Variant metadata below).
When an utter: trigger fires with on: activate, variant selection runs after the skill’s memory is reset for that new run. Conditions that read that skill’s own fields therefore see each field’s initial_value (or null when unset) — not leftovers from a previous run. Unless those fields declare a matching initial_value, conditioned variants miss and the default variant is delivered.For activation wording that depends on facts already known in the session, gate on project memory (session.project.*) or set a skill-field initial_value that the condition can match. For values collected after the skill starts, use a skill utter: when: trigger (or deliver the response from a later step) instead of relying on skill memory at on: activate.

Default variant rules

When any variant on a response has a condition, exactly one variant must omit condition. That variant is the fallback when no condition matches. It may appear anywhere in the YAML list — first, last, or between conditioned variants. The engine skips default variants while evaluating conditions, then delivers that single fallback variant. rasa train rejects:
  • empty or non-list response entries (must be a non-empty variant list)
  • empty or whitespace-only text on any variant
  • invalid or empty condition syntax (must be a string expression)
  • memory references in a condition that are not declared in skill or project memory.yml
  • conditioned variants with no default, or more than one default
Train errors during project validation (rasa data validate, packaging): All project responses.yml files are checked; findings accumulate across files. Runtime model load (packaged agent) still uses strict registry parsing. Unknown variant keys there raise ValidationError with code responses.registry.invalid_variant and stop loading that file.

Unsupported variant keys

Permitted keys on a variant are text, metadata, condition, and buttons. channel: is not supported — adding it is reported at project validation time and causes strict registry load to fail at runtime.

Static buttons

Add an optional buttons: list on a variant to show tap targets with the response. Buttons are delivered on collect asks (utterance:), action steps (action:), and confirmation prompts (requires_confirmation). Inspector and other channels that render BotUttered.data["buttons"] show them on the ask; voice channels may summarize buttons in a compact format. Each button is a mapping with:
skills/card_replace/responses.yml
At delivery the engine resolves placeholders in each title and payload from readable memory, then attaches the wired buttons to the bot message and the output channel.

Plain-text payloads

When payload does not start with set:, a tap is handled like the customer typed that string — the orchestrator runs and the LLM may call set_fields or interpret the text. On a collect step this is the path for legacy-style buttons whose payload is the value itself (for example standard). Use plain-text payloads when you want the model to handle ambiguity or follow-up questions. Use set: payloads when the tap should write memory deterministically (see below).

Set-memory payloads

When payload starts with set:, it must match:
The value is everything after the first = (values may contain =). Examples:
rasa train validates each set: payload against the same rules the engine uses at tap time:
  • The memory key must be a declared field that the owning skill (or, for project-root responses, every skill that references that response) is allowed to write — the skill’s own fields or project memory. Button taps are engine writes, so llm_settable is not required (unlike LLM set_fields).
  • Placeholders in titles must reference readable memory for that observing skill.
  • For categorical targets, <value> must be one of the declared enum_values.
The payload value is always a string after the first =. At runtime the engine coerces it to the field type: At runtime a set: tap writes memory directly (no LLM set_fields call) when all of these hold:
  • The payload appears on the latest agent message’s buttons (older or forged set: text is stale).
  • The target is writable for the active skill (or project.).
  • The field exists and the value coerces to its type.
The channel still records a user utterance whose text is the raw set: string. That string is omitted from the LLM prompt for the turn. Collect steps advance because memory was written after the ask — not because the model interpreted the tap as an answer. Later turns show the filled value in the Memory section of the prompt. Behavior by scope: run_after_setting_<entry> memory validation tools run after a successful write, including when the field is not llm_settable. rasa train only accepts that tool when <entry> is llm_settable or owned by a collect: step — so collect shortcuts can have one; a field that is only button-writable cannot declare one. Tools named after project fields are invalid at train (project is not LLM-writable); write project.* from a skill-field memory validation tool or another tool instead. If a memory validation tool rejects the write, the write is rolled back and collect does not advance. The LLM may still respond (for example to explain the failure) using the button title when the channel supplies it, not the raw set: string. A listed button can still fail at runtime when a memory validation tool rejects the value even though train-time validation passed — design the tool and the copy accordingly. Malformed set: shapes (prefix set: but not the grammar above) fall back to the button title for LLM interpretation when the channel provides button_title metadata; without a title the engine utters utter_set_memory_button_skipped and does not call the LLM.

Inbound button_title metadata

Channels that deliver button taps should put the visible label on the inbound user message as metadata key button_title (a non-empty string). The engine reads that key when:
  • Filling {button_title} in utter_set_memory_button_skipped (when the title is missing, the quoted {button_title} token is dropped so the sentence stays grammatical — for example That selection is no longer available…)
  • Substituting the title for the raw set: string after a memory validation tool rejects the write, or after a malformed set: payload that has a title, so the LLM can interpret the tap without seeing the payload
If your channel does not forward button_title, skip notices still fire. A rejected write without a title still calls the LLM with the user text omitted from the prompt.

Stale-button default response

Packaged system defaults include utter_set_memory_button_skipped. It is also listed under Other packaged responses. Rephrase is off on the packaged default so untrusted button titles are not sent to an LLM:
Override it like any other built-in by declaring the same name in your project-root or skill responses.yml. Keep a {button_title} placeholder if you want the channel-supplied label in the notice.

Incoming-message reject default response

Packaged system defaults include utter_incoming_message_rejected. The engine sends it when modify_incoming_message crashes or times out. It is also used when HookRejection omits response_message, names a response that is not defined, or names a response whose conditioned variants all miss. Rephrase is off on the packaged default:
Override it by declaring the same name in your project-root responses.yml. For rejection-specific copy, define another response and pass its name—not free-form text—to HookRejection(response_message="utter_message_rejected", reason="..."). Named reject replies are resolved against the logged tracker the same way as other responses: conditioned variants, {session.*} placeholders, and static buttons apply. LLM rephrase is skipped because the orchestrator does not run.

Outgoing-text reject default response

Packaged system defaults include utter_outgoing_text_rejected. The engine sends it when modify_outgoing_text raises HookRejection without a usable response_message. Rephrase is off on the packaged default:
Override it by declaring the same name in your project-root responses.yml. For rejection-specific copy, pass a response name to HookRejection(response_message="utter_response_blocked", reason="...").

Turn-error fallbacks

When Mantle must abort a turn because a fail-closed modify hook crashed or timed out, or because the main model call failed, it speaks a packaged response. These names are also listed under Other packaged responses.
Override any of these names in your own responses.yml. The exception text is never included. metadata.rephrase on these responses is ignored, because the model call cannot be trusted on this path. If a name is missing or no variant matches, Mantle speaks the packaged sentence above rather than the response name. Other hooks do not use the hook-error responses: fail-open points keep the last good payload, knowledge-query failures skip the search and continue the turn, and HookRejection / RetryModel keep their own contracts.

Dynamic buttons

Use a local source tool when the choices are only known at delivery time. The variant names the tool instead of declaring a static list:
skills/delivery/responses.yml
The source tool calls context.set_buttons() with Button values:
skills/delivery/tools.py
Button fields control the tap payload: One set_buttons() call may mix memory-writing and plain-text buttons, as in the example. Calling it again replaces the previous list. The source buffer applies only to the named response: a message emitted with context.send() remains a separate message without those buttons. Source tools are engine-only. They are omitted from the LLM tool schemas and a direct model-authored call is rejected. rasa train also requires a source to:
  • resolve to a local tool for the owning skill, or for every skill that references a project-root response
  • not be an MCP tool
  • not use requires_confirmation
When a source fails, supplies no buttons, or supplies only invalid buttons, the response text is still sent without buttons. Invalid items are skipped individually, so valid siblings remain. This includes malformed buttons and ref values that are unknown or not writable by the active skill.
A variant’s buttons: value is either a static list or a {source: ...} mapping. Put mixed static and runtime choices in the source tool’s single set_buttons() call; they cannot be mixed in YAML.

Where the file lives

At load, every file is merged into one flat registry, later sources overriding earlier ones on duplicate names:
  1. Packaged system defaults
  2. Each bundled default skill’s responses.yml
  3. Each project skill’s responses.yml, in directory order
  4. The project-root responses.yml
This is how you override a built-in message: declare the same response name in your own file. Redefining utter_silence_timeout_check_in, for example, replaces the check-in the bundled default_silence_timeout skill delivers. The same merge order applies to utter_set_memory_button_skipped and the turn-error fallbacks (utter_model_request_hook_error, utter_tool_call_hook_error, utter_model_call_error). The bundled default_session_start skill has no greeting response. To greet, override the skill as described in Start a conversation. Because the registry is flat, response names are global rather than scoped to the skill that declares them. Prefix names with the skill (utter_card_replace_completed) so two skills keep their wording distinct.

Interpolation

A placeholder in braces is replaced with a memory value at delivery time. Two forms work, and they resolve differently:
Use the namespaced form to read another skill’s public field. The bare form is convenient for a skill’s own entries and follows the same resolution order a tool’s context.memory.get() uses. A placeholder whose value is unset at delivery time is left in the text as written, so interpolate only values you know are set by the time the response is sent, typically ones written earlier in the same ordered block. Interpolation applies to responses. For live values in skill.md instruction prose, use @memory.… instead — see skill.md. Do not put bare session.* in free prose; train fails and points you at @memory.

metadata.rephrase

A response may opt into being reworded by the LLM instead of delivered verbatim:
With rephrase: true the orchestrator makes a tool-free LLM call to reword the selected variant’s text in context, and falls back to the literal text if that call fails or comes back empty. Leave it off (the default) for anything compliance-sensitive: that is the entire point of declaring the response.

Variant metadata

Every key under metadata on the selected variant is stored on the BotUttered event for that delivery (for example variant_id for channel routing). The engine adds its own keys (utter_action, mantle_response_source, timing); those win if a name collides with authoring metadata. Buttons on the variant are independent of metadata — declare them on the variant root as buttons: (see Static buttons).

How a response is delivered

Four things reference a response by name: The two step fields differ:
utter: is accepted as a step-level alias for action: and is normalised at load. Prefer action:. rasa train validates every response name referenced from any of the four places above, and runs per-variant checks on project-authored responses.yml files (non-empty text, valid conditions, default-variant rules). A name that is not in the registry, or a malformed variant list, is caught before the agent runs.

See also