Skip to main content

Defining Tools

Tools are Python functions that an agent can call during a conversation to fetch or mutate live application data. They are defined in your app's module code using the @tool decorator and are discovered automatically when you sync them.

Creating a tools.py file

Create a tools.py file inside any module in your app's backend/ directory:

backend/
└── items/
├── models.py
├── views.py
└── tools.py ← define tools here

The @tool decorator

items/tools.py
from zango.ai.tools.decorator import ToolParam, ToolSafety, tool


@tool(
name="get_item_details",
description="Fetches details of an item by its ID. Returns name, description, and quantity.",
section="items",
safety=ToolSafety.READ_ONLY,
timeout_seconds=10,
)
def get_item_details(
item_id: int = ToolParam(description="The database ID of the item"),
) -> dict:
from .models import Item

try:
item = Item.objects.get(pk=item_id)
except Item.DoesNotExist:
return {"error": f"No item found with id {item_id}"}

return {
"id": item.id,
"name": item.name,
"description": item.description,
"quantity": item.quantity,
}

Decorator parameters

ParameterTypeDefaultDescription
namestrrequiredUnique tool name shown to the LLM and in the App Panel.
descriptionstrrequiredExplains to the LLM what this tool does and when to use it. Write it clearly: the LLM decides whether to call the tool based on this text.
sectionstr"general"Groups related tools together in the App Panel.
safetyToolSafetyREAD_ONLYREAD_ONLY, WRITE, or EXTERNAL. See Safety levels.
timeout_secondsint30Maximum time allowed before the tool is aborted.
rate_limitint | NoneNoneMax calls per minute. None means unlimited.
memory_policystr"include"Whether this tool's call and result are replayed in session memory. See Memory policy.

Tool parameters with ToolParam

Each function argument that the LLM can provide must use ToolParam as its default value:

def my_tool(
patient_id: str = ToolParam(description="The UUID of the patient record"),
include_history: bool = ToolParam(description="Whether to include past visits"),
) -> dict:
...

The description in ToolParam is sent to the LLM so it knows how to populate each argument. Always write descriptions from the LLM's perspective: what is this value, and where does it come from?

Safety levels

from zango.ai.tools.decorator import ToolSafety

ToolSafety.READ_ONLY # Tool only reads data; cannot write, update, or delete
ToolSafety.WRITE # Tool modifies data in the database
ToolSafety.EXTERNAL # Tool calls an external service (email, SMS, third-party API)
LevelWhen to use
READ_ONLYAny tool that only fetches or queries data
WRITETools that create, update, or delete records
EXTERNALTools that trigger side-effects outside the database (send email, call an API)

Memory policy

When an agent has short-term memory enabled, every tool call and its result are saved to the session history and replayed on the next agent.run() call. memory_policy controls whether a specific tool participates in that replay.

ValueBehaviour
"include" (default)The call and result are stored in session history and sent to the LLM on subsequent turns.
"exclude"The call and result are dropped from loaded history. Use for side-effect tools that should not be replayed (e.g. send email, send SMS).
from zango.ai.tools.decorator import ToolParam, ToolSafety, tool

# This tool's execution will NOT appear in session history on the next turn
@tool(
name="send_notification",
description="Sends a push notification to the user. Call this once the response is ready.",
safety=ToolSafety.EXTERNAL,
memory_policy="exclude", # don't replay; the notification was already sent
)
def send_notification(
user_id: int = ToolParam(description="The user to notify"),
message: str = ToolParam(description="The notification message body"),
) -> dict:
...
return {"sent": True}

:::tip When to use memory_policy="exclude" Use "exclude" for any tool that triggers an irreversible real-world action: sending an email, posting an SMS, calling a payment API. Replaying those actions on the next turn would cause double-sends or duplicate charges. :::

Returning data from tools

Tools must return a dict. The LLM receives this dictionary as the tool result and uses it to formulate its response.

# Good: structured, descriptive keys
return {
"patient_name": "John Doe",
"screening_date": "2024-03-15",
"outcome": "Eligible",
}

# Good: error case
return {"error": "Patient not found with ID 123"}

Tenant context in tools

Tools run in the same tenant schema as the request or task that triggered the agent. You do not need to set connection.set_tenant(); Zango handles it automatically.

# This ORM query automatically uses the correct tenant schema
item = Item.objects.get(pk=item_id)

Multiple tools in one file

A single tools.py can define as many tools as needed:

@tool(name="get_item_details", ...)
def get_item_details(item_id: int = ToolParam(...)) -> dict:
...

@tool(name="get_item_count", ...)
def get_item_count() -> dict:
from .models import Item
return {"total_items": Item.objects.count()}

@tool(name="update_item_quantity", safety=ToolSafety.WRITE, ...)
def update_item_quantity(
item_id: int = ToolParam(description="The item ID"),
quantity: int = ToolParam(description="The new quantity"),
) -> dict:
from .models import Item
Item.objects.filter(pk=item_id).update(quantity=quantity)
return {"success": True, "new_quantity": quantity}

Discovery and sync

Tools become available to agents once they are synced: Zango scans every tools.py across your modules, registers new tools, and updates changed ones (names, descriptions, parameters, safety). The zango-app-developer plugin runs this sync through the platform API whenever it adds or edits a tool, so you rarely trigger it by hand. After a sync, each tool carries live metadata, its module path, return type, parameter schema, and usage counters (calls, errors, timeouts, average time), and can be attached to any agent.

note

Tools are tenant-scoped. Syncing for one tenant does not affect another.

Synced tools listed in the App Panel
After a sync, each @tool appears with its section, safety level, parameters, and usage stats, ready to attach to an agent.

Next steps

With tools defined and attached to an agent, run the agent from your code.