I trace CrewAI with Langfuse. How do I switch to Rius?
Rius traces CrewAI through its openai integration plus observe wrappers, so you replace your Langfuse setup with one init call and a few decorators. CrewAI’s model calls arrive as generations with model, tokens and cost. Your entrypoint becomes the root agent span and your tools become tool spans. You can run Rius next to Langfuse while you compare, then remove Langfuse.
How it works
CrewAI 1.x drives the OpenAI client natively, so the openai integration in the Rius SDK captures every model call the crew makes. rius.init() turns that integration on automatically when it is installed. You do not port Langfuse generation blocks one by one.
The crew structure runs inside the framework, where auto-instrumentation cannot see it. You add that shape yourself with observe on the entrypoint around crew.kickoff() and on each tool body. The CrewAI integration page covers the full setup. The integrations overview lists the other frameworks and libraries.
If LiteLLM is installed in your environment, CrewAI uses it as a fallback. In that case, use the litellm integration instead of openai.
Langfuse concepts map across like this:
- A Langfuse trace is a Rius trace, and an observation is a span.
- Sessions stay sessions. Replace Langfuse’s session propagation with
with rius.session(session_id):. - Langfuse’s
user_idbecomesuser.id. Replace its user propagation withwith rius.user(user_id):. - Your Langfuse project becomes an agent, named by
service.name. - One
RIUS_API_KEYreplaces the Langfuse key pair.
Set it up
-
Install the Rius SDK with the OpenAI instrumentation, next to CrewAI. The extra installs the instrumentation, not the library, so CrewAI stays at the version you already run.
pip install "glassflow-rius[openai]" crewaiWith LiteLLM installed, use
pip install "glassflow-rius[litellm]" litellminstead. -
Replace the Langfuse client setup with
rius.init. Then wrap the entrypoint and each tool body withobserve. Put@tooloutermost andobserveon the function body.lookupstands in for your own function.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())The TOOL kind matters. It makes the span show up as a tool call in Rius, where a bare
observewould not. -
Port your scopes. Wrap each kickoff in
rius.session()with your own conversation or job id, and userius.user()where you set a user in Langfuse. -
Keep CrewAI’s own tracing prompt quiet in CI and containers by setting
CREWAI_TRACING_ENABLED=false. Otherwise CrewAI asks about its tracing product on first run. -
Run both backends side by side. Run the swapped service on Rius while other services still report to Langfuse. Or, if you send OTLP through a Collector, add Rius as a second exporter. The Langfuse migration guide has the Collector configuration and the cutover checklist.
-
Run one ticket that calls a tool and open the trace in the console. Compare trace counts, tokens and cost against Langfuse for a few days. Then remove the Langfuse SDK or exporter and revoke its keys.
What you get
A ticket that calls one tool looks like this in Rius:
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- Root agent span. It is your
observewrapper around the kickoff. - A generation for every model call. Each carries model, tokens, cost and time to first token. Costs are list-price estimates in US dollars, not billed amounts.
- Named tool spans. Each one is your
observewrapper on a tool body. - Sessions and users. Cost, failures and traces group per session and per user.
Two things look different from what you may expect:
- Crews and tasks produce no spans of their own, so a generation does not show which task it belongs to. Wrap per-task entrypoints with
observeif you need that. - Generation spans carry the generic name
ChatCompletion.
Streamed calls carry token counts only when usage is enabled on the underlying client.
Some Langfuse features have no Rius equivalent. These are scores, annotation queues, evals, datasets and prompt management. Existing traces also stay in Langfuse, and Rius starts from the first span you send. Rius is a managed service only.
When something else fits
If scores, evals, datasets or prompt management are part of how you work, keep Langfuse for those and send traces to both tools. If you need to self-host your observability backend, Rius does not fit, because it runs only as a managed service. For other starting points, see Migrate to Rius.