Supyagent
Examples & Recipes

Custom Tool

Build and deploy a custom API integration tool end-to-end, from creation with supypowers to use in an agent.

Building a Custom Tool

This walkthrough takes you through building a custom tool from scratch: creating the script with supypowers, implementing the logic, testing it, and using it in an agent. We will build a weather lookup tool that calls an external API.

Step 1: Create the Tool Scaffold

Use supyagent tools new to create a tool from template:

supyagent tools new weather

This creates powers/weather.py with a Pydantic-based scaffold:

powers/weather.py
# /// script
# dependencies = ["pydantic"]
# ///
"""
weather - Custom supypowers tool.
"""
from pydantic import BaseModel, Field


class WeatherInput(BaseModel):
    """Weather input parameters."""
    value: str = Field(..., description="Input value")


class WeatherOutput(BaseModel):
    """Weather output."""
    ok: bool
    data: str | None = None
    error: str | None = None


def weather(input: WeatherInput) -> WeatherOutput:
    """
    Describe what this tool does.

    Examples:
        >>> weather({"value": "test"})
    """
    try:
        return WeatherOutput(ok=True, data=f"Processed: {input.value}")
    except Exception as e:
        return WeatherOutput(ok=False, error=str(e))

Step 2: Implement the Tool

Replace the scaffold with a real implementation. We will add httpx as a dependency and call the Open-Meteo API (free, no API key required):

powers/weather.py
# /// script
# dependencies = ["pydantic", "httpx"]
# ///
"""
weather - Get current weather for a location using Open-Meteo API.
"""
import httpx
from pydantic import BaseModel, Field


class GetWeatherInput(BaseModel):
    """Input for get_weather function."""
    latitude: float = Field(..., description="Latitude of the location")
    longitude: float = Field(..., description="Longitude of the location")
    city: str = Field(default="", description="City name (for display purposes)")


class GetWeatherOutput(BaseModel):
    """Weather data output."""
    ok: bool
    data: dict | None = None
    error: str | None = None


def get_weather(input: GetWeatherInput) -> GetWeatherOutput:
    """
    Get current weather conditions for a latitude/longitude.

    Returns temperature, wind speed, and weather description.

    Examples:
        >>> get_weather({"latitude": 40.7128, "longitude": -74.0060, "city": "New York"})
    """
    try:
        url = "https://api.open-meteo.com/v1/forecast"
        params = {
            "latitude": input.latitude,
            "longitude": input.longitude,
            "current_weather": True,
            "temperature_unit": "celsius",
        }

        response = httpx.get(url, params=params, timeout=10)
        response.raise_for_status()
        data = response.json()

        current = data.get("current_weather", {})

        weather_codes = {
            0: "Clear sky", 1: "Mainly clear", 2: "Partly cloudy",
            3: "Overcast", 45: "Foggy", 48: "Depositing rime fog",
            51: "Light drizzle", 53: "Moderate drizzle", 55: "Dense drizzle",
            61: "Slight rain", 63: "Moderate rain", 65: "Heavy rain",
            71: "Slight snow", 73: "Moderate snow", 75: "Heavy snow",
            80: "Slight rain showers", 81: "Moderate rain showers",
            82: "Violent rain showers", 95: "Thunderstorm",
        }

        result = {
            "city": input.city or f"{input.latitude}, {input.longitude}",
            "temperature_celsius": current.get("temperature"),
            "wind_speed_kmh": current.get("windspeed"),
            "wind_direction": current.get("winddirection"),
            "description": weather_codes.get(
                current.get("weathercode", -1), "Unknown"
            ),
        }

        return GetWeatherOutput(ok=True, data=result)

    except httpx.HTTPStatusError as e:
        return GetWeatherOutput(ok=False, error=f"API error: {e.response.status_code}")
    except httpx.RequestError as e:
        return GetWeatherOutput(ok=False, error=f"Request failed: {e}")
    except Exception as e:
        return GetWeatherOutput(ok=False, error=str(e))


class GeolocateInput(BaseModel):
    """Input for geolocate function."""
    city: str = Field(..., description="City name to look up")


class GeolocateOutput(BaseModel):
    """Geolocation data output."""
    ok: bool
    data: dict | None = None
    error: str | None = None


def geolocate(input: GeolocateInput) -> GeolocateOutput:
    """
    Look up latitude/longitude for a city name using Open-Meteo geocoding.

    Examples:
        >>> geolocate({"city": "Tokyo"})
    """
    try:
        url = "https://geocoding-api.open-meteo.com/v1/search"
        params = {"name": input.city, "count": 1}

        response = httpx.get(url, params=params, timeout=10)
        response.raise_for_status()
        data = response.json()

        results = data.get("results", [])
        if not results:
            return GeolocateOutput(ok=False, error=f"City not found: {input.city}")

        location = results[0]
        return GeolocateOutput(ok=True, data={
            "city": location.get("name"),
            "country": location.get("country"),
            "latitude": location.get("latitude"),
            "longitude": location.get("longitude"),
        })

    except Exception as e:
        return GeolocateOutput(ok=False, error=str(e))

Key Design Points

  • Two functions in one script: get_weather and geolocate become two tools (weather__get_weather and weather__geolocate)
  • Dependencies declared: httpx is in the # /// script block -- uv installs it automatically
  • Pydantic models: Input and output are typed with Field descriptions for clear tool schemas
  • Error handling: Errors are returned in the ok/error pattern, never raised
  • No print(): Output is pure JSON

Step 3: Test the Tool

Using supypowers Directly

# Test geolocation
supypowers run weather:geolocate '{"city": "Tokyo"}'

Output:

{
  "ok": true,
  "data": {
    "city": "Tokyo",
    "country": "Japan",
    "latitude": 35.6895,
    "longitude": 139.6917
  }
}
# Test weather lookup
supypowers run weather:get_weather '{"latitude": 35.6895, "longitude": 139.6917, "city": "Tokyo"}'

Using supyagent tools test

supyagent tools test weather__geolocate '{"city": "London"}'
supyagent tools test weather__get_weather '{"latitude": 51.5074, "longitude": -0.1278, "city": "London"}'

Using supypowers test

supypowers test weather:geolocate
supypowers test weather:get_weather --fixture test_weather_input.json

Step 4: Verify Tool Discovery

Check that your new tools are discovered:

supyagent tools list

You should see:

Name                     Description                                Source
weather__get_weather     Get current weather conditions...          weather.py
weather__geolocate       Look up latitude/longitude for a city...   weather.py

If the tools do not appear, try reloading in chat with /reload.

Step 5: Use in an Agent

The tools are automatically available to any agent with matching tool permissions:

supyagent chat myagent
You: What's the weather like in Paris right now?

myagent> Let me look that up for you.

  [tool: weather__geolocate] city="Paris" ...
  [tool: weather__get_weather] latitude=48.8566, longitude=2.3522 ...

The current weather in Paris:
- Temperature: 12°C
- Conditions: Partly cloudy
- Wind: 15 km/h from the west

Restricting Tool Access

If you want only specific agents to use the weather tool:

agents/weather-agent.yaml
tools:
  allow:
    - "weather__*"     # Only weather tools
    - "web__*"         # Plus web tools

Step 6: Add Secrets (If Needed)

If your API requires authentication, use environment variables:

import os

def call_paid_api(input: ApiInput) -> ApiOutput:
    api_key = os.environ.get("WEATHER_API_KEY")
    if not api_key:
        return ApiOutput(ok=False, error="WEATHER_API_KEY not set. Use /creds set WEATHER_API_KEY")
    # Use api_key in your request

Store the key:

supyagent config set WEATHER_API_KEY

Or declare it in the agent YAML:

credentials:
  - name: WEATHER_API_KEY
    description: "API key for premium weather service"
    required: true

Advanced: Tools with External Dependencies

For tools that need system packages or complex dependencies:

# /// script
# dependencies = ["pydantic", "httpx", "beautifulsoup4", "lxml"]
# ///

All dependencies are resolved by uv at execution time. No manual installation needed.

Advanced: Multi-File Tool Organization

For complex tools, you can organize into subdirectories within powers/:

powers/
  weather.py          # Simple weather tool
  github/
    issues.py         # GitHub issue management
    repos.py          # Repository operations
  slack/
    messages.py       # Message sending
    channels.py       # Channel management

Use supypowers docs --recursive to discover tools in subdirectories.