Skip to main content
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 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. 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:
skills/sim_management/skill.md
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:
skills/sim_management/skill.md
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:
skills/card_advisory/skill.md
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 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:
skills/card_replace/memory.yml
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, 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 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:
skills/sim_management/skill.md
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 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:
skills/card_replace/skill.md
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, 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 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:
responses.yml
skills/sim_management/skill.md
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 that ends with action_listen. That ends the turn without a model call:
skills/sim_management/skill.md
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. 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:
skills/card_advisory/skill.md
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. 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:, 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:
skills/card_replace/skill.md
A collect: step asks only while its field is unset, unless you set ask_before_filling. 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:
skills/sim_management/skill.md
You can also deliver that line the first time the fact is set, without asking the model to remember to say it. Use utter with when::
skills/sim_management/skill.md

Repeat a rule in the section it governs

A field description, or a rule in 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 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 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:
skills/sim_management/skill.md
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. 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:
skills/handoff/skill.md

Reference