Supyagent
Tools

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_tool

This 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.

powers/weather.py
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

PatternUsageExample
Field(...)Required fieldname: str = Field(..., description="...")
Field(default=X)Optional with defaultlimit: int = Field(default=10, description="...")
Optional[str]Nullable fieldtag: Optional[str] = Field(default=None, description="...")
List[str]Array fieldtags: 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.

powers/weather.py
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")

Follow the ok / data / error convention used throughout supyagent:

class MyOutput(BaseModel):
    success: bool
    result: Optional[str] = None
    error: Optional[str] = None

Step 4: Write the Function

The function name becomes part of the tool identifier. An agent calls it as weather:get_weather.

powers/weather.py
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:

powers/weather.py
# /// script
# dependencies = ["pydantic", "httpx"]
# ///

Complete Example

Here is the full tool, from top to bottom:

powers/weather.py
# /// 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 .env

Output:

{"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:

RuleReason
Function has exactly one parameter named inputSupypowers discovers functions by this signature
input must be typed as a Pydantic BaseModelEnables automatic schema generation and validation
No print() statementsOutput goes to stdout which breaks JSON result parsing
No input() callsThere is no interactive terminal during agent execution
Dependencies in the # /// script blockEach script is executed in its own isolated uv environment
Return a dict or Pydantic modelResults are serialized to JSON automatically
Write a docstringIt 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:

powers/math_tools.py
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