Create tools for your agents. With ADK, tools can be created using a Python program or an OpenAPI specification.

Creating Python-Based Tools

You can create Python-based tools by defining functions in a Python file and annotating them with the @tool decorator. Each tool must include a docstring to describe its purpose, inputs, and outputs. These elements help the agent understand how to use the tool effectively. Sample Python tool
sample.py
#test_tool.py
from ibm_watsonx_orchestrate.agent_builder.tools import tool, ToolPermission


@tool(name="myName", description="the description", permission=ToolPermission.ADMIN)
def my_tool(input: str) -> str:
    """Executes the tool's action based on the provided input.

    Args:
        input (str): The input of the tool.

    Returns:
        str: The action of the tool.
    """

    #functionality of the tool

Using the @tool decorator

The @tool decorator converts a Python function into a callable tool that agents can invoke. Place the decorator directly above the function you want to expose. You can also configure optional parameters to enhance the tool’s behavior:
Python
@tool(
    name: str = None,
    description: str = None,
    input_schema: ToolRequestBody = None,
    output_schema: ToolResponseBody = None,
    permission: ToolPermission = ToolPermission.READ_ONLY,
    expected_credentials: List[ExpectedCredentials] = None,
    display_name: str = None,
    kind: PythonToolKind = PythonToolKind.TOOL
)

Writing a docstring

Each tool can include a docstring that describes its functionality. This description helps the agent understand the tool’s purpose and how to use it.
  • Python tools must use Google-style docstrings to apply descriptions and document the tool.
    docstrings.py
    @tool
    def tool(input: str) -> str:
        """
        My tool's description.
        
        Args:
            input (str): Input of the tool.
    
        Returns:
            str: Return of the tool.
        """
    
        return "Hello, world!"
    
  • Docstrings provide the descriptions of what your tool does. If you want to provide simpler descriptions, you can pass a description to the @tool decorator:
    docstrings.py
    @tool(description="Description of the tool")
    def tool(input: str) -> str:
        return "Hello, world!"
    

Input and ouput types

Your Python tools must use one of the following types: Tools can’t return unsupported types (such as numpy.arrays or pandas.DataFrame, for example). In cases that you want to return custom types, you can wrap them in a Pydantic BaseModel:
data_fetcher.py
from typing import Dict, Any
import json
import pandas as pd
from pydantic import BaseModel
from ibm_watsonx_orchestrate.agent_builder.tools import tool, ToolPermission


# Define the Pydantic model for the response
class SummaryStats(BaseModel):
    column: str
    mean: float
    min: float
    max: float


class DataFrameSummary(BaseModel):
    row_count: int
    column_count: int
    summaries: list[SummaryStats]


@tool(
    name="simple_dataframe_summary",
    permission=ToolPermission.ADMIN,
)
def simple_dataframe_summary(input: str) -> str:
    """
    Compute basic summary statistics (mean, min, max) for numeric columns in a CSV string.
    
    Args:
        input (str): JSON string with a 'csv' field containing CSV data.

    Returns:
        str: JSON string of DataFrameSummary (Pydantic BaseModel).
    """
    try:
        payload = json.loads(input)
        csv_data = payload.get("csv")
        if not csv_data:
            return json.dumps({"error": "Missing 'csv' field in input."})

        # Create DataFrame
        df = pd.read_csv(pd.compat.StringIO(csv_data))

        # Compute summary for numeric columns
        summaries = []
        for col in df.select_dtypes(include="number").columns:
            summaries.append(SummaryStats(
                column=col,
                mean=float(df[col].mean()),
                min=float(df[col].min()),
                max=float(df[col].max())
            ))

        result = DataFrameSummary(
            row_count=len(df),
            column_count=df.shape[1],
            summaries=summaries
        )

        return result.model_dump_json()
    except Exception as e:
        return json.dumps({"error": str(e)})

Using external libraries

If your Python function relies on external libraries or packages, ensure that your tool works correctly by specifying these dependencies in the requirements.txt file:
requirements.txt
ibm-watsonx-orchestrate
numpy
vector_stats.py
from typing import Any, Dict, List
import json
import numpy as np
from ibm_watsonx_orchestrate.agent_builder.tools import tool, ToolPermission


@tool(
    name="vector_stats",
    permission=ToolPermission.ADMIN,
)
def vector_stats(input: str) -> str:
    """
    Compute descriptive statistics for a numeric vector
    with optional L2 normalization and z-score based outlier
    detection. Input is a JSON string.
    
    Executes vector statistics on a list of numbers.

    Args:
        input (str): JSON string with fields:
            - values (list[number] | str): The numeric values. If str, comma-separated.
            - normalize (bool, optional): If true, returns L2-normalized vector.
            - zscore_outliers (bool, optional): If true, returns z-score outliers.
            - z_thresh (float, optional): Z-score threshold (default 3.0).
            - precision (int, optional): Decimal places for rounding (default 6).

    Returns:
        str: JSON string containing statistics and (optionally) normalization and outliers.
    """
    # 1) Parse input into a payload
    try:
        payload = json.loads(input)
    except json.JSONDecodeError:
        # Fallback: assume it's a plain string of comma-separated numbers
        payload = {"values": input}

    # 2) Extract values and coerce to float list
    values = payload.get("values")
    if isinstance(values, str):
        items = [v.strip() for v in values.split(",") if v.strip()]
        data = [float(v) for v in items]
    elif isinstance(values, list):
        data = [float(v) for v in values]
    else:
        return json.dumps({"error": "`values` must be a list or a comma-separated string."})

    arr = np.asarray(data, dtype=float)

    # Filter non-finite (NaN/inf)
    finite_mask = np.isfinite(arr)
    arr = arr[finite_mask]

    if arr.size == 0:
        return json.dumps({"error": "No valid numeric values after filtering NaN/inf."})

    precision = int(payload.get("precision", 6))

    # 3) Compute robust statistics
    result: Dict[str, Any] = {
        "count": int(arr.size),
        "mean": round(float(np.mean(arr)), precision),
        "median": round(float(np.median(arr)), precision),
        "std": round(float(np.std(arr, ddof=1)) if arr.size > 1 else 0.0, precision),
        "min": round(float(np.min(arr)), precision),
        "max": round(float(np.max(arr)), precision),
        "sum": round(float(np.sum(arr)), precision),
        "p25": round(float(np.percentile(arr, 25)), precision),
        "p75": round(float(np.percentile(arr, 75)), precision),
    }

    # 4) Optional normalization (L2)
    if payload.get("normalize"):
        norm = np.linalg.norm(arr)
        if norm == 0:
            normalized = [0.0] * arr.size
        else:
            normalized = (arr / norm).tolist()
        result["normalized_l2"] = [round(float(x), precision) for x in normalized]

    # 5) Optional z-score outlier detection
    if payload.get("zscore_outliers"):
        z_thresh = float(payload.get("z_thresh", 3.0))
        mu = float(np.mean(arr))
        sigma = float(np.std(arr, ddof=1)) if arr.size > 1 else 0.0

        if sigma == 0.0:
            outlier_idx: List[int] = []
        else:
            z = (arr - mu) / sigma
            outlier_idx = np.where(np.abs(z) > z_thresh)[0].tolist()

        result["outliers"] = {
            "indices": outlier_idx,
            "values": [round(float(arr[i]), precision) for i in outlier_idx],
            "z_thresh": z_thresh,
        }

    return json.dumps(result)

Creating tools that accept files

You can create Python tools that accept files or return files to download. To do that, you must comply with the following requirements:
  • To accept files as input, the tool must accept a sequence of bytes as arguments.
  • To return a file for download, the tool must return a sequence of bytes as output.
The following example accepts a file as input and returns the processed file for download:
create_images_with_filter.py
from typing import Union
from io import BytesIO
from typing import Any
from ibm_watsonx_orchestrate.agent_builder.tools import tool, ToolPermission
import base64

@tool(permission=ToolPermission.READ_ONLY)
def create_image_with_filter(name: str, age: int, image_bytes: bytes) -> bytes:
    """Gets user information, applies a filter to the image, and returns the processed image.

    Args:
        name (str): The user's name.
        age (int): The user's age.
        image_bytes (bytes): The original image in bytes format.

    Returns:
        bytes: The processed image in bytes format.
    """

    import cv2
    import numpy as np
    from typing import Union
    from io import BytesIO
    # Convert bytes to numpy array
    image_array = np.frombuffer(image_bytes, np.uint8)
    image = cv2.imdecode(image_array, cv2.IMREAD_COLOR)

    if image is None:
        raise ValueError("Invalid image bytes provided")

    # Apply bilateral filter for smoothing while preserving edges
    smooth = cv2.bilateralFilter(image, d=9, sigmaColor=75, sigmaSpace=75)

    # Convert to grayscale and detect edges
    gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
    edges = cv2.medianBlur(gray, 7)
    edges = cv2.adaptiveThreshold(
        edges, 255, cv2.ADAPTIVE_THRESH_MEAN_C, 
        cv2.THRESH_BINARY, blockSize=9, C=2
    )

    # Convert edges to color image
    edges_colored = cv2.cvtColor(edges, cv2.COLOR_GRAY2BGR)

    # Combine smoothed image with edges
    cartoon = cv2.bitwise_and(smooth, edges_colored)

    # Enhance color saturation
    hsv = cv2.cvtColor(cartoon, cv2.COLOR_BGR2HSV)
    hsv[..., 1] = cv2.multiply(hsv[..., 1], 1.4)  # increase saturation
    cartoon_enhanced = cv2.cvtColor(hsv, cv2.COLOR_HSV2BGR)

    # Convert back to bytes
    success, buffer = cv2.imencode('.jpg', cartoon_enhanced)
    if not success:
        raise RuntimeError("Failed to encode image")
    
    return buffer.tobytes()

More examples

from typing import List

import requests
from pydantic import BaseModel, Field
from enum import Enum

from ibm_watsonx_orchestrate.agent_builder.tools import tool, ToolPermission


class ContactInformation(BaseModel):
    phone: str
    email: str


class HealthcareSpeciality(str, Enum):
    GENERAL_MEDICINE = 'General Medicine'
    CARDIOLOGY = 'Cardiology'
    PEDIATRICS = 'Pediatrics'
    ORTHOPEDICS = 'Orthopedics'
    ENT = 'Ear, Nose and Throat'
    MULTI_SPECIALTY = 'Multi-specialty'


class HealthcareProvider(BaseModel):
    provider_id: str = Field(None, description="The unique identifier of the provider")
    name: str = Field(None, description="The providers name")
    provider_type: str = Field(None, description="Type of provider, (e.g. Hospital, Clinic, Individual Practitioner)")
    specialty: HealthcareSpeciality = Field(None, description="Medical speciality, if applicable")
    address: str = Field(None, description="The address of the provider")
    contact: ContactInformation = Field(None, description="The contact information of the provider")


@tool
def search_healthcare_providers(
        location: str,
        specialty: HealthcareSpeciality = HealthcareSpeciality.GENERAL_MEDICINE
) -> List[HealthcareProvider]:
    """
    Retrieve a list of the nearest healthcare providers based on location and optional specialty. Infer the
    speciality of the location from the request.

    Args:
        location: Geographic location to search providers in (city, state, zip code, etc.)
        specialty: (Optional) Medical specialty to filter providers by (Must be one of: "ENT", "General Medicine", "Cardiology", "Pediatrics", "Orthopedics", "Multi-specialty")

    Returns:
      A list of healthcare providers near a particular location for a given speciality
    """
    resp = requests.get(
        'https://find-provider.1sqnxi8zv3dh.us-east.codeengine.appdomain.cloud',
        params={
            'location': location,
            'speciality': specialty
        }
    )
    resp.raise_for_status()
    return resp.json()['providers']

Creating OpenAPI-based tools

OpenAPI tools are created based on an OpenAPI specification file. To import an OpenAPI tool, configure the servers and paths properties; the entire OpenAPI specification file is not required. Example:
YAML
servers:
- url: mytoolserver
paths:
/:
   get:
      operationId: myTool
      summary: myName
      description: the description
      requestBody:
         ...
      responses:
         ...
You can import asynchronous APIs as tools using an OpenAPI specification. To support this, the OpenAPI specification must include a callbacks schema and an input parameter named callbackUrl. Use callbackUrl exactly as the parameter name; other names are not supported. The supported type for the callback response is application/json.
YAML
servers:
  - url: mytoolserver
paths:
/:
   post:
      operationId: myTool
      summary: myName
      description: the description
      parameters:
        - in: header
          name: callbackUrl
          description: The callback URL
          required: true
          schema:
            type: string
      requestBody:
         ...
      callbacks:
        callback:
          '{$request.header.callbackUrl}':
            post:
              requestBody:
              ...
              responses:
              ...