# Sync tool hooks now run the tool in async agents

> Agno now awaits the result of a sync tool hook in the async tool path, so a hook that returns function_call(**arguments) runs the tool body under arun() and returns its real output.

- Published: 2026-09-23
- Author: Kaustubh Shukla
- Categories: Changelog
- Canonical: https://www.agno.com/articles/sync-tool-hooks-now-run-the-tool-in-async-agents
- Markdown: https://www.agno.com/articles/sync-tool-hooks-now-run-the-tool-in-async-agents.md

Under `arun()`, Agno's `FunctionCall.aexecute()` builds the tool hook chain from async wrappers, so the `function_call` a hook receives returns a coroutine. A sync hook written as `return function_call(**arguments)` can't await it. The hook returned the unawaited coroutine, and Agno treated that as the tool result.

The tool body never ran. The model got `<coroutine object ...>` as the tool output, and any exception the tool would have raised disappeared. The same thing happened to a sync tool as soon as any hook in the stack was async, since that also sends the call through `aexecute()`.

This script runs one hook through the async path, with no model:

```python
import asyncio

from agno.tools.function import Function, FunctionCall

calls = []

async def get_weather(city: str) -> str:
    calls.append(city)
    return f"Sunny in {city}"

def logging_hook(function_name, func, args):
    print(f"[logging_hook] Calling {function_name} with {args}")
    return func(**args)

fc = FunctionCall(function=Function.from_callable(get_weather), arguments={"city": "Tokyo"})
fc.function.tool_hooks = [logging_hook]

async def main():
    result = await fc.aexecute()
    print("result:", result.result)
    print("tool body ran:", bool(calls))

asyncio.run(main())
```

```text
Before (real output)
[logging_hook] Calling get_weather with {'city': 'Tokyo'}
result: <coroutine object FunctionCall._build_nested_execution_chain_async.<locals>.create_hook_wrapper.<locals>.wrapper.<locals>.next_func at 0x107e6ac50>
tool body ran: False

Now (real output)
[logging_hook] Calling get_weather with {'city': 'Tokyo'}
result: Sunny in Tokyo
tool body ran: True
```

Agno now awaits any awaitable a hook returns, at every level of the async chain. Async generators and plain short-circuit values pass through untouched, and the sync chain is unchanged.

See the [cookbook](https://github.com/agno-agi/agno/blob/main/cookbook/02_agents/09_hooks/tool_hooks.py), and learn more about [tool hooks](https://docs.agno.com/tools/hooks) in the documentation.
