Skip to content

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.

from victor import tools


@tools.tool
def ping() -> str:
    """Reply pong."""
    return "pong"

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:

from victor.runtime import data

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
name: bot
label: Bot
version: 0.1.0
requires: [base]
agents: [agents.bot]
tools: [tools.ping]

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=True if 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.