> ## Documentation Index
> Fetch the complete documentation index at: https://mantle.rasa.com/llms.txt
> Use this file to discover all available pages before exploring further.

# When prose is not enough

> What to put in memory, responses, constraints, and ordered blocks when a sentence in the skill body is not enough.

A skill is read twice: once to choose it, and again while the skill is active.
These practices are for that second reading, and for the memory fields and
tools the model uses along the way. [Writing prose](/best-practices/writing-prose) covers
how to brief the body. This page covers the cases where a sentence in the body
is not enough.

## State when the skill is finished

Say when the skill is done in two places. `complete_when` in the skill
frontmatter is the gate the engine checks. A sentence in the body, at the step
where the work ends, tells the model when to call `complete_skill`.

Put the facts that must be true before the skill can end in
[`complete_when`](/reference/skill-md#complete_when). Use `condition` for what
memory can show, and `criteria` for what needs reading the conversation. For
example, a SIM skill can end only once a replacement is ordered or the user
declines one, and a tool sets `replacement_settled` when either happens:

```markdown skills/sim_management/skill.md theme={null}
---
name: SIM Management
description: SIM card - block, replace, or check status
complete_when:
  condition: "session.sim_management.replacement_settled"
---
```

A `condition` with no `criteria` also completes the skill on its own once that
condition is true.

Sometimes the model calls `complete_skill` too early. `complete_when` refuses
to complete the skill, and the skill stays open. It does not tell the model
when to try, so the body sentence is what cuts the number of refused calls. Add
it next to the step that looks like the end but is not. For example:

```markdown skills/sim_management/skill.md theme={null}
Block the SIM with @tool.block_sim. Blocking the SIM does not complete this
skill. Complete it after the replacement is ordered, or after the user
declines one.
```

A skill without `complete_when` needs the body sentence alone. Write it at the
step that finishes the job. For example, a status skill completes after it has
given the facts from the tool and the next step. An advice skill completes
after the answer, so it does not stay open until something cancels it:

```markdown skills/card_advisory/skill.md theme={null}
Once you have answered with the numbers from the tool, call @tool.complete_skill.
```

Word each `criteria` item so a later turn cannot undo it. The judge reads the
whole conversation and fails closed, so a refused `complete_skill` leaves the
skill open and the model writes its own ending. For example, "states the
answer to the user's question" fails when the user's last message is "thanks,"
because that message has no answer. Write "answered the user's question at
least once; a later thank-you or decline does not undo that."

## Describe every memory field the model can set

A name and a type are not enough for a field the model writes. The
[description](/reference/memory-yml) says what the memory item is about. When a
similar answer could be mistaken for it, also say which user answer sets the
field and which answers leave it unset. For example, a skill asks whether to
lock a card after it is reported lost or stolen:

```yaml skills/card_replace/memory.yml theme={null}
wants_card_locked:
  type: bool
  llm_settable: true
  description: >
    The user's consent to lock the card.
    Set only after the user answers the lock question.
    "My card was stolen" does not set this.
```

For example, a field that records whether the user recognizes their recent
charges is set from their answer when the list is shown, not from an earlier
remark like "I think someone used my card."

A description is prose, not a control. An `llm_settable` field can be written
on any turn of the skill, so the model can still fill it from a remark before
the question is asked. When a wrong early write is costly, reject it in a
[memory validation tool](/reference/tools#memory-validation-tools), not in the
description. The tool returns an error while the question has not been asked,
and the engine rolls the write back. See
[Close a side effect until its facts exist](#close-a-side-effect-until-its-facts-exist)
for why this matters to a gate.

## Write a value to memory when a later turn needs it

Passing a value into a tool does not store it. The result of a one-time check
is not kept either. If a later turn needs the fact, write it to memory when you
learn it. For a value the user gives, make the field `llm_settable` and tell
the model to record it with `@tool.set_fields`. For a result, have the tool
write it to memory.

Keep one copy. A tool that takes the same value as an argument gives the model
two places to write it, and they can differ. Let the tool read the field from
memory and gate it on that field with `requires`.

For example, the user chooses an eSIM or a physical SIM. A later response still
says which SIM was ordered, so record the choice first, and let the order tool
read it:

```markdown skills/sim_management/skill.md theme={null}
---
tool_constraints:
  - order_replacement_sim:
      requires: "session.sim_management.sim_type"
---

When the user decides, record sim_type with @tool.set_fields, then call
@tool.order_replacement_sim.
```

For another example, a one-time check rather than a value the user said: a
tool checks once whether rush shipping is available and writes that result to
memory. Later turns read the field, so the model does not offer rush shipping
to a user who cannot get it.

## Close a side effect until its facts exist

Add [`requires`](/reference/constraint-table) on the tool that places an order,
a lock, or another side effect, so the tool stays unavailable until the facts
it depends on exist.

For example, the tool that orders a replacement card stays hidden until the
user has picked a shipping speed and the rush check has run:

```yaml skills/card_replace/skill.md theme={null}
tool_constraints:
  - process_card_replacement:
      requires: "session.card_replace.shipping_type and session.card_replace.rush_eligible != None"
```

A gate is only as strong as the ways its fields can be written. Before you
rely on `requires` or a precondition, list how each field it reads can be set:
by the model through `set_fields`, by a `collect:` step, by tool code, or by a
`set:` button. Close the paths that should not open the gate. In the example,
`shipping_type` is `llm_settable`, so the model can set it from a remark and
open the gate. If that is not acceptable, reject the early write in a
[memory validation tool](/reference/tools#memory-validation-tools), or write
the field from tool code only.

## Use a response when the words have to be exact

When the user has to hear a specific fact, put the line in
[`responses.yml`](/reference/responses-yml) and attach it to the tool with
`on_success`. Do not leave that sentence for the model to restate. The model
may summarize a result and drop the detail the user needs.

For example, after a device lookup the user needs the exact serial number for a
police report:

```yaml responses.yml theme={null}
responses:
  utter_device_info_shared:
    - text: Your device serial number is {session.sim_management.serial_number}.
```

```yaml skills/sim_management/skill.md theme={null}
tool_constraints:
  - get_device_info:
      on_success: utter_device_info_shared
```

The same applies to a tool that only writes memory. For example, a tool that
locks a card and stores `card_locked` says nothing to the user. Attach a
response if the user has to hear that the card is locked.

`on_success` delivers the line, and then the turn goes on. The model gets
another call in the same turn and can add its own message behind the exact
line: a question it already knows the answer to, a restated duration, or an
offer nobody asked for. When nothing may follow the line, deliver it from an
[ordered block](/build-guide/ordered-blocks) that ends with `action_listen`.
That ends the turn without a model call:

```markdown skills/sim_management/skill.md theme={null}
:::ordered_block id=share_device_info
steps:
- id: lookup
  execute_tool: get_device_info
  next: read_back

- id: read_back
  action: utter_device_info_shared
  next: wait

- id: wait
  action: action_listen
  next: END
:::
```

The cost is that a second question in the same user message is not answered,
because the turn ends after the line. A verbatim line also repeats verbatim.
If the user asks for more on the same topic, running the block again gives
them the same text. Give the model another move for that case, such as a tool
that says there is nothing further, or leave the follow-up to prose.

## Keep a second fact out of a tool result

The model speaks what a tool returns. If a payload mixes the fact the user
asked for with a fact that belongs on another path, split the tools or drop the
extra field.

For example, a device lookup returns the serial number and also instructions
for finding a lost phone. A user whose phone was stolen asks only for the
serial number, and the model reads out the location instructions too. Return
only the serial number from the lookup tool. Move the location instructions to
a separate tool that only the lost-phone path offers.

The other side holds too: a fact that is not in the result is not said. Put
every fact the answer needs in the result. For example, a coverage lookup that
returns the rebate but not the remaining limit makes the model say "covered,
50% back" to a user who has nothing left to claim.

When an instruction applies only to the turn a tool ran, put it in the result
as well. Writing prose shows this with a
[`next_step` field](/best-practices/writing-prose#let-the-tool-result-carry-what-happens-next).
For example, "this list is complete, so a service not in it is not covered"
in the tool description was not enough. The same sentence in the result was.

## Put a limit in the skill description

If a skill must not carry out a change, say so in the skill description. A line
in the body alone is not enough: the model still tries to make the change while
the skill is active. For example, a skill that explains card options should not
change the user's card:

```yaml skills/card_advisory/skill.md theme={null}
description: Card advice - explain the options. This skill cannot process a product change.
```

When the user asks to upgrade, the skill explains the options instead of trying
to change the card.

Say which failure the limit prevents. If the skill has a tool for the change,
the risk is that the tool runs. Gate that tool with `requires`, or leave it
out of the skill. If the skill has no such tool, the risk is a claim: the model
says the change happened when nothing did. The description is what stops that
claim.

## Continue from a choice the user already made

Record the choice in memory, and have the next step read that field. Ask only
when it is still unset.

This is for preferences: a shipping speed, a SIM type, a reason. Do not use it
for a field that a `requires` gate or a precondition reads. Letting the model
fill that field from an earlier remark opens the gate without the question it
was meant to protect. See
[Close a side effect until its facts exist](#close-a-side-effect-until-its-facts-exist).

Make the field `llm_settable`. At the step that would ask, tell the model to
write a value the user already gave with `@tool.set_fields` and continue. Point
the steps that follow at that field with
[`if:`](/build-guide/scoped-instructions), so the skill follows the choice
instead of offering it again.

For example, if the user has already said why they need a replacement, record
the reason and follow that path:

```markdown skills/card_replace/skill.md theme={null}
If the user has already said why they need a replacement, record it with
@tool.set_fields(replacement_reason=...) and continue.
Ask only when the reason is still unknown.
```

A `collect:` step asks only while its field is unset, unless you set
[`ask_before_filling`](/reference/ordered-block-steps). Record the value before
that step.

For example, if the user already named a shipping speed, record
`shipping_type` before the shipping collect step, so the step does not offer
both speeds.

When the named choice is the consent and the action is cheap to undo, record
it and call the tool. Do not add a separate yes or no question.

For example, naming a wallet is the consent to add it. Record the provider and
provision the card. Do not ask whether they want a wallet first.

For an action that is hard to undo, such as a transfer to a human agent or a
payment, ask. Otherwise the model decides what counts as consent, and a remark
becomes a yes.

## Tie a step to the fact that makes it true

Place a step right after the fact it depends on, not after a step the user
might skip or reorder. Scope it with `if:` on that fact, so it appears as soon
as the fact is known.

For example, a user whose phone was stolen should always be told to report the
theft to the police. If that advice sits after the replacement order, a user
who asks only to block the SIM never hears it. Put it under the stolen branch,
before blocking or ordering:

```markdown skills/sim_management/skill.md theme={null}
if: session.sim_management.incident_reason == 'stolen'
Advise the user to report the theft to the police. Say this before you
block the SIM or look up device details, even if the user asks for those first.
```

You can also deliver that line the first time the fact is set, without asking
the model to remember to say it. Use [`utter`](/reference/skill-md#utter) with
`when:`:

```yaml skills/sim_management/skill.md theme={null}
utter:
  - utter_sim_incident_advice:
      when: "session.sim_management.incident_reason == 'stolen'"
```

## Repeat a rule in the section it governs

A field description, or a rule in [`agent.yml`](/reference/agent-yml), is not
always enough. Restate the instruction in the section of the skill it governs.
The model follows it more thoroughly there than from a line it read earlier.

[Writing prose](/best-practices/writing-prose#put-a-rule-where-its-cost-matches-its-reach)
already says this is the one place repeating yourself pays: a rule at the top
of a long body is far from the section it governs. The same holds for a memory
description and for an agent-wide rule.

For example, a memory description says a lock flag is set only after the user
answers the lock question, and that "my card was stolen" does not set it. Say
that again in the section for a stolen card, where the model decides whether
to ask. If `agent.yml` says to speak an identifier only as a tool returned it,
say that again in the section that reads the lookup back.

Write that instruction in one place. When a `collect:` step collects the field,
its description is the one the model sees, and it takes preference over the
memory field's description. Do not set both.

## Put a required call in an ordered block

A prose instruction that names a tool does not make the model call it. When a call must
happen, put it in an [ordered block](/build-guide/ordered-blocks) and point at
it with `@block`.

For example, after a SIM is blocked, the device lookup must always run. A
sentence like "then look up the device" is sometimes skipped. An ordered block
runs the lookup without asking the model:

```markdown skills/sim_management/skill.md theme={null}
1. Invoke @block.block_sim_flow to block the SIM and look up the device.

:::ordered_block id=block_sim_flow
steps:
- id: do_block
  execute_tool: block_sim
  next: share_device_info

- id: share_device_info
  execute_tool: get_device_info
  next: END
:::
```

The sentence that names the block is still prose, and the model can skip it
too. In a skill with a body, "Invoke `@block.main` first" is sometimes ignored:
the model answers on its own and the block never runs. If the call must happen
whenever the skill runs, make the skill blocks-only. The body is one block and
nothing else, so activating the skill runs it. See
[Fully controlled](/build-guide/ordered-blocks#fully-controlled).

For example, a handoff skill has to check that a human agent is available and
ask for consent before every transfer. As a blocks-only skill, both steps run
on every activation:

```markdown skills/handoff/skill.md theme={null}
---
name: Handoff
description: Human agent - transfer the conversation when the user asks for one
---

:::ordered_block id=main
steps:
- id: check_availability
  execute_tool: check_agent_availability
  next: ask_consent

- id: ask_consent
  collect: wants_transfer
  utterance: utter_ask_transfer
  ask_before_filling: true
  next: END
:::
```

## Reference

* [`complete_when`](/reference/skill-md#complete_when) for the completion gate
* [Memory](/reference/memory-yml) for `llm_settable` and field descriptions
* [Memory validation tools](/reference/tools#memory-validation-tools) for rejecting a write
* [Tool constraints](/reference/constraint-table) for `requires` and `on_success`
* [Responses](/reference/responses-yml) for lines the user has to hear verbatim
* [Scoped instructions](/build-guide/scoped-instructions) for `if:`
* [Ordered blocks](/build-guide/ordered-blocks) for a call that must happen


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.