async Python function decorated with @tool. The function
signature is the schema the LLM sees, and there is no separate registration
step.
Defining a tool
skills/check_balance/tools.py
tool and ToolContext come from rasa.mantle.tools.decorator, and
ToolResult from rasa.mantle.tools.result.
Requirements:
- The function must be
async. @tooltakesdescriptionas a keyword argument.@tool(description="…"). A bare@toolwithout it raises.- The return type is
ToolResult. - Name the injected parameter
context. The runtime passes it as the keyword argumentcontext=, and leaves it out of the LLM’s schema.
@tool calls are subject to a wall-clock timeout. Configure it with
tool_timeout in agent.yml (default
10 seconds). MCP tools use the same agent default unless the server sets its
own tool_timeout in integrations.yml.
How the schema is derived
Type mapping:
The generated object sets
additionalProperties: false.
Each generated property carries its type. Put the meaning of an argument into
the tool’s
description, or name the parameter so its purpose is obvious.Where tools are discovered
For a skillskills/<id>/, in resolution order:
- Skill-local:
skills/<id>/tools.pyandskills/<id>/tools/*.py. Both locations are scanned; no declaration needed. A name defined intools/*.pyshadows the same name intools.py(logged as a duplicate). - Shared:
tools/*.pyat the agent root. Only tools the skill declares viaimport_toolsare attached to it; an undeclared shared tool is not available to that skill at all. - MCP:
mcp/<server-id>:<tool-name>entries inimport_tools. The skill receives only those allowlisted tools; the LLM sees the bare tool name. The server must be declared undermcp_servers.
rasa train — see
import_tools.
When a tool module cannot be imported, rasa train
fails. When a referenced MCP server cannot connect, authenticate, or list tools,
agent startup stops with an error naming the failure, so a misconfiguration
surfaces before the first conversation rather than as a missing tool mid-turn.
Importing shared project code
While tool modules are imported, the project root is onsys.path, so a lib/
package at the agent root is importable from any tool location:
ToolContext
The runtime constructs one ToolContext per invocation and injects it:
context.model carries two read-only values that are fixed for the loaded
model: llm_config, the same provider settings the orchestrator uses from
integrations.yml, and references_index, the packaged knowledge index (or
None when the project ships no reference files). Use it when a tool needs to
run its own model call or query the same knowledge the agent searches.
send is a coroutine, so await it: await context.send("One moment…").context.events is a fresh list, so appending to or removing from it does not
touch conversation state. Memory is the only supported write path.
If the user has already interrupted the turn (voice barge-in), send drops the
message.
context.memory
- a bare name as declared in
memory.yml— resolved against the active skill’s schema first, then the project-rootmemory.yml - a fully-qualified name (
<skill_id>.<entry>orproject.<entry>) — same visibility as conditions / prompt memory: the active skill’s public and private fields, every other skill’s public fields, and project fields, minus the active skill’saccess.deny_read
session. prefixes here (that form is for conditions). Cross-skill
writes stay out of scope: set only accepts bare names for the active skill
or project. Do not use the internal memory manager or clear memory entries —
rasa data validate rejects that. To clear a field this skill owns, use
context.memory.set(field, None). Fields another skill owns must be cleared
from that skill.
getreturnsNonewhen the entry is unset, outside the readable set (for example another skill’sprivatefield), or when the skill’saccess.deny_readblocks it. It never raises.setwrites through immediately and raisesMemoryWriteErrorwhen the entry is undeclared,deny_writeblocks it, the name collides with the reservedsystem.*namespace, or aproject.*field is already set (ProjectMemoryAlreadySetError). YAMLset_memory:steps andrun_after_setting_*hooks skip an already-set project field instead of failing the step, so a later collect or correct is not rolled back.
Every key a tool writes must be declared in the skill’s
memory.yml or the
project-root memory.yml. rasa train rejects an undeclared write with
undeclared_memory_write. This is the most common authoring failure.
See the memory.yml reference.ToolResult
llm_response: any JSON-serialisable value; this is what the LLM reads as the tool’s result. It is used for LLM-invoked tools; framework-invoked tools (ordered-blockexecute_tool:steps, post-write hooks) act throughcontext.- A tool that only sends a message or writes memory returns a bare
ToolResult().
Gating a tool
By default every skill-local tool is visible to the LLM whenever its skill is active. Addtool_constraints in the frontmatter to gate one behind a memory
condition:
- A skill-local tool is visible unless it appears in
tool_constraintswith an unmet condition. - A shared tool must be listed in
import_tools; atool_constraintsentry then gates it as well.
Built-in framework tools
These names are reserved. A builder tool that reuses one raises at model load.
Behaviors worth knowing:
complete_skillis withheld while an ordered block is active. The engine finishes the block when it reachesEND. A hybrid skill (prose plus ordered blocks) offerscomplete_skillagain once control returns to its prose instructions. Ordered-block-only skills never offer it. Do not add a final block step that asks the model to callcomplete_skill.cancel_skillstays available throughout, so the customer can still abandon the skill mid-block.- Once
search_knowledgehas run in a turn, the tool list collapses toactivate,search_knowledge, andcannot_help, plusresolve_tool_confirmationwhen a confirmation is pending. The skill’s own tools are withheld so a grounded answer does not re-drive the skill, and return on the next turn. cannot_helpis withheld until a search has run when a knowledge base exists, so the model cannot decline before retrieval could have answered.
Post-write hooks
A tool namedrun_after_setting_<entry> is an internal hook, not an LLM tool.
It is never offered to the model. The runtime calls it after a top-level
set_fields or correct write to that entry, so you can validate the value or
derive state from it.
tools/card_hooks.py
- Returning
llm_responsewith a non-nullerrorkey rolls the triggering write back and surfaces the message to the LLM.{"error": None}is success. - Writes made inside a hook do not re-trigger hooks.
- A skill-local
run_after_setting_<field>hook can write project memory after the LLM records a skill field. Shared or skill hooks named after a project field are invalid — the LLM never sets project memory.
Cancellation
A turn can end while a tool is still running, whether from a voice barge-in, a session timeout, or a dropped connection. The runtime raisesasyncio.CancelledError inside the tool at whatever await it is suspended on.
There is nothing to wire up.
Only await points are interruptible; synchronous code between them runs to
completion.
What happens to your tool
When your tool returns normally, the runtime records the call and advances the skill to its next step. When your tool is cancelled mid-await, it does neither. The step stays
pending, so the same tool can run again on the next turn. Memory it already
wrote with context.memory.set(...) is kept.
That gap matters for a side-effecting tool: your backend may have been called
successfully while the conversation holds no record of it.
Check before irreversible work
context.is_cancelled is True once the turn has been cancelled. Check it
immediately before anything you cannot safely repeat, and between consecutive
side effects:
await for a cancellation to slip into.
Release resources on cancellation
If your tool holds a resource, catchasyncio.CancelledError, release it, and
re-raise. The turn has already ended, so the runtime discards anything you
return from the handler.
See also
- Tools concept: when to reach for a tool, including MCP imports
- Conditions: the
requires:expression grammar memory.ymlreference: declaring what tools may writeskill.mdreference:import_toolsandtool_constraintsintegrations.yml: MCP server configagent.yml: default tool-call timeout