How do I see every tool call in an agent built with CrewAI?
Rius records every model call in a CrewAI agent on its own, and you see each tool call as a TOOL span once you wrap the tool body with observe. Crews and tasks produce no spans of their own, so the entrypoint and the tools are the two places you add code. Open the trace in the console and every tool call sits in the tree between the model calls that chose it and used its result.
How it works
CrewAI 1.x drives the OpenAI client, so the openai integration of the Rius SDK captures the model calls without any code from you. If LiteLLM is installed, CrewAI uses it instead, and you use the LiteLLM integration. Both are listed on the Integrations page, and the CrewAI integration page covers the details.
The crew itself runs inside the framework, where auto-instrumentation cannot see it. That is why the split looks like this:
| In your CrewAI agent | In the trace |
|---|---|
Your function around crew.kickoff() | The root AGENT span, from your observe wrapper |
| A model call | An LLM generation with the model, and tokens and cost when the call reports usage |
| A tool | A TOOL span, from your observe wrapper on the tool body |
| Crews and tasks | No spans of their own |
Without the observe wrapper on a tool, the tool call has no span. You still see the model call where the agent decides to call it and the one that follows, but not the tool in between. For the broader picture of watching agents in production, see Monitor AI agents in production.
Set it up
- Install the SDK with the OpenAI instrumentation next to CrewAI:
pip install "glassflow-rius[openai]" crewai- Wrap each tool body with
observeand the TOOL kind, and put@tooloutermost. The order matters. Wrap your entrypoint as an AGENT span, so the tool spans parent under the run through ambient context:
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())-
Name your tool functions clearly. The tool span carries the name you give it, while the generation spans are named
ChatCompletion, so the tool names are what tell the steps apart. -
Set
CREWAI_TRACING_ENABLED=falsewhere no terminal is attached, such as CI or containers. CrewAI asks about its own tracing product interactively on the first run. -
If one conversation or job spans several kickoffs, scope each kickoff with
rius.session()and your own id, so the runs group as one session. -
Run one ticket that calls a tool, then open the trace in the console.
What you get
A run that calls one tool reads like this in the trace tree:
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 answersOn the Traces page you can find the tool calls in several ways:
- Waterfall view. Open the trace to see the tool spans in order, between the model calls before and after them. A failed tool span is highlighted along the path from the root.
- Spans tab. Filter by the Tool kind to list every tool call across all runs, then sort by duration or open one in its trace.
- Retry chains. Back-to-back calls of the same tool collapse into one row such as
account_status · 3 attempts · recovered, and you can expand it for each attempt. - Model calls around the tool. Each generation shows its model, and tokens and cost when the call reported usage. A streamed call carries token counts only when usage is enabled on the underlying OpenAI client. A call with no usage shows a blank cost rather than $0.
Costs are list-price estimates in US dollars, not billed amounts.
When something else fits
- If you need to know which task a model call belongs to, wrap per-task entrypoints with
observe, because crews and tasks produce no spans on their own. - If your CrewAI setup uses LiteLLM, follow the LiteLLM integration for the model calls and keep the same
observewrapper on your tools.