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
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 optionalcondition 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:- Skip variants with no
conditionduring this pass. - For each conditioned variant, evaluate its
conditionagainst readable memory. The first variant whose condition is true is delivered. - If none match, deliver the default variant — the one that omits
condition. - If every variant is unconditioned, deliver the first variant.
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).
Default variant rules
When any variant on a response has acondition, 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
texton any variant - invalid or empty
conditionsyntax (must be a string expression) - memory references in a
conditionthat are not declared in skill or projectmemory.yml - conditioned variants with no default, or more than one default
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 aretext, 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 optionalbuttons: 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
title and payload from
readable memory, then attaches the wired buttons to the bot message and the
output channel.
Plain-text payloads
Whenpayload 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
Whenpayload starts with set:, it must match:
= (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_settableis not required (unlike LLMset_fields). - Placeholders in titles must reference readable memory for that observing skill.
- For categorical targets,
<value>must be one of the declaredenum_values.
=. 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.
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}inutter_set_memory_button_skipped(when the title is missing, the quoted{button_title}token is dropped so the sentence stays grammatical — for exampleThat selection is no longer available…) - Substituting the title for the raw
set:string after a memory validation tool rejects the write, or after a malformedset:payload that has a title, so the LLM can interpret the tap without seeing the payload
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 includeutter_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:
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 includeutter_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:
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 includeutter_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:
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.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
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
ref values that are unknown or not writable by the active skill.
Where the file lives
At load, every file is merged into one flat registry, later sources
overriding earlier ones on duplicate names:
- Packaged system defaults
- Each bundled default skill’s
responses.yml - Each project skill’s
responses.yml, in directory order - The project-root
responses.yml
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: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:
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 undermetadata 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
- Responses concept: when to declare wording
- skill.md: the
utter:trigger andtool_constraintsshapes - Ordered block steps: the step fields that deliver responses (including collect asks with buttons)
- Conditions: the namespaced reference form