> ## Documentation Index
> Fetch the complete documentation index at: https://developer.watson-orchestrate.ibm.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Writing tool schemas for Python tools

Define schemas for watsonX Orchestrate Python tools.

A tool schema defines the structure of data that flows into and out of your Python tools. It consists of two parts:

* **`input_schema`**: Defines what parameters a tool accepts. Wrap it in `ToolRequestBody.model_validate()`
* **`output_schema`**: Defines what data a tool returns. Wrap it in `ToolResponseBody.model_validate()`

Import `tool`, `ToolRequestBody`, and `ToolResponseBody` from the ADK package:

```python Python theme={null}
from ibm_watsonx_orchestrate.agent_builder.tools import tool, ToolRequestBody, ToolResponseBody
```

Schemas serve multiple purposes:

* **Runtime validation**: Ensures data conforms to expected types and constraints
* **UI generation**: Automatically creates forms in the WatsonX Orchestrate interface
* **Documentation**: Provides clear parameter descriptions for you and LLMs
* **API integration**: Uses in REST APIs for programmatic tool management

## Configuring input schemas

Input schemas define the parameters your tool accepts, serving as the contract between your tool and its callers. They specify what data you require, what is optional, and the expected format for each parameter.

Input schemas provide validation, automatic UI form generation, clear documentation, type safety, and guidance for AI agents on proper tool usage. Define schemas with clear parameter descriptions, appropriate types, minimal required fields, numeric constraints where applicable, and focused functionality.

You can configure input schemas in two primary forms: single required parameter for simple tools, or multiple parameters with a mix of required and optional fields for more complex operations.

### Configuring single parameter

This form of input schema has one required parameter. Use this pattern when your tool needs exactly one piece of information to function.

```python Python [expandable] theme={null}
@tool(
    description="Retrieves allowed values for the Country field",
    input_schema=ToolRequestBody.model_validate({
        "type": "object",
        "properties": {
            "substring": {
                "type": "string",
                "description": "Case-insensitive substring to filter countries. For example, use 'United' or 'Canada'",
            },
        },
        "required": ["substring"],
        "additionalProperties": False,
    }),
)
def search_countries(substring: str) -> str:
    # Implementation
    pass
```

### Configuring multiple parameters

When your tool needs multiple pieces of information, define a mix of required and optional parameters. This pattern is common for update operations where you must provide some fields while others remain optional.

* **Required parameters**: List them in the `required` array
* **Optional parameters**: Do not include them in the `required` array. Use `| None` (Python 3.10+) or `Optional[T]` from `typing` in the function signature
* **Default values**: Give optional parameters `= None` as default value

```python Python [expandable] theme={null}
@tool(
    description="Updates an existing payment method",
    input_schema=ToolRequestBody.model_validate({
        "type": "object",
        "properties": {
            "payment_method_id": {
                "type": "string",
                "description": "The unique identifier of the payment method to update",
            },
            "nickname": {
                "type": "string",
                "description": "Optional new nickname for the payment method",
            },
            "expiry_month": {
                "type": "integer",
                "minimum": 1,
                "maximum": 12,
                "description": "Optional new expiry month (1-12)",
            },
            "is_default": {
                "type": "boolean",
                "description": "Optional flag to set this as the default payment method",
            },
        },
        "required": ["payment_method_id"],
        "additionalProperties": False,
    }),
)
def update_payment_method(
    payment_method_id: str,
    nickname: str | None = None,
    expiry_month: int | None = None,
    is_default: bool | None = None,
) -> str:
    # Implementation
    pass
```

## Configuring output schemas

Output schemas define the structure of data your tool returns, serving as the contract between your tool and its consumers. They specify the format and type of data that callers can expect to receive.

Output schemas provide validation, automatic UI display generation, clear documentation, type safety, and proper data flow between tools and agents. Define schemas with clear descriptions, appropriate types, and structures that match your return values exactly.

You can configure output schemas in two primary forms: simple string returns for unstructured or formatted text, or Pydantic models for structured data with multiple fields.

### Returning simple parameters

Use simple string returns for tools that return unstructured text or single values. This is the most common pattern for tools that format their output as human-readable text.

```python Python theme={null}
output_schema=ToolResponseBody.model_validate({
    "type": "string",
    "description": "A human-readable string containing the result data",
})
```

Function signature:

```python Python theme={null}
def tool_name(...) -> str:
    return str(data)
```

### Returning multiple parameters

When your tool returns structured data with multiple fields of different types, use Pydantic models as return types. For more information, see [Pydantic models](#pydantic-models).

## Configuring dynamic schemas

In integration-heavy environments, schemas change frequently. Dynamic input and output schemas enable **select fields** in a tool's input to be mutable at runtime—securely and predictably—without affecting immutable core fields.

Use dynamic schemas when:

* You need to add new attributes to objects such as CRM records
* You need a small number of user-defined fields beyond a stable core API
* You want Builder UI users to change types for dynamic fields only. For example, change string to number

```python Python [expandable] theme={null}
from ibm_watsonx_orchestrate.agent_builder.tools import tool


@tool(
    enable_dynamic_input_schema=True,   # must be True for dynamic_input_schema to take effect
    enable_dynamic_output_schema=True,  # must be True for dynamic_output_schema to take effect
    dynamic_input_schema={  # schema for the extra **kwargs fields
        'type': 'object',
        'properties': {
            'c': {'type': 'integer', 'description': 'The third integer to add'}
        },
        'required': []
    },
    dynamic_output_schema={  # schema for the extra fields in the returned dict
        'type': 'object',
        'properties': {
            'sum': {'type': 'integer', 'description': 'The sum of a, b and c'},
            'c': {'type': 'integer', 'description': 'The third integer to add'}
        },
        'required': []
    }
)
def add_kwargs(a: int, b: int, **kwargs) -> dict:
    """
    Adds a, b, and an optional c (from kwargs) and returns the result.

    Args:
        a (int): The first integer to add.
        b (int): The second integer to add.

    Returns:
        dict: A dictionary with keys 'sum' (total of a + b + c) and 'c' (value of c, defaulting to 0).

    Example:
        >>> add_kwargs(3, 5, c=2)
        {'sum': 10, 'c': 2}
    """
    sum = a + b + kwargs.get("c", 0)
    return {'sum': sum, 'c': kwargs.get("c", 0)}


if __name__ == "__main__":
    print(add_kwargs(1, 2, c=3))
```

```python Python [expandable] theme={null}
from ibm_watsonx_orchestrate.agent_builder.tools import tool


@tool(
    enable_dynamic_input_schema=True,   # must be True for dynamic_input_schema to take effect
    enable_dynamic_output_schema=True,  # must be True for dynamic_output_schema to take effect
    dynamic_input_schema={  # schema for the extra **kwargs fields
        'type': 'object',
        'properties': {
            'c': {'type': 'string', 'description': 'The third word'}
        },
        'required': []
    },
    dynamic_output_schema={  # schema for the extra fields in the returned dict
        'type': 'object',
        'properties': {
            'message': {'type': 'string', 'description': 'The message that combines a, b and c'},
            'c': {'type': 'string', 'description': 'The third word'}
        },
        'required': []
    }
)
def combine_words(a: str, b: str, **kwargs) -> dict:
    """
    Receive required three inputs as a, b, c.
    Combine them and returns the result.

    Args:
        a (string): The first word.
        b (string): The second word.


    Returns:
        message: The message combines `a` and `b`.

    Example:
        >>> combine_words("hello", "world")
        "helloworld"
    """
    message = a + b + kwargs.get("c", "")
    return {'message': message, 'c': kwargs.get("c", "")}


if __name__ == "__main__":
    print(combine_words("hello", "world", c="Hana"))
```

## Parameter types

JSON Schema supports various data types for defining tool parameters. Each type has specific properties and validation rules that help ensure data integrity and provide clear documentation.

### String

Use the `string` type for text-based parameters such as names, descriptions, identifiers, or any free-form text input.

```python Python theme={null}
"parameter_name": {
    "type": "string",
    "description": "Description of the parameter",
}
```

### Integer

Use the `integer` type for whole numbers without decimal points. This is ideal for counts, indices, IDs, or any numeric value that should not have fractional parts.

```python Python theme={null}
"parameter_name": {
    "type": "integer",
    "description": "Description of the parameter",
}
```

You can add validation constraints to ensure values fall within acceptable ranges:

```python Python theme={null}
"parameter_name": {
    "type": "integer",
    "minimum": 1,
    "maximum": 12,
    "description": "Month value between 1 and 12",
}
```

### Number (Float)

Use the `number` type for numeric values that may include decimal points. This type accepts both integers and floating-point numbers.

```python Python theme={null}
"parameter_name": {
    "type": "number",
    "description": "Description of the parameter",
}
```

You can add validation constraints to ensure values fall within acceptable ranges:

```python Python theme={null}
"parameter_name": {
    "type": "number",
    "minimum": 1,
    "maximum": 12,
    "description": "Month value between 1 and 12",
}
```

### Boolean

Use the `boolean` type for true or false values.

```python Python theme={null}
"parameter_name": {
    "type": "boolean",
    "description": "Description of the parameter",
}
```

### Date

Use the `string` type with `format: "date"` for date values. Dates must be in ISO 8601 format: YYYY-MM-DD.

```python Python theme={null}
"parameter_name": {
    "type": "string",
    "format": "date",
    "description": "Date in YYYY-MM-DD format",
}
```

Example values: `"2024-01-15"`, `"2023-12-31"`, `"2025-06-01"`

### Nested object

Use the `object` type for complex parameters that contain multiple related fields. This allows you to group related data together in a structured way.

```python Python [expandable] theme={null}
"parameter_name": {
    "type": "object",
    "description": "Description of the object",
    "properties": {
        "nested_field1": {
            "type": "string",
            "description": "Description of nested field",
        },
        "nested_field2": {
            "type": "string",
            "description": "Description of nested field",
        },
    },
    "required": ["nested_field1", "nested_field2"],
}
```

Best practices:

* Always include a `description` for the object and each nested field
* Use the `required` array to specify which nested fields are mandatory
* Keep nesting levels reasonable and avoid deeply nested structures when possible
* Consider using Pydantic models for complex nested structures. For more information, see [Pydantic models](#pydantic-models)

### Pydantic models

When you have complex nested structures or want type-safe parameter definitions, you can define Pydantic models and use them as both input parameters and return types.

Benefits of Pydantic models:

* **Type validation**: Automatic validation of data structure
* **Clear contracts**: Explicit definition of what the tool accepts and returns
* **IDE support**: Autocomplete and type hints when working with parameters and results
* **Reusability**: Use models across multiple tools
* **Documentation**: Self-documenting parameters and return values

#### Using as input parameters

First, create your Pydantic model classes with clear docstrings and type hints:

```python Python [expandable] theme={null}
from pydantic import BaseModel

class ReadDocumentPageRange(BaseModel):
    """Inclusive, 1-indexed range of pages to read from a document."""
    start: int
    end: int

class NamedDocument(BaseModel):
    """A reference to a previously uploaded document."""
    name: str
    key: str
```

Then, use your Pydantic models in the input schema. Each field in the model must be represented in the schema properties:

```python Python [expandable] theme={null}
@tool(
    description="Reads content from a document with optional page range",
    input_schema=ToolRequestBody.model_validate({
        "type": "object",
        "properties": {
            "key": {
                "type": "string",
                "description": "The unique key associated with the document",
            },
            "page_range": {
                "type": "object",
                "description": "Optional range of pages to read",
                "properties": {
                    "start": {
                        "type": "integer",
                        "description": "The 1-indexed page number to start reading at",
                    },
                    "end": {
                        "type": "integer",
                        "description": "The page number to end reading at (inclusive)",
                    },
                },
                "required": ["start", "end"],
            },
        },
        "required": ["key"],
        "additionalProperties": False,
    }),
)
def read_document(
    key: str,
    page_range: ReadDocumentPageRange | None = None
) -> str:
    """Reads content from a document with optional page range filtering.
    
    Args:
        key: Unique identifier for the document
        page_range: Optional page range to read. If None, reads entire document
    
    Returns:
        Document content as a string
    """
    # Implementation
    pass
```

<Warning>
  Important:

  * Use descriptive class names that clearly indicate the data structure
  * Add docstrings to explain what the model represents
  * Define all fields with appropriate type hints
  * Use optional fields with `| None` or `Optional[T]` for non-required parameters
  * Match the schema properties to the Pydantic model fields exactly
  * Include required fields in the model in the schema's `required` array
  * Ensure each nested object in the schema corresponds to a Pydantic model
</Warning>

#### Using as return parameter

When your tool returns structured data with multiple fields of different types, define a Pydantic model as the return type. This provides type safety and clear structure for complex return values.

Define the Pydantic model:

```python Python theme={null}
from pydantic import BaseModel

class DocumentUploadResult(BaseModel):
    """Result returned after uploading a document."""
    status: str
    key: str
```

Use in output schema:

Match the output schema exactly to your Pydantic model's structure:

```python Python [expandable] theme={null}
@tool(
    description="Accepts a document upload from the user",
    output_schema=ToolResponseBody.model_validate({
        "type": "object",
        "properties": {
            "status": {
                "type": "string",
                "description": "Status of the upload",
            },
            "key": {
                "type": "string",
                "description": "Unique key identifying the uploaded document",
            },
        },
        "required": ["status", "key"],
        "additionalProperties": False,
    }),
)
def accept_document_upload(content: bytes) -> DocumentUploadResult:
    """Accepts and processes a document upload.
    
    Args:
        content: Raw bytes of the document to upload
    
    Returns:
        DocumentUploadResult with status and unique key
    """
    # Implementation
    pass
```

Complete example with complex return types:

This example shows a tool that returns a complex structure with arrays and nested objects:

```python Python [expandable] theme={null}
from pydantic import BaseModel

class AccountList(BaseModel):
    """List of bank accounts with their details."""
    accounts: list[dict[str, str]]
    total_accounts: int

@tool(
    description="Get a list of all bank accounts with their types and balances",
    output_schema=ToolResponseBody.model_validate({
        "type": "object",
        "properties": {
            "accounts": {
                "type": "array",
                "description": "List of account objects",
                "items": {
                    "type": "object",
                    "properties": {
                        "account_number": {"type": "string"},
                        "account_type": {"type": "string"},
                        "balance": {"type": "number"},
                    },
                },
            },
            "total_accounts": {
                "type": "integer",
                "description": "Total number of accounts",
            },
        },
        "required": ["accounts", "total_accounts"],
        "additionalProperties": False,
    }),
)
def list_accounts() -> AccountList:
    """Retrieves all bank accounts with their details.
    
    Returns:
        AccountList containing all accounts and total count
    """
    # Implementation
    pass
```

### AgentRun context

When your tools need access to user identity, credentials, or context variables, use the `AgentRun` parameter. This parameter is special and you should NOT include it in the input schema.

<Note>
  The framework automatically injects the `AgentRun` context parameter. Do not define it in your input schema. Only include it in your function signature.
</Note>

```python Python [expandable] theme={null}
from ibm_watsonx_orchestrate.run.context import AgentRun

@tool(
    description="Creates a legal matter using user context",
    input_schema=ToolRequestBody.model_validate({
        "type": "object",
        "properties": {
            "matter_title": {
                "type": "string",
                "description": "Title of the legal matter",
            },
            "matter_description": {
                "type": "string",
                "description": "Brief description of the matter",
            },
        },
        "required": ["matter_title", "matter_description"],
        "additionalProperties": False,
    }),
)
def create_matter(context: AgentRun, matter_title: str, matter_description: str) -> str:
    """Creates a new legal matter with user context."""
    # Access user identity from context
    user_email = context.request_context.get('wxo_email_id')
    user_name = context.request_context.get('wxo_user_name')
    
    return f"Matter '{matter_title}' created by {user_name} ({user_email})"
```

## Important rules

1. **Every parameter must have a description** - You need this for the parameter to show up in the WatsonX UI

2. **Parameter names must match** - Match schema property names exactly to function parameter names

3. **List only required parameters** - Only include parameters in the `required` array that you truly require

4. **Optional parameters** - Mark optional parameters with `| None` (Python 3.10+) or `Optional[T]` from `typing` in the function signature, and set their default value to `= None`

5. **Output schema must match return type** - Match the output schema exactly to the Pydantic model structure or return type

6. **Context parameter is special** - The framework injects the `context: AgentRun` parameter and you should NOT include it in the input schema. For more information, see [AgentRun context](#agentrun-context)

7. **Function signature must match schema** - Match the function parameters to the input schema properties exactly:

   ```python Python [expandable] theme={null}
   input_schema=ToolRequestBody.model_validate({
       "type": "object",
       "properties": {
           "origin": {"type": "string", "description": "..."},
           "destination": {"type": "string", "description": "..."},
           "max_price": {"type": "number", "description": "..."},
       },
       "required": ["origin"],
       "additionalProperties": False,
   })

   def search_flights(
       origin: str,                    # Required, in "required" list
       destination: str | None = None, # Optional, not in "required" list
       max_price: float | None = None, # Optional, not in "required" list
   ) -> str:
       # Implementation
       pass
   ```

## See also

* [Tool response structure and annotations](tool_response_structure) - Learn about returning structured responses with widgets
* [Widget integration](widget_integration) - Add interactive forms to your tool responses
* [Creating tools](create_tool) - General guide to creating Python tools
