How do I trace a CrewAI agent deployed to Vercel functions?
Rius traces a CrewAI agent on Vercel functions when you call rius.init once at module level, wrap the entry point with observe, and flush spans before each invocation ends. Model calls arrive as generations with tokens and cost. The service name you pass to init is the agent’s name in Rius. The three steps below cover init placement, flushing and naming. The CrewAI integration page has the full framework recipe.
How it works
- Init runs once per process. Put
rius.initat module level, outside your handler. A serverless runtime freezes the process between invocations, so the same initialized client serves every invocation that reuses the process. - Spans export in the background. The SDK queues spans and sends them from a background thread. A frozen process exports nothing, and the exit hook may never run. This is why a serverless function calls
client.flush()at the end of every invocation. - Turn heartbeats off. Heartbeats are a signal for long-lived processes. Between invocations the pings stop and the instance flaps between alive and
staleorgone. Setheartbeat=Falsefor serverless runtimes. The agent then shows as no heartbeat and lists no instances, but its traces arrive as normal. - Model calls come from the OpenAI instrumentation. CrewAI drives the OpenAI client, so the
openaiintegration records each model call. Crews and tasks produce no spans of their own, so yourobservewrappers give the run its shape.
Set it up
-
Add the packages to your function’s dependencies. Rius never installs the libraries it instruments, so CrewAI comes separately.
pip install "glassflow-rius[openai]" crewaiSet
CREWAI_TRACING_ENABLED=falsein the function’s environment variables. CrewAI asks about its own tracing on the first run, and no terminal is attached to answer. -
Initialize at module level, wrap the entry point, and flush in a
finallyblock so the flush runs even when the crew raises.import rius from crewai import Agent, Crew, Task from rius import SpanKind client = rius.init( api_key="ri_...", service_name="support-crew", heartbeat=False, ) support_agent = Agent( role="Support specialist", goal="Answer support tickets accurately and concisely.", backstory="You resolve product and account questions.", 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()) def run(ticket: str) -> str: try: return handle_ticket(ticket) finally: client.flush(timeout_millis=30_000)Call
runfrom your function’s entry point. Flush once, after the answer is ready, never inside the crew. Do not callclient.shutdown()per invocation. It releases the init slot, so reserve it for a real process stop. -
Name the agent. The
service_nameyou pass to init is the name the Agents page groups your traces under.RIUS_AGENT_NAMEoverrides it when you want a different grouping name, andRIUS_MAIN_AGENT_VERSIONsets the version the Agents page compares across deploys. Give theobservewrapper a name too, such ashandle-ticket. That name labels the root span of each run. -
Name each tool. If a crew tool is your own function, put
@tooloutermost and@rius.observe(kind=SpanKind.TOOL)on the function body. The tool then appears as a named tool span under the run. A bareobservewould make a chain span, and the Tool kind in the Spans tab’s All kinds menu and the Tool Loop alert would never see it. -
Scope each request to a session if one function call is one conversation: wrap the kickoff in
rius.session()and reuse your own id.
For the rest of init’s options and the delivery rules, see Reliability. The same flush rule applies to any function runtime. The Integrations page lists the other frameworks.
What you get
- A root agent span named
handle-ticketfor every invocation, with the model-call generations beneath it. Each generation carries the model, tokens, cost and time to first token. Costs are list-price estimates in US dollars. - Your tool functions as named tool spans, so you can see which tool a run called and when.
- The agent listed under your service name, with its traces, cost and errors, and no hand-written registration.
- Spans delivered before the runtime freezes.
client.flush()returnsFalseif the most recent export failed, so you can log a warning when spans did not leave the process. - Known blind spots: crews and tasks have no spans of their own, and generation spans are named
ChatCompletion. To see which task a generation belongs to, wrap per-task entry points withobserve. Streamed calls need usage enabled on the OpenAI client to carry token counts.
When something else fits
- If your function is written in TypeScript, there is no CrewAI integration for it. CrewAI is Python only, and the Integrations matrix shows what each language supports.
- If the same crew runs as a long-lived worker or server rather than a per-request function, you can leave heartbeats on. Python also flushes automatically at clean interpreter exit, so you need no flush after each request.
- If a task’s structure matters more than the run’s, add an
observewrapper per task entry point instead of relying on one root span.