Docs/Guides/Assistants/Tool calling

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.

What you will learn
  • 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 @tool decorator. 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:

  1. Scope mismatch. The tool was filtered out before the model saw it. Check the run log → "available tools" section.
  2. Description too vague. The model needs to know when to use the tool. Rewrite the docstring with a concrete example.
  3. Conflicting tools. If you have email.send and email.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