Skip to content

tutorial

Chapter 3 of 6

Chapter 3 — Global tools

by Rod Rivera Published

When more than one skill needs the same capability, define it once at the project root and import it explicitly.

Some capabilities genuinely are shared. Looking up the signed-in customer is one: both Check Balance and Transfer Money want the caller’s name, and neither of them owns that lookup.

That is a global tool.

tools/customer.py               <- defined once, at the project root
skills/check_balance/skill.md   <- import_tools: [get_customer_info]
skills/transfer_money/skill.md  <- import_tools: [get_customer_info]

The tool

# tools/customer.py
@tool(description="Look up the signed-in customer's profile (name and segment).")
async def get_customer_info(context: ToolContext = None) -> ToolResult:
    customer_id = context.memory.get("project.customer_id")
    if not customer_id:
        return ToolResult(llm_response={"ok": False, "error": "not_authenticated"})

    customer = customer_by_id(str(customer_id))
    return ToolResult(
        llm_response={"ok": True, "customer_id": customer["customer_id"],
                      "name": customer["name"], "segment": customer["segment"]}
    )

The import is the point

Unlike local tools, a global tool is not picked up automatically. Each skill that wants it says so:

---
name: Check Balance
description: >
  Tell the caller the balance of one of their accounts.
import_tools:
  - get_customer_info
---

That line is doing real work. It is the skill declaring its dependencies, so when you move skills/check_balance/ to another agent, the folder tells you exactly what else has to travel with it. Without it you would be reading Python to find out.

It also keeps the model’s tool list small. A skill only sees the tools it owns and the ones it imported, so the model is choosing between a handful of options rather than everything in the project.

The decision rules

Make a tool global only when all three hold:

Test get_customer_info
More than one skill calls it? Yes — Check Balance and Transfer Money
Skill-agnostic — does it know why it was called? No, and it should not
About the end user rather than one workflow? Yes — it returns the profile

Fail any one and it stays local. A tool used by two skills that still encodes one skill’s workflow is a sign the two skills should share a sub-skill, not a tool.

Why not “always local, import when needed”

Both designs give bounded, declarative skills. Rasa chose the global root because it makes shared surface discoverable: open tools/ and you can see everything more than one skill depends on. With the alternative, sharing is implicit and you find it only by reading imports across every skill.

It also matters for how teams work. Enterprise agents are usually built by several teams, each contributing skills. Keeping each team’s working area bounded — their own folder, their own tools — reduces the surface where two teams collide. The shared surface stays small, visible, and deliberate.

A signal worth watching

If most of your tools end up global, something is off. Either the skills are cut too finely, or what you are sharing is really a library function rather than a tool. That is worth feeding back to Rasa — it questions what local tools are for.

Tools do not have to run in-process

A tool is anything the agent can do. Today these all execute locally, inside your project. Remote execution over MCP servers is the same idea with a different transport: the skill imports a tool and calls it, and where the code runs is an implementation detail. The scoping rules in this chapter do not change.