Skip to Content

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() with observe and kind=SpanKind.AGENT. This is the root span.
  • The tools. Put observe with kind=SpanKind.TOOL on 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, observe uses the function’s name as the tool name. An unnamed TOOL span is called execute_tool followed by that name, for example execute_tool account_status. Pass tool_name when the span’s tool name should differ from the function name. See Tracing your code for the full set of observe options.

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

  1. Install the SDK with the openai extra next to CrewAI. The full SDK setup is on the CrewAI integration page.
pip install "glassflow-rius[openai]" crewai
  1. Initialize Rius, then wrap the tool body and the entrypoint. Keep the decorator order: @tool outermost, observe on 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())
  1. Repeat the observe wrapper with kind=SpanKind.TOOL on every tool you want traced. A tool without it produces no span.
  2. Run one ticket that calls a tool and open the trace in the console. You should see the root AGENT span, the LLM generations and your named TOOL spans.
  3. Set CREWAI_TRACING_ENABLED=false where 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 litellm integration for those. The tool wrapper stays the same.
  • For other frameworks and how each one’s tools appear, see Integrations.

Still stuck? Get help

Last updated on