skill.md is the only required file in a skill folder. It is YAML frontmatter
followed by a markdown body.
skills/card_replace/skill.md
--- on the first line and close the frontmatter
with a second ---. A missing delimiter fails with
skill.markdown.invalid_frontmatter_delimiter.
The skill id is the folder name
The skill id — used for routing, memory namespacing, and@skill. references — is always the parent directory name, never the name: field.
A skill in skills/card_replace/ with name: Card Replace is:
- referenced in prose as
@skill.card_replace - namespaced in memory as
session.card_replace.<entry>
Frontmatter properties
Unknown keys (for example a
descripton: typo) fail compile with
skill.markdown.unknown_frontmatter_key. name and description must be YAML
strings — a number or boolean is rejected the same way a non-boolean hidden
is.
Write description as a routing summary, including the phrasings a customer
would actually use. Skill retrieval embeds the skill name and this text
(and, by default, the skill’s memory field schema).
Skill-level requires is not supported. Use precondition when another skill
must run first, hidden: true to keep a skill off the routing catalogue, and
tool_constraints requires: to gate tools.
precondition
A semantic name. Bind it in agent.yml
(orchestrator.preconditions) so the engine parks this skill, runs resolve_with,
then continues or abandons. When resolve_with is already running, the engine
reuses that skill instead of interrupting it.
hidden
When true, the skill is omitted from the routing catalogue. Deterministic
call: / link: steps and a precondition resolve_with may still start it.
Prose @skill mentions of a hidden skill fail validation.
complete_when
Skill-level completion contract. Must be a mapping with at least one of:
condition:; keep criteria for things that need
reading the conversation. Duplicate strings are rejected at compile time. When a
check fails, the tool error can name that item so the agent knows what to do
next.
Bare strings are rejected at compile time. Skill-level complete_when also
requires an autonomous or hybrid skill (one with LLM-driven prose steps).
Fully controlled ordered-block-only skills complete via block END without
this gate — remove skill-level complete_when, or add an autonomous/hybrid
step and exit via complete_skill.
When authored:
complete_skillis hard-gated: unmet condition or failed criteria return a tool error and leave the skill active.- While an ordered block is active,
complete_skillis not offered. The engine finishes the block viaEND; a hybrid skill offerscomplete_skillagain after returning to prose. - While a tool confirmation is pending for the active skill,
complete_skillis refused — the LLM needs to resolve the tool confirmation first. cancel_skillis never gated bycomplete_when.- Condition only (no
criteria): the skill auto-completes when the condition becomes true. - Non-empty
criteria: auto-complete is disabled; exit only via a successfulcomplete_skill(condition + judge). The judge fails closed: if the check times out, errors, or returns a malformed answer,complete_skillis refused (the skill stays active) rather than treated as complete.
complete_when on ordered-block instructions: steps is a plain
condition string — no mapping, no criteria.
If you want to ensure that certain ordered blocks finished before a skill
completes, add the corresponding criteria.
import_tools
Allowlist of tools this skill may use beyond its own auto-discovered
tools.py / tools/*.py. Each entry is either:
tools.py or tools/*.py are auto-discovered and must
not be listed. A shared or MCP tool that is not declared here is not attached to
the skill — there is no auto-import of every tool on a server.
For an MCP entry, the LLM and tool_constraints see the bare tool name
(get_balance). Name it in prose the same way you name a local tool. The
mcp/<server>: prefix is only for the allowlist and for resolving which server
to call.
Collisions and load failures
Within one skill, bare names must be unique across local tools, shared imports, and MCP imports.rasa train fails when:
- Two
import_toolsentries resolve to the same bare name (for examplemcp/a:echoandmcp/b:echo, ormcp/banking:get_balancenext to a localget_balance) - An MCP entry references a server id missing from
integrations.yml - An MCP tool name is not a valid LLM function name. The tool name is sent to
the model as-is, so it may only contain letters, digits,
_, and-(at most 64 characters). A remote tool namedget.balancecannot be imported, even though the server id may contain dots.
mcp/<server>:<tool> string may appear in more than one skill. Each
skill that needs the tool must list it in its own import_tools.
To share a Python tool across skills, define it once in the agent-root tools/
folder and declare it in each skill’s import_tools.
See Import MCP tools for a short end-to-end example.
tool_constraints
A list of single-key mappings, tool name to options:
See Tool Constraints for worked examples.
An entry that is not a single-key mapping, or whose options are not a mapping,
fails with
skill.markdown.invalid_tool_constraints.
utter
Fires a verbatim response from responses.yml on a skill lifecycle event or
when a memory condition first becomes true:
An entry may set both. These are distinct from a tool’s
on_success: /
on_failure:, which attach to a gated tool in tool_constraints.
disabled
always_include_in_prompt
activate list even if
search did not retrieve it. Default is false.
hidden, disabled, and engine-managed still win: an always-included skill is
omitted when it is not in the legal routing catalogue. This is a skill-level
flag only — not an ordered-block property.
Body format
The body is markdown. It supports:Prose paragraphs
Always visible to the LLM. Blank lines separate paragraphs; each paragraph is a scoping unit.if: markers
A paragraph whose first line is if: <condition> is included only when the
condition holds:
if: paragraph per case; for exclusive either/or branching, use
an ordered block’s next: branches.
Memory in prose (@memory)
To put a live memory value into LLM-facing instructions, use:
Access matches what the active skill can read elsewhere: project fields; other
skills’ public fields; this skill’s public and private fields. Another
skill’s private fields and
@memory.system.* are rejected at train time.
At prompt build the engine replaces the token with the current value (same
stringification as the ### Memory: and
### Project Memory listings). Unset
or unreadable references are left as the literal token and logged. For a set
PII field, the whole token is replaced with [set] (for example
Contact @memory.project.phone becomes Contact [set]). That differs from
the Project section, which lists project.phone: [set] as a line.
Incomplete forms such as @memory.project (missing the field segment) cannot
be substituted — rasa train fails.
Example:
session.* for if: conditions and structured YAML (for example tool
parameters). Putting bare session.* in free prose also fails train — use
@memory.… instead. Response templates still use {session.*} — see
Responses.
Jumps to blocks and skills
Both take the id:
@block. names a block declared in the same file, and
@skill. names the target skill’s directory name. Incomplete lookalikes
(@skill, @block.foo.bar, @tool.lock.card) cannot be substituted —
rasa train fails. Use exactly one segment after the prefix.
You can name a tool in plain prose or with @tool.<name> when you want the
exact callable spelled in the instructions. rasa train checks @tool tokens
in two steps: token shape must be exactly one segment after @tool. (see
incomplete lookalikes above), and the name must resolve to a tool visible while
this skill is active (skill-local and declared shared @tool functions, MCP
imports, and engine framework tools such as set_fields). A shape or name
mistake fails train instead of leaving literal @tool… text at runtime.
How the parent is parked
The engine parks the current skill differently depending on what put the new one on top:
A
@skill. target must not be a hidden skill. Public memory is readable
across skills in every case, so the difference is control rather than data:
the first two are part of the skill’s design, the third is the customer
steering the conversation.
Ordered blocks
routable: inside the block. See
Ordered Blocks for step types.
Steps inside ordered blocks use the same YAML schema as top-level flow steps.
Unknown step properties are rejected at skill load and during
rasa data validate — misspelled or obsolete keys fail with a clear error
instead of being silently ignored.
Two removed collect-step keys have dedicated migration messages:
Any other unrecognized step property produces a generic validation error naming
the unknown key.
A skill needs prose or a block
Askill.md with neither prose instructions nor an ordered block fails with
skill.markdown.missing_instructions.
The combination determines routability:
Companion files
Each of the first three is scoped to the skill.
memory.yml declares entries
under this skill’s namespace, tools.py is attached to this skill, and
responses.yml merges into the shared registry under the names it declares.
references/ is project-wide
skills/<id>/references/**/*.md is indexed into the same index as the
agent-root references/ folder, and search_knowledge queries that whole index
on every call whatever skill is active. The folder location decides where the
files sit in your project, and the index a search reads is the same either way.
Two consequences worth designing around:
- Whether
search_knowledgeis offered at all depends on the project having an index, not on the active skill having areferences/folder. - Each retrieved snippet reaches the model with its
sourcefile path and ascopeof eitherglobalor the owning skill id. Hits are sections of the file, not the whole document. Write each document to stand on its own rather than relying on the skill it sits under to supply the context.
See also
- Conditions: the expression grammar
- Ordered Blocks: step types and branching
- References: how knowledge is indexed and searched
- Import MCP tools: allowlisting remote tools
integrations.yml: MCP server config