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 crewaiUsage
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()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, plustrain/test/replay. Flows:flow.kickoff/kickoff_async/akickoff. - Tools: both
@toolfunction tools andBaseToolsubclasses are auto-traced (across crewai 0.130.x through 1.15.x). Leave them alone — you do not add@neatlogs.spanor a manualtrace()to a tool. The one exception: a retrieval/embedding tool can addwith neatlogs.trace(kind="RETRIEVER")inside its body to attachneatlogs.retrieval.*(query + documents) for the dashboard. - CrewAI's own built-in telemetry (the no-I/O
Crew Created/Task Createdlifecycle 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 agentneatlogs.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:
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.
