Tool calling.
The assistant gets useful when it can act, not just write. Tools are how it acts. This guide shows how to define a tool, scope it, expose it to a workflow, and read the audit trail of every call.
- The anatomy of a tool definition
- How to register a tool from the SDK and from a workflow file
- How scoping works at the tool level vs the workflow level
- Common debugging patterns for "the assistant chose not to call my tool"
Anatomy of a tool
A tool is a callable function plus a typed schema. The schema is how the model decides when to use it.
from wrxstack import tool @tool( name="crm.create_opportunity", description="Create an opportunity on an existing account.", scope=["crm:opportunities.write"], ) def create_opportunity(account_id: str, name: str, arr_usd: float): """Create a new opportunity. Returns the opportunity id.""" return client.crm.opportunities.create( account=account_id, name=name, arr_usd=arr_usd, )
The decorator does two things. It registers the function as a tool the assistant can see, and it attaches the required scope. The function will refuse to run if the calling workflow lacks the scope.
Registering a tool
Tools register at workflow-application time. The CLI scans your codebase for @tool decorators and uploads the schemas to wrxstack alongside the workflow TOML.
wrxstack workflow apply ./support-triage.toml # scanning for @tool decorators in ./assistants/… # found 4 tools, registering with wrxstack # crm.create_opportunity · scope=crm:opportunities.write # crm.update_stage · scope=crm:opportunities.write # slack.post_message · scope=slack:write # audit.note · scope=audit:write
Scope: tool level and workflow level
Two scope layers. Both must allow.
- Tool scope. Declared on the
@tooldecorator. The minimum scope required for the tool to do its job. - Workflow scope. Declared in the workflow's
[scope]block. The scope the workflow declares to its users.
The intersection is what the assistant sees in its tool list at run time. If your workflow declares crm:opportunities (read-only) but your tool requires crm:opportunities.write, the tool won't appear; the assistant won't try to call it.
Debugging "why didn't it call my tool?"
The model decides whether to call a tool. When it doesn't, the run log records why. Three common causes:
- Scope mismatch. The tool was filtered out before the model saw it. Check the run log → "available tools" section.
- Description too vague. The model needs to know when to use the tool. Rewrite the docstring with a concrete example.
- Conflicting tools. If you have
email.sendandemail.draft, the model may pick the wrong one. Make one of them the obvious choice in the description.
Audit
Every tool call is logged with: caller, tool name, input arguments, output (or error), duration, and the model rationale that led to the call. Pull the audit for a single run:
client.audit.runs.get("run_01HQ3K...") The response is JSON. Pipe it through jq for spot-checking, or archive it wherever you keep long-lived logs.
Next steps
- Safety & approvals for human-in-the-loop on sensitive tools.
- Assistants API for programmatic tool registration.