Creating Tools
Step-by-step guide to building custom tools for your agents using the supypowers framework.
Creating Tools
Every tool is a self-contained Python script that lives in the powers/ directory. This guide walks you through creating a tool from scratch.
Step 1: Generate the Scaffold
Use the supypowers new command to create a new tool from the built-in template:
supypowers new my_toolThis creates powers/my_tool.py with a ready-to-edit template. The command outputs:
{
"ok": true,
"created": "powers/my_tool.py",
"run_with": "supypowers run my_tool:my_tool '{\"value\": \"test\"}'"
}If you haven't initialized a project yet, run supypowers init first to create the powers/ directory.
Step 2: Define the Input Model
Every function takes exactly one parameter named input, typed as a Pydantic BaseModel. The field descriptions are critical -- they tell the LLM what each field means and how to use it.
from pydantic import BaseModel, Field
class WeatherInput(BaseModel):
"""Get current weather for a location."""
city: str = Field(..., description="City name (e.g., 'San Francisco')")
units: str = Field(
default="metric",
description="Temperature units: 'metric' (Celsius) or 'imperial' (Fahrenheit)"
)Field Guidelines
| Pattern | Usage | Example |
|---|---|---|
Field(...) | Required field | name: str = Field(..., description="...") |
Field(default=X) | Optional with default | limit: int = Field(default=10, description="...") |
Optional[str] | Nullable field | tag: Optional[str] = Field(default=None, description="...") |
List[str] | Array field | tags: List[str] = Field(default_factory=list, description="...") |
Always use description in every Field() -- this is what the LLM reads to understand how to call your tool.
Step 3: Define the Output Model
An output model is recommended but optional. If you return a plain dict, it works too.
class WeatherOutput(BaseModel):
"""Weather data response."""
ok: bool = Field(..., description="Whether the lookup succeeded")
temperature: Optional[float] = Field(default=None, description="Temperature in requested units")
condition: Optional[str] = Field(default=None, description="Weather condition (e.g., 'sunny')")
error: Optional[str] = Field(default=None, description="Error message if lookup failed")Recommended Output Pattern
Follow the ok / data / error convention used throughout supyagent:
class MyOutput(BaseModel):
success: bool
result: Optional[str] = None
error: Optional[str] = NoneStep 4: Write the Function
The function name becomes part of the tool identifier. An agent calls it as weather:get_weather.
def get_weather(input: WeatherInput) -> WeatherOutput:
"""Get the current weather for a city. Returns temperature and conditions."""
try:
resp = httpx.get(
"https://api.weatherapi.com/v1/current.json",
params={"key": os.environ.get("WEATHER_API_KEY"), "q": input.city},
timeout=10,
)
resp.raise_for_status()
data = resp.json()
temp = data["current"]["temp_c"] if input.units == "metric" else data["current"]["temp_f"]
return WeatherOutput(
ok=True,
temperature=temp,
condition=data["current"]["condition"]["text"],
)
except Exception as e:
return WeatherOutput(ok=False, error=str(e))The docstring becomes the function's description in the tool schema -- write it clearly for the LLM.
Step 5: Add the Script Header
Every script needs the # /// script metadata block at the top to declare its dependencies:
# /// script
# dependencies = ["pydantic", "httpx"]
# ///Complete Example
Here is the full tool, from top to bottom:
# /// script
# dependencies = ["pydantic", "httpx"]
# ///
"""
Weather lookup tool.
Run with: supypowers run weather:get_weather '{"city": "London"}'
"""
import os
from typing import Optional
import httpx
from pydantic import BaseModel, Field
class WeatherInput(BaseModel):
"""Get current weather for a location."""
city: str = Field(..., description="City name (e.g., 'San Francisco')")
units: str = Field(
default="metric",
description="Temperature units: 'metric' (Celsius) or 'imperial' (Fahrenheit)"
)
class WeatherOutput(BaseModel):
"""Weather data response."""
ok: bool = Field(..., description="Whether the lookup succeeded")
temperature: Optional[float] = Field(default=None, description="Current temperature")
condition: Optional[str] = Field(default=None, description="Weather condition")
error: Optional[str] = Field(default=None, description="Error message if failed")
def get_weather(input: WeatherInput) -> WeatherOutput:
"""Get the current weather for a city. Returns temperature and conditions."""
try:
api_key = os.environ.get("WEATHER_API_KEY")
if not api_key:
return WeatherOutput(ok=False, error="WEATHER_API_KEY not set")
resp = httpx.get(
"https://api.weatherapi.com/v1/current.json",
params={"key": api_key, "q": input.city},
timeout=10,
)
resp.raise_for_status()
data = resp.json()
temp = data["current"]["temp_c"] if input.units == "metric" else data["current"]["temp_f"]
return WeatherOutput(
ok=True,
temperature=temp,
condition=data["current"]["condition"]["text"],
)
except Exception as e:
return WeatherOutput(ok=False, error=str(e))Running Your Tool
Test it from the command line:
# Run with inline secrets
supypowers run weather:get_weather '{"city": "London"}' --secrets WEATHER_API_KEY=abc123
# Run with a .env file
supypowers run weather:get_weather '{"city": "Paris"}' --secrets .envOutput:
{"ok": true, "data": {"ok": true, "temperature": 18.5, "condition": "Partly cloudy", "error": null}}The Supypower Contract
These are the hard rules every tool must follow:
| Rule | Reason |
|---|---|
Function has exactly one parameter named input | Supypowers discovers functions by this signature |
input must be typed as a Pydantic BaseModel | Enables automatic schema generation and validation |
No print() statements | Output goes to stdout which breaks JSON result parsing |
No input() calls | There is no interactive terminal during agent execution |
Dependencies in the # /// script block | Each script is executed in its own isolated uv environment |
| Return a dict or Pydantic model | Results are serialized to JSON automatically |
| Write a docstring | It becomes the function description in the tool schema |
Multiple Functions Per Script
A single script can contain multiple functions. Each one becomes a separate tool:
def add(input: AddInput) -> AddOutput:
"""Add two numbers together."""
...
def multiply(input: MultiplyInput) -> MultiplyOutput:
"""Multiply two numbers together."""
...The agent sees these as math_tools:add and math_tools:multiply.
What's Next
- Dependencies -- Managing per-script dependency isolation
- Testing -- Test tools before giving them to agents
- Examples -- Annotated examples for common patterns