CrewAI

Trace a CrewAI crew with Neatlogs (Python).

Wrap your crew with neatlogs.wrap(crew) and the whole run is traced — agents, tasks, tool calls, and their LLM calls all appear in the span tree. One call, no per-call decorators.

Prerequisites

pip install -U neatlogs crewai

Usage

import os
import neatlogs

neatlogs.init(
    api_key=os.environ["NEATLOGS_API_KEY"],
    workflow_name="crewai-demo",
)

from crewai import Agent, Task, Crew

crew = neatlogs.wrap(Crew(agents=[...], tasks=[...]))
result = crew.kickoff()
print(result)

neatlogs.flush()
neatlogs.shutdown()
Note

Do not pass instrumentations=[...] for CrewAI. wrap() patches LLM.call directly, so the LLM span is captured regardless of the model backend (OpenAI, Azure, Gemini, Anthropic, local) — you don't need to match a provider to the model string. Pairing a provider instrumentor would double-fire the LLM span.

What wrap() covers

wrap() patches every run entrypoint and installs class-level hooks, so the full span tree is captured:

WORKFLOW  crew.kickoff()
  ↳ AGENT   each agent's task execution
  ↳ TOOL    each tool call (BaseTool.run or CrewStructuredTool.invoke)
  ↳ LLM     LLM.call (the underlying model request)
  • Entrypoints: kickoff / kickoff_async / akickoff / kickoff_for_each / kickoff_for_each_async / akickoff_for_each, plus train / test / replay. Flows: flow.kickoff / kickoff_async / akickoff.
  • Tools: both @tool function tools and BaseTool subclasses are auto-traced (across crewai 0.130.x through 1.15.x). Leave them alone — you do not add @neatlogs.span or a manual trace() to a tool. The one exception: a retrieval/embedding tool can add with neatlogs.trace(kind="RETRIEVER") inside its body to attach neatlogs.retrieval.* (query + documents) for the dashboard.
  • CrewAI's own built-in telemetry (the no-I/O Crew Created / Task Created lifecycle spans) is auto-suppressed so it doesn't pollute your traces.

Standalone agents (no Crew)

wrap() also handles a single agent run — agent.kickoff(messages=...) with no Crew. Wrap the agent before kicking it off:

agent = neatlogs.wrap(Agent(role="...", goal="...", backstory="...", tools=[...]))
result = agent.kickoff(messages="What is 2 + 2?")

This emits an AGENT span (crewai.agent.<role>) capturing the messages input, with tool/LLM calls nested under it.

Prompt templates

To attach prompt templates to CrewAI work (the framework owns the LLM call, so you can't wrap it with trace()), two helpers complement wrap():

  • neatlogs.bind_templates(llm, system_tpl) — attach a system prompt template to an LLM before passing it to an agent
  • neatlogs.register_crewai_task(task, user_tpl) — associate a user prompt template with a task after creating it

See the CrewAI Multi-Agent Crew guide for an end-to-end example.

Sessions & end-user

Say you kick off a crew several times over one conversation and want all of those runs tracked as a single session, attributed to the user they belong to — Neatlogs handles that. Since neatlogs.wrap(crew) already opens the WORKFLOW root, wrap the kickoff() in identify() and it picks up the session and end-user from the surrounding block:

# Same session_id every turn → one session; end_user_id attributes it to your user.
with neatlogs.identify(session_id=f"conv_{conversation_id}", end_user_id=user_id):
    result = crew.kickoff()

Not every run is a conversation. A standalone workflow — a one-off crew run, not a back-and-forth — is a single trace with no turns to group. You still attribute it to the customer it ran for, sourcing the id and any metadata from your own user or request object:

Warning

A standalone workflow must not carry a session_id. Omit it entirely — Neatlogs sets session_id = trace_id, so the run is its own single-turn session, fully attributed to the end-user. Reusing one session_id across unrelated runs wrongly folds them into a single multi-turn conversation.

def run_crew(inputs, user):
    # Standalone workflow — attribute to the end-user; do NOT pass session_id.
    with neatlogs.identify(
        end_user_id=str(user.id),
        end_user_metadata={"plan": user.plan},
    ):
        return crew.kickoff(inputs=inputs)

See Sessions and End-User Identity for the full model.

On this page

Ask Neatlogs AI

Answers from the docs

How can I help?

Ask anything about instrumenting, tracing, or the Neatlogs dashboard.