Tools & server actions¶
A module can add its own server-side behaviour in two distinct ways, and it is worth being clear about the difference up front:
- A tool is a capability you give an agent. The agent decides to call it in the middle of a conversation to get something done — look something up, do a calculation, create a record.
- A server action is a capability you attach to a model. A user runs it by clicking a button in a record's form view. It always runs against one specific record.
Both are plain Python functions with a decorator. This page shows how to write each, register it in your module, and hook it up.
| Tool | Server action | |
|---|---|---|
| Import | from victor import tools |
from victor import actions |
| Decorator | @tools.tool |
@actions.action(model=..., name=...) |
| Who triggers it | An agent, during a conversation | A user, clicking a button |
| Attached to | An agent (via its tools list) |
A model |
| Lives under | the module's tools/ folder |
the module's actions/ folder |
| Registered by | tools: in module.yaml |
actions: in module.yaml |
Define a tool¶
A tool is a normal Python function decorated with @tools.tool. The decorator
does not change how the function behaves — it stays a plain callable — it just
registers it so an agent can call it.
Four things about the function become the tool the agent sees:
| Part of the function | Becomes | Notes |
|---|---|---|
| The name | The tool's name | This is what the agent calls, and what you list in an agent's tools. |
| The docstring | The description the agent reads | Write it as an instruction to the model — say what the tool does and when to use it. |
| The typed arguments | The tool's inputs | Annotate every argument (query: str, values: dict). |
| The return value | The result handed back to the agent | Make it JSON-serializable — a str, dict, or list[dict]. |
The docstring is a prompt
The agent chooses whether and how to call your tool based on its name and docstring alone. Treat the docstring as instructions the model reads, not as internal developer notes.
Arguments and return values¶
Annotate each argument with its type. Give an argument a default to make it optional — the agent may then omit it.
from victor import tools
@tools.tool
def calc_add(a: int = 0, b: int = 0):
return {"sum": a + b}
@tools.tool
def calc_note(text: str = ""):
return {"note": text}
Return whatever the agent needs as its result, as long as it is
JSON-serializable. A short str, a dict, or a list[dict] all work well.
Sync or async tools¶
A tool can be a regular def or an async def — both are supported. Use
async def when the body needs to await something, such as reading instance
data.
from victor import tools
from victor.runtime import data
@tools.tool
async def search_contacts(query: str) -> list[dict]:
"""Search company contacts by name or email."""
return await data.search("contact", query, ["display_name", "email"])
Read and write instance data from a tool¶
To let a tool reach the data in the instance — the records defined by your models and the models shipped by other modules — import the runtime data helpers:
These are the helpers available to a tool body:
| Helper | Purpose |
|---|---|
data.search(model, query, fields) |
Search records of model, returning the given fields. |
data.list_records(model, query) |
List records of model matching query. |
data.create(model, values) |
Create a record from a values dict. |
data.update(model, id, values) |
Update the record id with values. |
data.list_models() |
List the models available in the instance. |
data.list_actions(model) |
List the server actions defined on model. |
data.run_action(model, id, name) |
Run a named server action on one record. |
All of them return JSON-serializable values, so you can return the result
straight to the agent. The read helpers (search, list_records,
list_models, list_actions) are safe to call freely; the mutating ones
(create, update, run_action) change data and are usually paired with
approval — see below.
The contact model referenced above is provided by the built-in contact app;
see Models & fields for how models and their fields are
defined.
Hold a tool for human approval¶
Any tool that creates, updates, or otherwise acts on data should pause for a
person to approve it before it runs. Pass approve=True to the decorator:
from victor import tools
from victor.runtime import data
@tools.tool(approve=True)
async def create_record(model: str, values: dict) -> dict:
"""Create a record in a data model (e.g. create_record('contact', {'display_name': 'Globex'})).
Requires human approval before it runs."""
return await data.create(model, values)
With approve=True, when the agent decides to call the tool the run is held
until a human approves it; only then does the function body execute.
approve defaults to False.
Approve anything that mutates
Read-only tools can run freely, but give every tool that writes data —
creating, updating, sending, deleting — approve=True. It puts a human in
the loop before the agent changes anything.
Register tools in your module¶
Put your tool functions in Python files under the module's tools/ folder, for
example tools/ping.py or tools/contacts.py. Then list them under the
tools: key in module.yaml so they load. Values are dotted paths relative
to the module, with no .py extension.
module.yaml value |
Loads |
|---|---|
tools: [tools.ping] |
tools/ping.py |
tools: [tools.contacts] |
tools/contacts.py |
tools: [tools] |
every tool in the tools/ package |
See Module structure & manifest for the rest of
module.yaml.
Give a tool to an agent¶
Registering a tool makes it available; an agent only sees the tools it
names. In the agent's class, list the tool function names it is allowed to
call in tools = [...]:
from victor import agents
class Bot(agents.Agent):
name = "bot"
label = "Bot"
model = "anthropic:claude-sonnet-4-6"
instructions = "You are a bot."
tools = ["ping"]
Each entry is a string matching a tool function's name — "ping",
"search_contacts", and so on. An agent that does not name a tool cannot call
it. For everything else about agents, see Building agents.
Server actions¶
A server action is the counterpart to a tool: instead of being chosen by an agent, it runs when a user clicks a button in a record's form view, and it operates on that one open record.
Register it with @actions.action from from victor import actions. The
handler is an async function that receives the current record's id and returns
a dict (or None).
from victor import actions
@actions.action(model="memory_fact", name="reject")
async def reject(session, record_id) -> dict:
"""Reject a proposed fact (kept for audit, never retrieved)."""
# ...load the record, update it...
return {"status": "rejected"}
The decorator takes:
| Parameter | Required | Meaning |
|---|---|---|
model |
Yes | The model the action attaches to, e.g. model="email_message". |
name |
No | The action name a view button refers to. Defaults to the function name. |
The handler signature is always async def f(session, record_id) -> dict | None:
record_id identifies the record the user is acting on, and the return value is
a dict (or None).
Register your actions the same way you register tools — put the handlers under
the module's actions/ folder and list the package under the actions: key in
module.yaml:
name: mail
label: Mail
version: 0.1.0
requires: [base, contact, security]
models: [models]
actions: [actions]
tools: [tools.send]
Wire a button to an action in a view¶
A server action is triggered from a form view with a <button> element. Its
action attribute matches the action's name, and string is the label the
user sees:
<view id="view_message_form" model="email_message" type="form">
<group string="Message">
<field name="email_to" widget="email"/>
<field name="subject"/>
<field name="body_html" widget="textarea"/>
</group>
<button action="send" string="Send" variant="default" show_on_new="1"/>
<button action="retry" string="Retry"/>
<button action="cancel" string="Cancel"/>
</view>
The <button> element accepts:
| Attribute | Purpose |
|---|---|
action |
The name of the @actions.action to run. |
string |
The button's label. |
variant |
Visual style — "default" or "destructive". |
show_on_new |
"1" shows the button on an unsaved/new record. |
run_on_save |
"1" runs the action when the record is saved. |
invisible |
An expression that hides the button conditionally, e.g. invisible="state == 'installed'". |
preview |
Another action to confirm through first (a preview step). |
See Views for the full form-view DSL, and Actions & menus for how views and buttons fit into a module.
Which one do I want?¶
- Reach for a tool when you want an agent to be able to do something on
its own during a conversation — fetch data, run a computation, draft or create
a record (with
approve=Trueif it writes). - Reach for a server action when you want a user to do something to a specific record by clicking a button in its form — send it, retry it, approve or reject it.
You can ship both from the same module: agents get tools, records get buttons.