Skip to main content

Build & Push a Custom Tool

Built-in tools (Read, Glob, WebFetch, …) cover a lot, but eventually your agent needs to do something specific — hit your internal API, transform data a particular way, look something up. That's a custom tool: you write it once, push it to Waxell, and any of the tenant's agents can reference it by name.

This is the companion to Ship a Claude Agent — same wax CLI, one extra artifact.

1. Write the tool

A tool is a folder with a Python file and an optional TOOL.json manifest:

weather/
├── TOOL.json # name + metadata (optional)
└── weather.py # the tool source

weather.py — a @tool-decorated function. Type the params; the docstring becomes the description the model sees:

from waxell_runtime import tool


@tool
def get_weather(ctx, city: str, units: str = "celsius") -> dict:
"""Get the current weather for a city. Units: celsius or fahrenheit."""
# Pure-Python logic + ctx.* helpers (domains, secrets, the LLM router).
# Do real I/O through a domain or ctx.domain(...) — see the v1 limits below.
return {"city": city, "temp": 21, "units": units, "conditions": "clear"}

TOOL.json (optional — --name and the folder name are fallbacks):

{
"name": "get_weather",
"display_name": "Get Weather",
"description": "Current weather for a city."
}

2. Push it

wax -p acme_prod push-tool ./weather
wax -p acme_prod push-tool ./weather --change-summary "add fahrenheit"

The control plane validates the source on push:

  • No dangerous callsos.system, subprocess.*, eval, exec, __import__ are rejected (the source runs sandboxed).
  • No third-party dependencies in v1 — keep it stdlib + ctx. For external APIs, call them through a domain (ctx.domain(...)), which is the governed, per-user-auth boundary. See Ship a Claude Agent §4.

Confirm it registered:

wax -p acme_prod registry list --kind tool        # or: wax agents info <agent>

3. Wire it into an agent

Reference the tool by name in the agent's waxell.yaml, then re-push the agent (bump the version):

agents:
- name: trip_planner
version: 1.1.0 # bumped
framework: claude_agent_sdk
tools: [Read, WebFetch, get_weather] # built-ins + your custom tool
# ...
wax -p acme_prod push --include-source
wax -p acme_prod agents info trip_planner # confirm the tool shows under bindings

When the agent runs, the platform surfaces your tool to the model (for claude_agent_sdk agents it appears under the MCP bridge) and executes it host-side — so secrets and domains resolve under governance, not in the model.

Versioning & updates

push-tool promotes the new version to default unless you pass --no-default. To iterate:

wax -p acme_prod push-tool ./weather --change-summary "fix rounding"

Each push is a new immutable version; the agent picks up the default. Use --no-default to stage a version without promoting it.

Gotchas

  • Bump the agent version: when you re-push the agent after adding a tool.
  • v1 = pure Python, no deps. Need a library or a network call? Put that behind a domain and call it with ctx.domain(...) — that's also where per-end-user auth lives.
  • Don't hardcode the tool name in the system prompt — the platform may expose it as mcp__…__get_weather; tell the agent to "use the weather tool."

Next