Skip to Content

How do I trace an agent built with CrewAI and deployed to AWS Lambda?

Rius traces a CrewAI agent on AWS Lambda when you call rius.init once outside your handler, wrap the crew’s kickoff in observe, and call flush at the end of every invocation. The model calls CrewAI makes arrive as generations with tokens and cost, and your observe wrapper becomes the root agent span. Lambda freezes the process between invocations, so the flush is what gets your spans out. Set the service name and the observe name to the name you want the agent to carry in Rius.

How it works

  • Model calls. CrewAI 1.x drives the OpenAI client, so the openai integration captures its model calls with model, tokens and cost. If LiteLLM is installed in your Lambda package, CrewAI uses it instead, and you use the LiteLLM integration. Both are covered on the CrewAI integration page.
  • Run structure. Crews and tasks produce no spans of their own. Your observe wrapper around the kickoff becomes the root agent span, and an observe wrapper on a tool body becomes a tool span.
  • Export. Spans are queued in the process and sent in batches from a background thread. Lambda suspends the process after each invocation, and a suspended thread exports nothing. The exit hook may never run either, so you flush explicitly at the end of every invocation, as described on the reliability page.
  • Heartbeat. The SDK sends a liveness heartbeat by default. In a frozen runtime the instance flaps between alive and stale or gone, so you turn the heartbeat off in Lambda. Your agent still appears from its traces.

Set it up

  1. Add the SDK and CrewAI to your Lambda package. The shared setup, including keys and endpoints, is on the integrations page.
pip install "glassflow-rius[openai]" crewai
  1. Set these in the function’s environment variables:
    • RIUS_API_KEY holds your Rius API key, so it stays out of your code.
    • CREWAI_TRACING_ENABLED=false keeps CrewAI’s own tracing product from prompting interactively on first run. A Lambda has no terminal attached.
  2. Call rius.init at module level, outside the handler, so it runs once when the execution environment starts and warm invocations reuse it. Pass heartbeat=False. Set service_name to the agent’s name.
  3. Wrap the function that calls Crew.kickoff() in observe with kind=SpanKind.AGENT and a stable name. Wrap tool bodies with observe(kind=SpanKind.TOOL), with @tool outermost.
  4. In the handler, call client.flush in a finally block, so spans leave even when the crew raises an error.
import rius from crewai import Agent, Crew, Task from crewai.tools import tool from rius import SpanKind client = rius.init(service_name="support-crew", heartbeat=False) @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()) def lambda_handler(event, context): try: return handle_ticket(event["ticket"]) finally: client.flush(timeout_millis=30_000)
  1. Pick names deliberately. service_name is what the agent is grouped under, unless you set RIUS_AGENT_NAME, which overrides it. The observe name is the root span’s name in the trace, so keep it the same across deploys. You can also set RIUS_HEARTBEAT=false instead of passing heartbeat=False.

Keep flush at the invocation boundary. Do not call it inside tool bodies or other hot paths, because it blocks until the export completes. Do not call shutdown per invocation: it stops the export thread and releases the global init slot, and your next warm invocation would then have no exporter.

What you get

  • One trace per invocation, with a root agent span carrying your chosen name.
  • A generation span for each model call, with model, tokens and cost. Cost is a list-price estimate in US dollars, computed by Rius.
  • Tool spans with the names you gave them, parented under the run.
  • A delivery check: flush returns False on timeout or when the most recent export was rejected, so you can log it from the handler.
  • Known gaps. Generation spans are all named ChatCompletion. Which task a call belongs to is not visible unless you wrap per-task functions with observe. Streamed calls carry token counts only when usage is enabled on the underlying client.

When something else fits

  • If your Lambda has LiteLLM installed, CrewAI routes through it, so follow the LiteLLM integration for model-call capture instead.
  • If the function stays warm for long periods behind a container or server runtime instead of Lambda, the exit hook and the heartbeat work normally, and you can skip the per-invocation flush.
  • If your Lambda runs Node.js, CrewAI is not available there, because the CrewAI integration is Python only.

Still stuck? Get help

Last updated on