---
title: Agent Tools
---

# Agent Tools

Tools allow agents to interact with the outside world. By providing tools, you enable your agents to perform actions such as executing code, fetching data from APIs, querying databases, or performing complex calculations.

Under the hood, a tool is a standard Python function accompanied by a schema that describes its purpose, inputs, and outputs to the underlying Large Language Model (LLM).

## Creating Agent Tools

The easiest way to create a tool is by using the `@tool` decorator. This decorator automatically parses your function's signature, type hints, and docstring to generate a schema that the LLM can understand.

For the best results, **always provide clear type hints and a descriptive docstring**. The LLM relies heavily on these to know when and how to invoke your tool.

```python linenums="1"
from autogen.beta import tool

@tool
def calculate_shipping_cost(destination: str, weight_kg: float) -> str:
    """Calculates the shipping cost for a package based
    on its destination and weight.
    """
    return "$15.00"
```

Once defined, you can equip an agent with this capability by passing the tool to the `tools` list during the agent's initialization.

```python
from autogen.beta import Agent

agent = Agent(name="ShippingAssistant", tools=[calculate_shipping_cost])
```

!!! note
    For simpler use cases, you can pass an undecorated Python function directly to the agent's `tools` list. The framework will automatically convert it into a fully-fledged tool under the hood, extracting the schema from the signature and docstring just like the decorator would.

    ```python linenums="1"
    from autogen.beta import Agent

    def get_weather(location: str) -> str:
        """Returns the current weather for a given location."""
        return "Sunny, 22°C"

    # get_weather is automatically converted to a tool
    agent = Agent(name="WeatherBot", tools=[get_weather])
    ```

### Registering Tools via a Decorator

Alternatively, you can register a tool directly with an agent instance using its `@my_agent.tool` decorator.

This approach is particularly useful when you need to dynamically add capabilities to an agent after it has been created, or when you are logically organizing your code by attaching specific tools to specific agent instances.

```python linenums="1"
from autogen.beta import Agent

agent = Agent(name="CalculatorBot")

@agent.tool
def multiply(a: int, b: int) -> int:
    """Multiplies two integers and returns the result."""
    return a * b
```

### Tool middleware

To run async hooks around **one** tool (e.g. argument normalization, result redaction, bundled auditing), pass `middleware=[...]` with `ToolMiddleware` callables. See the dedicated [Tool middleware](/docs/beta/tools/tool_middleware) page.

## Synchronous and Asynchronous Tools

Agent interactions are naturally asynchronous. To support this, tools are executed within an asynchronous event loop by default. However, you have the flexibility to define your tool functions as either synchronous (`def`) or asynchronous (`async def`).

To ensure that heavy computational tasks or blocking I/O operations do not freeze the entire application, **synchronous tools are automatically executed in a separate thread** by default.

```python linenums="1"
# This synchronous tool runs in a separate thread to prevent blocking
@tool
def fetch_data_sync(url: str) -> str:
    """Fetches data from a URL using a blocking request library."""
    import requests
    return requests.get(url).text


# This native asynchronous tool runs directly in the main event loop
@tool
async def fetch_data_async(url: str) -> str:
    """Fetches data from a URL using an async request library."""
    import aiohttp
    async with aiohttp.ClientSession() as session:
        async with session.get(url) as response:
            return await response.text()
```

### Disabling Threaded Execution

If you have a synchronous function that executes very quickly (e.g., simple string manipulation or math) and you want to avoid the minor overhead of thread creation, you can disable threaded execution by passing `sync_to_thread=False` to the `@tool` decorator.

```python linenums="1"
# This tool runs synchronously in the main event loop,
# without a separate thread
@tool(sync_to_thread=False)
def format_name(first_name: str, last_name: str) -> str:
    """Formats a full name."""
    return f"{last_name.upper()}, {first_name.capitalize()}"
```

!!! warning
    When `sync_to_thread=False` is set, the synchronous tool runs directly within the asynchronous context. If the function performs time-consuming operations (like network requests or large loops), **it will block the entire event loop** until it finishes, preventing other agents or asynchronous tasks from making progress.

## Customizing Tool Schemas

LLMs perform best when they have precise constraints and detailed instructions. The framework automatically generates a tool's schema from its function signature and docstring. For more granular control, you can define minimum/maximum values, enforce specific formats, or override the tool's name.

You can override the basic properties directly in the decorator:

```python linenums="1" hl_lines="2 3"
@tool(
    name="custom_math_tool",
    description="Performs advanced mathematical operations.",
)
def math_op(a: int, b: int) -> int:
    return a + b
```

!!! note
    Explicitly setting the name and description overrides the automatically generated values.

### Deep Schema Customization with Pydantic

Under the hood, arguments are serialized and validated using [Pydantic](https://docs.pydantic.dev/latest/){.external-link target="_blank"} schemas. This means you can use standard `pydantic.Field` annotations to deeply customize individual schema parameters, providing the LLM with strict guidelines on what values are acceptable.

```python linenums="1" hl_lines="8-13 17-21"
from typing import Annotated
from pydantic import Field

@tool
def set_temperature(
    temp: Annotated[
        int,
        Field(
            ...,
            description="The target temperature.",
            ge=10,
            le=30,
        ),
    ],
    mode: Annotated[
        str,
        Field(
            ...,
            description="The thermostat mode.",
            pattern="^(heat|cool|auto)$",
        ),
    ]
) -> str:
    """Sets the thermostat to a specific temperature and mode."""
    return f"Set to {temp}°C in {mode} mode."
```

### Complete Custom Schema Example

A complete example combining custom tool properties and strict parameter validation looks like this:

```python linenums="1" hl_lines="6 7 12-17 21-25"
from typing import Annotated
from pydantic import Field
from autogen.beta import tool

@tool(
    name="create_user_profile",
    description="Creates a new user profile in the database.",
)
def create_profile(
    username: Annotated[
        str,
        Field(
            ...,
            description="The chosen username. Must be alphanumeric.",
            min_length=3,
            max_length=20,
        )
    ],
    age: Annotated[
        int,
        Field(
            ...,
            description="The user's age. Must be 18 or older.",
            ge=18,
        ),
    ],
) -> str:
    return f"Profile for {username} created."
```

This configuration generates the following detailed JSON schema, ensuring the LLM understands exactly what inputs are required and valid:

```json linenums="1"
{
    "description": "Creates a new user profile in the database.",
    "name": "create_user_profile",
    "parameters": {
        "properties": {
            "username": {
                "description": "The chosen username. Must be alphanumeric.",
                "maxLength": 20,
                "minLength": 3,
                "title": "Username",
                "type": "string"
            },
            "age": {
                "description": "The user's age. Must be 18 or older.",
                "minimum": 18,
                "title": "Age",
                "type": "integer"
            }
        },
        "required": [
            "username",
            "age"
        ],
        "type": "object"
    }
}
```

## Execution Context

Tools often need access to the broader execution context, such as injected dependencies, variables, or mechanisms for human-in-the-loop interactions. The AG2 framework supports these features natively.

!!! note
    Under the hood, these contextual capabilities are powered by the [FastDepends](https://github.com/Lancetnik/FastDepends){.external-link target="_blank"} library, ensuring robust and FastAPI-like dependency management.

To access the execution context from within your tool, you can simply type-hint an argument with the `Context` object.

```python linenums="1"
from autogen.beta import Context, tool

@tool
async def my_tool(context: Context) -> str:
    # Access context variables or dependencies here
    return f"Execution context: {context}"
```

For more detailed information on specific context features, see [Dependency Injection](/docs/beta/context/inject/), [Context Variables](/docs/beta/context/variables/), [Depends](/docs/beta/depends/), [Human-in-the-loop](/docs/beta/context/human_in_the_loop/).

## Returning Rich Tool Results

By default, returning a plain `str` from a tool is the simplest option — the framework wraps it in a `TextInput` automatically. When you need more control over the returned content, you can use typed `Input` classes or compose multiple outputs with `ToolResult`.

### Input Types

The framework provides typed input classes for text, structured data, images, audio, video, documents, and raw binary payloads. See [Multimodal Inputs](/docs/beta/inputs/inputs) for the full reference including provider support and factory variants.

Return any input type directly from a tool function just like you would a string:

```python linenums="1"
from autogen.beta import DataInput, TextInput, tool

@tool
def get_status(task_id: str) -> TextInput:
    """Returns a human-readable status update."""
    return TextInput(f"Task {task_id} is in progress.")

@tool
def get_user_profile(user_id: str) -> DataInput:
    """Returns a structured user profile."""
    return DataInput({"id": user_id, "name": "Alice", "role": "admin"})
```

### Returning Images and Binary Data

Use `ImageInput` when a tool needs to hand an image back to the model for further reasoning. The factory accepts a URL, a local file path, a pre-uploaded file ID, or raw bytes:

```python linenums="1"
from autogen.beta import ImageInput, ToolResult, tool

@tool
def fetch_chart(chart_id: str) -> ImageInput:
    """Fetches a chart image by ID and returns it for visual analysis."""
    url = f"https://charts.example.com/{chart_id}.png"
    return ImageInput(url)

@tool
def capture_screenshot(page: str) -> ImageInput:
    """Takes a screenshot of a page and returns it."""
    import subprocess
    raw = subprocess.check_output(["screenshot-cli", page])
    return ImageInput(data=raw, media_type="image/png")
```

When you have raw bytes of an arbitrary format, use `BinaryInput` directly and set the media type explicitly:

```python linenums="1"
from autogen.beta import BinaryInput, ToolResult, tool

@tool
def export_pdf(report_id: str) -> ToolResult:
    """Exports a report as a PDF and returns it alongside a summary."""
    pdf_bytes = _render_pdf(report_id)
    return ToolResult(
        f"Report {report_id} exported successfully.",
        BinaryInput(pdf_bytes, media_type="application/pdf"),
    )
```

!!! note
    Not all LLM providers support every media type. Check your provider's documentation for the list of accepted MIME types and file formats. The framework passes the binary payload through as-is; format validation is the provider's responsibility.

### Returning Multiple Inputs

Use `ToolResult` to combine multiple inputs into a single tool response. Each positional argument becomes a separate part the model receives:

```python linenums="1"
from autogen.beta import ImageInput, ToolResult, tool

@tool
def analyze_product(product_id: str) -> ToolResult:
    """Returns a product image alongside its structured metadata."""
    return ToolResult(
        ImageInput(f"https://cdn.example.com/products/{product_id}.jpg"),
        {"id": product_id, "name": "Widget Pro", "stock": 42},
    )
```

## Returning a Final Tool Result

By default, a tool result is sent back to the model so the agent can decide what to say next.
When the tool itself already knows the exact final answer, you can return `ToolResult(..., final=True)` to end the turn immediately without another model round-trip.

`ToolResult` accepts any `str` or `Input` as its first positional argument:

```python linenums="1"
from autogen.beta import Agent, DataInput, TextInput, ToolResult, tool

@tool
def handoff_to_human(ticket_id: str) -> ToolResult:
    """Escalates a request and returns the final user-facing message."""
    return ToolResult(
        f"Ticket {ticket_id} was escalated to a human agent.",
        final=True,
    )

@tool
def get_exchange_rate(currency: str) -> ToolResult:
    """Returns the current exchange rate as structured data."""
    return ToolResult(
        DataInput({"currency": currency, "rate": 1.23, "base": "USD"}),
        final=True,
    )


agent = Agent(name="SupportBot", tools=[handoff_to_human])

reply = await agent.ask("I need help with my ticket #123456.")
print(reply.body)
# Output: "Ticket 123456 was escalated to a human agent."
```

!!! note
    A `ToolResult` with `final=True` must contain **exactly one** part — either a `TextInput` or a `DataInput`. The content is returned as-is: `TextInput` produces the text directly, `DataInput` is JSON-serialized.

This is especially useful for tools that:

- perform an authoritative action and already know the exact reply
- return a message that should not be paraphrased by the model
- want to skip an extra LLM call for latency or cost reasons
