Skip to Content

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 agentIn the trace
Your function around crew.kickoff()The root AGENT span, from your observe wrapper
A model callAn LLM generation with the model, and tokens and cost when the call reports usage
A toolA TOOL span, from your observe wrapper on the tool body
Crews and tasksNo 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

  1. Install the SDK with the OpenAI instrumentation next to CrewAI:
pip install "glassflow-rius[openai]" crewai
  1. Wrap each tool body with observe and the TOOL kind, and put @tool outermost. 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())
  1. 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.

  2. Set CREWAI_TRACING_ENABLED=false where no terminal is attached, such as CI or containers. CrewAI asks about its own tracing product interactively on the first run.

  3. 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.

  4. 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 answers

On 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 observe wrapper on your tools.

Still stuck? Get help

Last updated on