How do I trace tool calls in CrewAI?
Rius traces CrewAI tool calls as TOOL spans when you wrap each tool body with observe, and every tool span nests under the root span of your run. CrewAI’s model calls arrive as generations with tokens and cost through the openai integration, so one trace shows each model call and each tool call in order. Crews and tasks produce no spans of their own, so the tool wrapper is what makes tools visible.
How it works
Rius captures CrewAI’s model calls through the openai integration, because CrewAI 1.x drives the OpenAI client natively. If LiteLLM is installed, CrewAI uses it as a fallback, and you use the litellm integration instead. Tool execution runs inside the framework, where auto-instrumentation cannot see it, so you mark it yourself.
- The run. Wrap the function that calls
crew.kickoff()withobserveandkind=SpanKind.AGENT. This is the root span. - The tools. Put
observewithkind=SpanKind.TOOLon the tool function body. Ambient context parents each TOOL span under the run, so you pass nothing between them. - The model calls. Each one is a generation span with model, tokens, cost and time to first token, nested under the same root.
- The tool name. Without
tool_name,observeuses the function’s name as the tool name. An unnamed TOOL span is calledexecute_toolfollowed by that name, for exampleexecute_tool account_status. Passtool_namewhen the span’s tool name should differ from the function name. See Tracing your code for the full set ofobserveoptions.
Crews and tasks add no spans, so a trace does not show which task a generation or tool call belongs to. If that matters, wrap per-task entrypoints with observe as well.
Set it up
- Install the SDK with the
openaiextra next to CrewAI. The full SDK setup is on the CrewAI integration page.
pip install "glassflow-rius[openai]" crewai- Initialize Rius, then wrap the tool body and the entrypoint. Keep the decorator order:
@tooloutermost,observeon the function body.
import rius
from crewai import Agent, Crew, Task
from crewai.tools import tool
from rius import SpanKind
rius.init(api_key="ri_...", service_name="my-agent")
@tool("account_status")
@rius.observe(kind=SpanKind.TOOL)
def account_status(email: str) -> str:
"""Fetch a customer's account standing by email."""
return lookup(email)
support_agent = Agent(
role="Support specialist",
goal="Answer support tickets accurately and concisely.",
backstory="You resolve product and account questions.",
tools=[account_status],
llm="gpt-5-mini",
)
@rius.observe(name="handle-ticket", kind=SpanKind.AGENT)
def handle_ticket(ticket: str) -> str:
task = Task(
description=f"Resolve this support ticket: {ticket}",
expected_output="A short, direct answer.",
agent=support_agent,
)
return str(Crew(agents=[support_agent], tasks=[task]).kickoff())- Repeat the
observewrapper withkind=SpanKind.TOOLon every tool you want traced. A tool without it produces no span. - Run one ticket that calls a tool and open the trace in the console. You should see the root
AGENTspan, theLLMgenerations and your namedTOOLspans. - Set
CREWAI_TRACING_ENABLED=falsewhere no terminal is attached, such as CI or containers. CrewAI otherwise asks about its own tracing product on first run.
What you get
A run that calls one tool looks like this:
handle-ticket AGENT 8.4s
├─ ChatCompletion LLM 3.9s agent decides to call the tool
├─ execute_tool account_status TOOL 0ms
└─ ChatCompletion LLM 4.3s agent answers- Each tool call has its own span with its duration, its input and output, and an error status if the tool raises. Exceptions are recorded on the span and re-raised.
- Tool spans sit between the generations that decide to call them and the generations that use the result, so you can read the agent’s loop top to bottom.
- Model calls carry tokens and cost, which Rius computes from the model and token counts. You never send a cost value.
- Generation spans are named
ChatCompletion, since CrewAI’s model calls share one generic name. - To group the turns of one conversation or job, scope each kickoff with
rius.session(), as described under sessions in Tracing your code.
When something else fits
- If you need to see which task a call belongs to, wrap per-task entrypoints with
observe, because CrewAI’s crew and task structure adds no spans. - If LiteLLM is installed, CrewAI uses it for model calls, so follow the
litellmintegration for those. The tool wrapper stays the same. - For other frameworks and how each one’s tools appear, see Integrations.