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.
