Description
Executing a tool whose _run (or wrapped func) returns a coroutine raises RuntimeError: asyncio.run() cannot be called from a running event loop whenever the call happens inside an already-running event loop — e.g. a FastAPI request handler, a Jupyter cell, or any async def. Synchronous scripts are unaffected, which is why this slips through basic testing but breaks every async-server deployment.
Location
lib/crewai/src/crewai/tools/base_tool.py:342 — BaseTool.run does result = asyncio.run(result)
lib/crewai/src/crewai/tools/base_tool.py:553 — Tool.run (the @tool decorator class), same pattern
lib/crewai/src/crewai/tools/structured_tool.py:441 and :446 — CrewStructuredTool.invoke, same pattern
result = self._run(*args, **kwargs)
if asyncio.iscoroutine(result):
result = asyncio.run(result) # <-- raises under a running loop
In-repo precedent (this is a known pattern here)
The codebase already solves exactly this elsewhere and the tool paths simply don't use it:
lib/crewai/src/crewai/tasks/llm_guardrail.py:24 — _run_coroutine_sync: try: asyncio.get_running_loop() and, if a loop is running, run the coroutine in a ThreadPoolExecutor(max_workers=1) via pool.submit(ctx.run, asyncio.run, coro).result(), else asyncio.run(coro).
- Same guard in
tools/mcp_native_tool.py:86, a2a/utils/agent_card.py, a2a/utils/delegation.py, mcp/tool_resolver.py, project/wrappers.py, project/annotations.py.
Reproduction
import asyncio
from crewai.tools import tool
@tool("echo")
async def echo(value: str) -> str:
"""Echo the value."""
await asyncio.sleep(0.01)
return f"echo: {value}"
async def handler(): # simulates a FastAPI route / Jupyter cell
return echo.run(value="hi")
asyncio.run(handler())
# RuntimeError: asyncio.run() cannot be called from a running event loop
The same failure occurs with a BaseTool subclass whose _run returns a coroutine and with CrewStructuredTool.from_function(func=<async fn>).invoke(...).
Expected
run() / invoke() return the coroutine's result regardless of whether an event loop is already running, consistent with llm_guardrail._run_coroutine_sync.
Environment
crewai main (v1.15.15 snapshot), Python 3.13.
Description
Executing a tool whose
_run(or wrappedfunc) returns a coroutine raisesRuntimeError: asyncio.run() cannot be called from a running event loopwhenever the call happens inside an already-running event loop — e.g. a FastAPI request handler, a Jupyter cell, or anyasync def. Synchronous scripts are unaffected, which is why this slips through basic testing but breaks every async-server deployment.Location
lib/crewai/src/crewai/tools/base_tool.py:342—BaseTool.rundoesresult = asyncio.run(result)lib/crewai/src/crewai/tools/base_tool.py:553—Tool.run(the@tooldecorator class), same patternlib/crewai/src/crewai/tools/structured_tool.py:441and:446—CrewStructuredTool.invoke, same patternIn-repo precedent (this is a known pattern here)
The codebase already solves exactly this elsewhere and the tool paths simply don't use it:
lib/crewai/src/crewai/tasks/llm_guardrail.py:24—_run_coroutine_sync:try: asyncio.get_running_loop()and, if a loop is running, run the coroutine in aThreadPoolExecutor(max_workers=1)viapool.submit(ctx.run, asyncio.run, coro).result(), elseasyncio.run(coro).tools/mcp_native_tool.py:86,a2a/utils/agent_card.py,a2a/utils/delegation.py,mcp/tool_resolver.py,project/wrappers.py,project/annotations.py.Reproduction
The same failure occurs with a
BaseToolsubclass whose_runreturns a coroutine and withCrewStructuredTool.from_function(func=<async fn>).invoke(...).Expected
run()/invoke()return the coroutine's result regardless of whether an event loop is already running, consistent withllm_guardrail._run_coroutine_sync.Environment
crewai
main(v1.15.15 snapshot), Python 3.13.