Tool Hooks

Use pre and post hooks to modify tool behavior.

You can use tool hooks to perform validation, logging, or any other logic before or after a tool is called.

Install dependencies: uv pip install agno openai httpx

Set your OpenAI API key:

export OPENAI_API_KEY="your-api-key"

A tool hook receives the function name, wrapped function call, and arguments. It can also access the Agent, Team, or RunContext. Call the wrapped function with the arguments to continue the chain. Raise an exception or return another result to stop or replace the call.

Use a supported parameter name when defining a tool hook: agent, team, run_context; name or function_name; function, func, or function_call; and args or arguments.

The fragments in this section reuse these imports:

import json
import time
from typing import Any, Callable, Dict

import httpx
from agno.agent import Agent
from agno.models.openai import OpenAIResponses
from agno.tools import tool
from agno.tools.hackernews import HackerNewsTools
from agno.utils.log import logger

For example:

def logger_hook(
    function_name: str, function_call: Callable, arguments: Dict[str, Any]
):
    """Log the duration of the function call"""
    start_time = time.time()

    # Call the function
    result = function_call(**arguments)

    end_time = time.time()
    duration = end_time - start_time

    logger.info(f"Function {function_name} took {duration:.2f} seconds to execute")

    # Return the result
    return result

or

def confirmation_hook(
    function_name: str, function_call: Callable, arguments: Dict[str, Any]
):
    """Confirm the function call"""
    if function_name != "get_top_hackernews_stories":
        raise ValueError("This tool is not allowed to be called")
    return function_call(**arguments)

You can assign tool hooks on agents and teams. The tool hooks will be applied to all tool calls made by the agent or team.

For example:

agent = Agent(
    model=OpenAIResponses(id="gpt-5.2"),
    tools=[HackerNewsTools()],
    tool_hooks=[logger_hook],
)

You can also get access to the RunContext object in the tool hook. Inside the run context, you will find the session state, dependencies, and metadata.

from agno.run import RunContext

def grab_customer_profile_hook(
    run_context: RunContext, function_name: str, function_call: Callable, arguments: Dict[str, Any]
):
    if not run_context.session_state:
        run_context.session_state = {}

    cust_id = arguments.get("customer")
    profiles = run_context.session_state.get("customer_profiles", {})
    if cust_id not in profiles:
        raise ValueError(f"Customer profile for {cust_id} not found")
    customer_profile = profiles[cust_id]

    # Replace the customer with the customer_profile for the function call
    arguments["customer"] = json.dumps(customer_profile)
    # Call the function with the updated arguments
    result = function_call(**arguments)

    return result

Multiple Tool Hooks

You can also assign multiple tool hooks at once. They will be applied in the order they are assigned.

agent = Agent(
    model=OpenAIResponses(id="gpt-5.2"),
    tools=[HackerNewsTools()],
    tool_hooks=[logger_hook, confirmation_hook],  # The logger_hook will run on the outer layer, and the confirmation_hook will run on the inner layer
)

You can also assign tool hooks to specific custom tools.

@tool(tool_hooks=[logger_hook, confirmation_hook])
def get_top_hackernews_stories(num_stories: int) -> str:
    """Fetch top stories from Hacker News.

    Args:
        num_stories (int): Number of stories to retrieve
    """
    # Fetch top story IDs
    response = httpx.get("https://hacker-news.firebaseio.com/v0/topstories.json")
    story_ids = response.json()

    # Fetch story details
    final_stories = []
    for story_id in story_ids[:num_stories]:
        story_response = httpx.get(
            f"https://hacker-news.firebaseio.com/v0/item/{story_id}.json"
        )
        story = story_response.json()
        if "text" in story:
            story.pop("text", None)
        final_stories.append(story)

    return json.dumps(final_stories)

agent = Agent(
    model=OpenAIResponses(id="gpt-5.2"),
    tools=[get_top_hackernews_stories],
)

Pre and Post Hooks

Pre and post hooks let you observe a tool call through its FunctionCall. Ordinary exceptions in these callbacks are logged and execution continues; they are not equivalent to a wrapping tool_hooks gate. To abort execution deliberately, use a supported run-control exception such as StopAgentRun from agno.exceptions (see Tool Exceptions).

Set the pre_hook in the @tool decorator to run a function before the tool call.

Set the post_hook in the @tool decorator to run a function after the tool call.

Here's an example that uses a pre_hook and post_hook along with agent dependencies.

pre_and_post_hooks.py
import json
from typing import Iterator

import httpx
from agno.agent import Agent
from agno.tools import FunctionCall, tool


def pre_hook(fc: FunctionCall):
    print(f"Pre-hook: {fc.function.name}")
    print(f"Arguments: {fc.arguments}")
    print(f"Result: {fc.result}")


def post_hook(fc: FunctionCall):
    print(f"Post-hook: {fc.function.name}")
    print(f"Arguments: {fc.arguments}")
    print(f"Result: {fc.result}")


@tool(pre_hook=pre_hook, post_hook=post_hook)
def get_top_hackernews_stories(agent: Agent) -> Iterator[str]:
    num_stories = agent.dependencies.get("num_stories", 5) if agent.dependencies else 5

    # Fetch top story IDs
    response = httpx.get("https://hacker-news.firebaseio.com/v0/topstories.json")
    story_ids = response.json()

    # Yield story details
    for story_id in story_ids[:num_stories]:
        story_response = httpx.get(
            f"https://hacker-news.firebaseio.com/v0/item/{story_id}.json"
        )
        story = story_response.json()
        if "text" in story:
            story.pop("text", None)
        yield json.dumps(story)


agent = Agent(
    dependencies={
        "num_stories": 2,
    },
    tools=[get_top_hackernews_stories],
    markdown=True,
)
agent.print_response("What are the top hackernews stories?", stream=True)

The last example yields a generator. Its post-hook can see the generator before its values are consumed; it is not an after-stream-completion callback. The tool reads the Agent's static dependency configuration. Use injected run_context.dependencies when the values come from the current run.