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, 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 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.
on: activate resolves response variants after that skill’s memory is reset for the new run. A conditional variant that reads the skill’s own fields usually misses (those fields are back at initial_value / null) and you get the default wording. Prefer project memory or a matching skill-field initial_value for activation CRVs; use when: when the value is written after the skill starts. See Conditional variants.
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:
Rephrase applies to whichever variant is selected at delivery, not always the first variant. Do not set this on anything compliance-sensitive.

Static buttons

Variants may declare a buttons: 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_fields on a collect step.
  • set: payload (set:session.<skill|project>.<entry>=<value>): on tap, the engine writes memory itself (llm_settable not 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 raw set: 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_title so that notice can name the button.
Train-time validation checks button shape, readable placeholders, and 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, set buttons: {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. 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.