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

# Tools reference

> Field-by-field contract for defining a tool, plus the built-in framework tools.

A tool is an `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

```python skills/check_balance/tools.py theme={null}
from rasa.mantle.tools.decorator import ToolContext, tool
from rasa.mantle.tools.result import ToolResult


@tool(description="Look up the customer's account balance")
async def check_balance(
    account_id: str,
    context: ToolContext = None,
) -> ToolResult:
    balance = await api.get_balance(account_id)
    context.memory.set("current_balance", balance)
    return ToolResult(llm_response={"balance": balance, "currency": "USD"})
```

`tool` and `ToolContext` come from `rasa.mantle.tools.decorator`, and
`ToolResult` from `rasa.mantle.tools.result`.

Requirements:

* The function must be `async`.
* `@tool` takes `description` as a **keyword argument**. `@tool(description="…")`.
  A bare `@tool` without it raises.
* The return type is `ToolResult`.
* Name the injected parameter **`context`**. The runtime passes it as the
  keyword argument `context=`, and leaves it out of the LLM's schema.

Local `@tool` calls are subject to a wall-clock timeout. Configure it with
[`tool_timeout` in `agent.yml`](https://rasa.com/docs/mantle/reference/agent-yml#tool_timeout) (default
`10` seconds). MCP tools use the same agent default unless the server sets its
own `tool_timeout` in [`integrations.yml`](https://rasa.com/docs/mantle/reference/integrations-yml#mcp_servers).

## How the schema is derived

| Schema field | Comes from |
| - | - |
| `name` | The function name |
| `description` | The `description=` argument to `@tool` |
| Property types | Python type annotations |
| `required` | Every annotated parameter with no default |
| *(excluded)* | The `context` parameter never appears in the schema |

Type mapping:

| Annotation | JSON schema |
| - | - |
| `str` | `{"type": "string"}` |
| `int` | `{"type": "integer"}` |
| `float` | `{"type": "number"}` |
| `bool` | `{"type": "boolean"}` |
| `list[str]` | `{"type": "array", "items": {"type": "string"}}` |
| `list` | `{"type": "array"}` |
| `Optional[T]` / `T \| None` | unwrapped to `T` |
| anything else | `{"type": "string"}` |

The generated object sets `additionalProperties: false`.

> [!NOTE]
>
> 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 skill `skills/<id>/`, in resolution order:

1. **Skill-local**: `skills/<id>/tools.py` **and** `skills/<id>/tools/*.py`. Both
   locations are scanned; no declaration needed. A name defined in `tools/*.py`
   shadows the same name in `tools.py` (logged as a duplicate).
2. **Shared**: `tools/*.py` at the agent root. Only tools the skill **declares**
   via `import_tools` are attached to it; an undeclared shared tool is not
   available to that skill at all.
3. **MCP**: `mcp/<server-id>:<tool-name>` entries in `import_tools`. The skill
   receives only those allowlisted tools; the LLM sees the bare tool name. The
   server must be declared under
   [`mcp_servers`](https://rasa.com/docs/mantle/reference/integrations-yml#mcp_servers).

First match wins among skill-local and shared names. An MCP bare name that
collides with a local or shared tool in the same skill fails `rasa train` — see
[`import_tools`](https://rasa.com/docs/mantle/reference/skill-md#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 on `sys.path`, so a `lib/`
package at the agent root is importable from any tool location:

```python theme={null}
from lib.wallet_data import WALLETS   # works in skills/<id>/tools.py and tools/*.py
```

Do this at **module top level**. The project root is only importable during tool
loading, not later when the tool is dispatched.

## `ToolContext`

The runtime constructs one `ToolContext` per invocation and injects it:

| Member | Type | Description |
| - | - | - |
| `await context.send(text)` | coroutine | Send a message to the user immediately, bypassing the LLM |
| `context.set_buttons(...)` | method | For a response button source tool, attach `Button` values to the response |
| `context.memory` | `MemorySlice` | Scoped read/write access to memory |
| `context.events` | `list[Event]` | A **copy** of the tracker's typed event history |
| `context.is_cancelled` | `bool` | `True` once the turn has been cancelled. See [Cancellation](#cancellation) |
| `context.model` | `ToolModelContext` | The project's LLM settings and knowledge index |

`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.

> [!NOTE]
>
> `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`

```python theme={null}
value = context.memory.get("selected_card_id")   # bare: active skill, then project
cards = context.memory.get("card_list", [])      # unset, None, or unreadable → []
value_from_another_skill = context.memory.get(
    "card_disambiguation.selected_card_id"
)  # fully-qualified: another skill's public field
context.memory.set("selected_card_id", card.id)  # writes slot_set and memory_set immediately
context.memory.set("project.authenticated", True)  # always project, even if the skill has authenticated
```

For **reads**, use either:

* a **bare** name as declared in `memory.yml` — resolved against the active
  skill's schema first, then the project-root `memory.yml`
* a **fully-qualified** name (`<skill_id>.<entry>` or `project.<entry>`) — same
  visibility as conditions / prompt memory: the active skill's public and
  private fields, every other skill's **public** fields, and project fields.

Do **not** write `session.` prefixes here (that form is for conditions). For
**writes**, use a **bare** name (active skill, then project) or a
**fully-qualified** name for a field this skill may write. `project.<entry>`
always targets project memory, even when the skill declares the same name.
`<active_skill_id>.<entry>` always targets this skill's field and does not
fall through to project. Cross-skill writes stay out of scope. Do not use
the internal memory manager or clear memory entries; `rasa data validate`
rejects that. The same rule applies to literal `context.memory.set(...)` calls
that `rasa train` inspects. To clear a field this skill owns, call
`context.memory.set(field, None)`. Fields another skill owns must be cleared
from that skill.

* `get` accepts an optional second argument used as the default. It returns
  that default when the entry is unset, stored as `None`, or outside the
  readable set (for example another skill's `private` field). When you do not
  pass a default, `get` returns `None`. Any other stored value wins, including
  falsy values such as `0`, `False`, or `""`. Stored `None` cannot be
  distinguished from a never-written entry, so both yield the default (this
  differs from `dict.get`). `get` never raises.
* `set` writes through immediately and **raises `MemoryWriteError`** when the
  entry is undeclared, the name is a cross-skill write, the name collides
  with the reserved `system.*` namespace, or a `project.*` field is already
  set (`ProjectMemoryAlreadySetError`). YAML `set_memory:` steps and
  `run_after_setting_*` hooks skip an already-set project field instead of
  failing the step, so a later collect or correct is not rolled back.

> [!NOTE]
>
> 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](https://rasa.com/docs/mantle/reference/memory-yml).

### `context.set_buttons`

`set_buttons` is only meaningful for a **response button source tool**. That
is a local tool named by a variant's `buttons: {source: <tool_name>}`. It
attaches `Button` values to the response that triggered the invocation.
Calling it outside that context raises `RuntimeError`.

```python theme={null}
from rasa.mantle.tools.buttons import Button

context.set_buttons(
    [
        Button(title="Tomorrow morning", value="tomorrow_morning",
               ref="session.delivery.delivery_window"),
        Button(title="Show later dates", value="later"),
    ]
)
```

A `Button` with `ref` writes `value` to that fully qualified memory field on
tap. Without `ref`, the engine sends the tap as plain user input. Calling
`set_buttons` again replaces the previously supplied list. Messages the tool
sends with `context.send()` remain separate and carry no buttons. See
[Dynamic buttons](https://rasa.com/docs/mantle/reference/responses-yml.md#dynamic-buttons).

## `ToolResult`

```python theme={null}
class ToolResult:
    llm_response: Any = None     # returned to the LLM
```

* **`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-block `execute_tool:` steps, post-write hooks) act through `context`.
* 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. Add `tool_constraints` in the frontmatter to gate one behind a memory
condition:

```yaml theme={null}
tool_constraints:
  - lock_card:
      requires: "session.card_replace.selected_card_id"
```

The tool stays out of the LLM's schema until the condition holds, and the same
check runs again before the function is called. See
[Conditions](https://rasa.com/docs/mantle/reference/conditions.md).

Visibility rules differ slightly by tier:

* A **skill-local** tool is visible unless it appears in `tool_constraints` with
  an unmet condition.
* A **shared** tool must be listed in `import_tools`; a `tool_constraints` entry
  then gates it as well.
* A tool named by a response variant's `buttons: {source: <tool_name>}` is a
  **response button source**. The engine invokes it before delivering that
  response, and omits it from the LLM tool schema so the model cannot
  dispatch it directly. See
  [Dynamic buttons](https://rasa.com/docs/mantle/reference/responses-yml.md#dynamic-buttons).

## Built-in framework tools

These names are **reserved**. A builder tool that reuses one raises at model
load.

| Tool | When it is offered | What it does |
| - | - | - |
| `activate` | Whenever at least one flow is startable | Start or switch to a flow (`flow_id` enum, rebuilt each turn) |
| `complete_skill` | While a skill's prose instructions are active | Mark the active skill finished |
| `cancel_skill` | While a skill is active | Abandon the active skill |
| `set_fields` | When the active skill has settable entries | Record one or more memory values in a single batch call |
| `correct` | When ≥1 earlier value is correctable this turn | Revise a value the user already provided |
| `resolve_tool_confirmation` | While a tool confirmation is pending | Record the user's approval or denial and resume |
| `search_knowledge` | When a references index was packaged | Retrieve from the knowledge base |
| `cannot_help` | After a search has run, or immediately when no knowledge base exists | Decline an unsupported request (`in_domain` boolean) |
| `listen` | Main LLM calls except guided collect, post-search answer calls, and pending tool confirmation | Silent wait: no message, no stack or memory change. Ignored if mixed with text or other tools |

Behaviors worth knowing:

* `complete_skill` is withheld while an ordered block is active. The engine
  finishes the block when it reaches `END`. A hybrid skill (prose plus ordered
  blocks) offers `complete_skill` again 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 call `complete_skill`. `cancel_skill`
  stays available throughout, so the customer can still abandon the skill
  mid-block.
* Once `search_knowledge` has run in a turn, the tool list collapses to
  `activate`, `search_knowledge`, and `cannot_help`, plus
  `resolve_tool_confirmation` when a confirmation is pending. The skill's own
  tools and `listen` are withheld so a grounded answer neither re-drives the
  skill nor ends silently before presenting the retrieved information. They
  return on the next turn.
* `cannot_help` is withheld until a search has run when a knowledge base exists,
  so the model cannot decline before retrieval could have answered.
* `listen` alone ends the turn with no message. It is the LLM wait tool.
  Ordered-block `action: action_listen` is a separate wait step.
* Guided collect steps, post-search answer calls, and pending tool confirmation
  do not offer `listen`.
* If `listen` is mixed with text or other tools, the engine ignores `listen`
  and follows the text and sibling tools.

## Post-write hooks

A tool named `run_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.

```python tools/card_hooks.py theme={null}
@tool(description="Resolve the chosen card label to an id")
async def run_after_setting_selected_card_label(
    context: ToolContext = None,
) -> ToolResult:
    label = context.memory.get("selected_card_label")
    card = _card_by_label(label, context.memory.get("card_list") or [])
    if card is None:
        return ToolResult(llm_response={"error": "No card matches that label."})
    context.memory.set("selected_card_id", card["id"])
    return ToolResult()
```

* Returning `llm_response` with a **non-null** `error` key 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 raises
`asyncio.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:

```python theme={null}
from rasa.mantle.tools.decorator import ToolContext, tool
from rasa.mantle.tools.result import ToolResult


@tool(description="Execute a money transfer between accounts")
async def process_transfer(
    amount: float,
    context: ToolContext = None,
) -> ToolResult:
    if context.is_cancelled:
        return ToolResult(llm_response={"ok": False, "reason": "cancelled"})

    await bank.transfer(amount)
    return ToolResult(llm_response={"ok": True})
```

Returning early is clean: the call is recorded and the step advances, with no
further `await` for a cancellation to slip into.

### Release resources on cancellation

If your tool holds a resource, catch `asyncio.CancelledError`, release it, and
**re-raise**. The turn has already ended, so the runtime discards anything you
return from the handler.

```python theme={null}
import asyncio


@tool(description="Stream a statement to the customer")
async def stream_statement(
    account_id: str,
    context: ToolContext = None,
) -> ToolResult:
    client = await bank.open_stream(account_id)
    try:
        data = await client.read_all()
    except asyncio.CancelledError:
        await client.close()
        raise
    return ToolResult(llm_response={"rows": data})
```

## See also

* [Tools concept](https://rasa.com/docs/mantle/concepts/tools.md): when to reach for a tool, including
  [MCP imports](https://rasa.com/docs/mantle/concepts/tools.md#mcp-tools)
* [Conditions](https://rasa.com/docs/mantle/reference/conditions.md): the `requires:` expression grammar
* [`memory.yml` reference](https://rasa.com/docs/mantle/reference/memory-yml): declaring what tools may write
* [`skill.md` reference](https://rasa.com/docs/mantle/reference/skill-md): `import_tools` and `tool_constraints`
* [`integrations.yml`](https://rasa.com/docs/mantle/reference/integrations-yml#mcp_servers): MCP server config
* [`agent.yml`](https://rasa.com/docs/mantle/reference/agent-yml#tool_timeout): default tool-call timeout


## Related topics

- [Tools](https://rasa.com/docs/mantle/concepts/tools.md)
- [Execution loop reference](https://rasa.com/docs/mantle/reference/execution-loop.md)
- [Rasa MCP Tools API Reference](https://rasa.com/docs/reference/api/rasa-mcp-tools.md)
- [Tool Constraints](https://rasa.com/docs/mantle/build-guide/tool-constraints.md)
- [Rasa MCP Tools](https://rasa.com/docs/pro/installation/rasa-mcp-tools.md)


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