responses.yml and
have the framework deliver it exactly as written. The LLM never rewrites it.
Defining responses
Response text lives in the skill’sresponses.yml:
skills/card_replace/responses.yml
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
Addcondition 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, static buttons, 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 frontmatterutter:
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. Do not also ask for that
approval in skill prose — the engine already asks. 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:Static buttons
Variants may declare abuttons: list so collect asks, utter steps, and
confirmation prompts show tap targets alongside the response text. Each button
has a title (required) and an optional payload. Placeholders in titles and
payloads resolve from memory at delivery the same way as in text.
- Plain-text payload (or no payload — then the resolved title is used):
treated like typed user input. The LLM can call
set_fieldson a collect step. set:payload (set:session.<skill|project>.<entry>=<value>): on tap, the engine writes memory itself (llm_settablenot required), runs memory validation tools, and does not put the raw payload in the LLM prompt. Prefer this for deterministic collect shortcuts. A successful tap still stores a user utterance on the tracker (the rawset:string); the engine then continues the turn, so a collect whose field was just written advances without asking the LLM to record the answer.- If the tap is stale, not writable, or the value cannot be applied, the
engine utters the packaged default
utter_set_memory_button_skipped(overridable; rephrase is off on the packaged copy) and ends the turn without calling the LLM. - Channels should send the visible label as inbound metadata
button_titleso that notice can name the button.
- If the tap is stale, not writable, or the value cannot be applied, the
engine utters the packaged default
set:
targets against button-writable memory (declared own-skill and project
fields). See Static buttons for
payload grammar, stale-notice behavior, button_title metadata, and
memory validation tools.
Dynamic buttons
When choices depend on live data, setbuttons: {source: <tool_name>} on the
response variant. The engine runs that local tool at delivery time, and the
tool supplies the choices with context.set_buttons([Button(...)]).
A Button with ref writes its value to that fully qualified memory field
on tap. A button without ref sends value, or its title when no value is
set, as plain user input. One call can mix both kinds.
Button-source tools are internal to the engine: they are hidden from the LLM
and cannot require confirmation or come from MCP. If the source fails or
supplies no valid choices, the response text is still delivered without
buttons.
See Dynamic buttons for the YAML and
Python example, validation rules, and failure behavior.
Overriding built-in messages
Bundled skills and a few packaged responses ship default wording. The full list, including when each message is sent, is located in Default skills. Everyresponses.yml merges into one registry keyed by name, so declaring a
name in your project-root file replaces the built-in one:
responses.yml
Reference
For the merge order, both interpolation forms, conditional variants, and every field, see theresponses.yml reference.