Migrate from Langfuse
If you are using Langfuse for LLM observability, switching to Waxell Observe is straightforward. This tutorial maps Langfuse concepts to their Waxell equivalents, shows side-by-side code migration, and highlights what Waxell adds on top.
Prerequisites
- An existing Langfuse integration you want to migrate
waxell-observeinstalled (pip install waxell-observe)- A Waxell API key (get one from your Waxell control plane dashboard)
What You'll Learn
- How Langfuse concepts map to Waxell equivalents
- Step-by-step code migration with before/after examples
- How to migrate prompts from Langfuse to Waxell
- What Waxell provides beyond Langfuse's feature set
Step 1: Concept Mapping
| Langfuse | Waxell Observe | Notes |
|---|---|---|
| Trace | AgentExecutionRun | Top-level execution unit. Created by @waxell.observe. |
| Generation | LlmCallRecord | Individual LLM call with model, tokens, cost. Auto-captured by waxell.init() -- no manual recording. |
| Span | Step | Sub-operation within a run. Use @waxell.step_dec or waxell.step(). |
| Session | Session (session_id) | Group of related runs. Pass session_id as a kwarg at call time. |
| Score | Score | Quality metrics (numeric, categorical, boolean). Via waxell.score(). |
| Prompt | Prompt + PromptVersion | Versioned prompt management with labels. Fetched via client.get_prompt(). |
| Tag | Tag | Searchable key-value labels. Via waxell.tag(). |
| User | User (user_id) | End-user tracking. Pass user_id as a kwarg at call time. |
| Metadata | Metadata + Tags | Tags are searchable key-value pairs; metadata supports complex values via waxell.metadata(). |
The core concepts are nearly 1:1, which makes migration mechanical. The main difference is that Waxell init() auto-instruments your LLM clients -- so there is no equivalent of trace.generation() to call manually. You also get governance, multi-tenancy, and agent lifecycle on top.
Step 2: Code Migration -- Basic Tracing
In Waxell, the decorator is the primary pattern. Call waxell.init() once at startup -- before importing your LLM SDK -- and every OpenAI/Anthropic call inside an @waxell.observe function is captured automatically.
Before (Langfuse)
from langfuse import Langfuse
langfuse = Langfuse(
public_key="pk-...",
secret_key="sk-...",
host="https://cloud.langfuse.com",
)
trace = langfuse.trace(
name="my-agent",
session_id="session-123",
user_id="user-42",
metadata={"environment": "production"},
)
generation = trace.generation(
name="chat",
model="gpt-4o",
input=[{"role": "user", "content": "Hello"}],
output={"role": "assistant", "content": "Hi there!"},
usage={"input": 10, "output": 5},
)
trace.score(name="quality", value=0.9)
langfuse.flush()
After (Waxell Observe)
import waxell_observe as waxell
waxell.init() # before openai import — enables auto-instrumentation
import openai
client = openai.OpenAI()
@waxell.observe(agent_name="my-agent")
async def my_agent(message: str) -> str:
waxell.tag("environment", "production")
response = client.chat.completions.create( # auto-captured -- no record_llm_call()
model="gpt-4o",
messages=[{"role": "user", "content": message}],
)
waxell.score("quality", 0.9)
return response.choices[0].message.content
# session_id / user_id are passed at call time -- @observe intercepts them
await my_agent("Hello", session_id="session-123", user_id="user-42")
Key differences from Langfuse:
- No
trace.generation()calls.waxell.init()instruments OpenAI/Anthropic/etc. directly -- the LLM call is captured the moment it happens. - No explicit flush. The
@observedecorator flushes on function exit. - session_id and user_id are call-time kwargs, not init-time arguments -- so the same decorated function can serve many sessions.
Step 3: Code Migration -- Decorator Pattern
Langfuse's @observe decorator maps to Waxell's @waxell.observe decorator.
Before (Langfuse)
from langfuse.decorators import observe
@observe()
def my_agent(query: str) -> str:
result = call_llm(query)
return result
After (Waxell Observe)
import waxell_observe as waxell
waxell.init()
import openai
client = openai.OpenAI()
@waxell.observe(agent_name="my-agent")
def my_agent(query: str) -> str:
response = client.chat.completions.create( # auto-captured
model="gpt-4o",
messages=[{"role": "user", "content": query}],
)
return response.choices[0].message.content
Notes:
- Waxell requires
agent_name(Langfuse uses the function name by default) - Waxell automatically captures function inputs, outputs, and every LLM call inside the function (set
capture_io=Falseto disable I/O capture) - For retrieval, tools, and decisions, Waxell provides additional decorators:
@waxell.retrieval,@waxell.tool,@waxell.decision,@waxell.step_dec
Step 4: Code Migration -- LangChain Integration
Both platforms provide LangChain callback handlers.
Before (Langfuse)
from langfuse.callback import CallbackHandler
handler = CallbackHandler(
public_key="pk-...",
secret_key="sk-...",
host="https://cloud.langfuse.com",
)
chain = prompt | llm
result = chain.invoke(
{"question": "What is Waxell?"},
config={"callbacks": [handler]},
)
After (Waxell Observe)
from waxell_observe.integrations.langchain import WaxellLangChainHandler
handler = WaxellLangChainHandler(agent_name="langchain-bot")
chain = prompt | llm
result = chain.invoke(
{"question": "What is Waxell?"},
config={"callbacks": [handler]},
)
# Flush when done
handler.flush_sync(result={"output": result.content})
The Waxell LangChain handler automatically captures:
- Every LLM call with model name, token counts, and cost estimates
- Prompt and response previews
- Chain and tool spans for the full execution trace
Step 5: Code Migration -- Sessions and Users
Before (Langfuse)
trace = langfuse.trace(
name="my-agent",
session_id="session-123",
user_id="user-42",
)
After (Waxell Observe)
Pass session_id and user_id as kwargs at call time. @waxell.observe intercepts them and applies them to the run -- so the same decorated function works for any session.
@waxell.observe(agent_name="my-agent")
async def my_agent(query: str) -> str:
response = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": query}],
)
return response.choices[0].message.content
await my_agent(
"What is Waxell?",
session_id="session-123",
user_id="user-42",
)
Session and user data is available in the Waxell dashboard under Observability > Sessions and Observability > Users.
Step 6: Code Migration -- Scores
Before (Langfuse)
trace.score(name="accuracy", value=0.95)
trace.score(name="category", value="relevant")
trace.score(name="thumbs_up", value=1)
After (Waxell Observe)
Call waxell.score() inline from anywhere inside the decorated function:
@waxell.observe(agent_name="my-agent")
async def my_agent(query: str) -> str:
answer = ...
waxell.score("accuracy", 0.95) # numeric (default)
waxell.score("category", "relevant", data_type="categorical")
waxell.score("thumbs_up", True, data_type="boolean")
return answer
Waxell has explicit data_type support (numeric, categorical, boolean) which enables proper analytics -- averages for numeric, value distributions for categorical, and pass/fail rates for boolean.
Step 7: Migrate Prompts
If you manage prompts in Langfuse, you can migrate them to Waxell's prompt management system.
Before (Langfuse)
prompt = langfuse.get_prompt("support-agent")
compiled = prompt.compile(customer_name="Alice")
After (Waxell Observe)
from waxell_observe import WaxellObserveClient
client = WaxellObserveClient()
# Fetch latest version
prompt = await client.get_prompt("support-agent")
compiled = prompt.compile(customer_name="Alice")
# Fetch specific version
prompt_v2 = await client.get_prompt("support-agent", version=2)
# Fetch by label
production_prompt = await client.get_prompt("support-agent", label="production")
Synchronous version:
prompt = client.get_prompt_sync(name="support-agent")
compiled = prompt.compile(customer_name="Alice")
To migrate your existing prompts:
- Export your prompts from Langfuse (Settings > Prompts)
- Create them in Waxell via the API or dashboard
- Update your code to fetch from Waxell instead of Langfuse
Step 8: Migration Checklist
Use this checklist to track your migration:
- Install
waxell-observe(pip install waxell-observe) - Set environment variables (
WAXELL_API_URL,WAXELL_API_KEY) - Add
waxell.init()at startup, before importing OpenAI/Anthropic/etc. - Replace
langfuse.trace()and@langfuse.observewith@waxell.observe(agent_name="...") - Remove all
trace.generation()calls --waxell.init()auto-captures LLM calls - Replace
trace.score()with inlinewaxell.score(name, value, data_type=...) - Replace metadata/tags with inline
waxell.metadata()/waxell.tag() - Move
session_id/user_idfrom constructor args to call-time kwargs - Replace
langfuse.get_prompt()withclient.get_prompt() - Replace LangChain
CallbackHandlerwithWaxellLangChainHandler - Remove
langfuse.flush()-- the@observedecorator handles flushing - Remove
langfusefrom dependencies - Verify data appears in Waxell dashboard
Step 9: What Waxell Adds Beyond Langfuse
Waxell Observe is more than a Langfuse replacement. It is part of a full agent governance platform.
Governance and Policy Enforcement
Waxell can enforce policies before, during, and after agent execution:
@waxell.observe(agent_name="support-bot", enforce_policy=True)
async def handle_query(query: str) -> str:
# If a budget, safety, or operations policy blocks this agent,
# a PolicyViolationError is raised before execution begins.
response = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": query}],
)
return response.choices[0].message.content
Policy types include:
- Budget policies -- Block or warn when cost or token limits are exceeded
- Safety policies -- Enforce tool call limits and step count maximums
- Operations policies -- Monitor execution duration and latency
Multi-Tenancy
Full tenant isolation out of the box. Each tenant gets:
- Separate data schemas (no data leakage between tenants)
- Independent model cost overrides
- Per-tenant policies and governance rules
- Isolated API keys and authentication
Agent Lifecycle Management
Beyond observability, Waxell manages the full agent lifecycle:
- Deploy and configure agents via the control plane
- Start/stop/pause agents with management commands
- Agent registry for discovering and managing all agents
- Workflow orchestration with pause/resume support
OpenTelemetry Native
Waxell generates standard OpenTelemetry traces alongside its HTTP data path:
- Compatible with any OTel-compatible backend (Jaeger, Grafana Tempo, Datadog)
- Distributed tracing across services
- Correlate agent execution with infrastructure metrics
- No vendor lock-in on the tracing layer
Enterprise Features
- SSO -- SAML/OIDC single sign-on
- RBAC -- Role-based access control with fine-grained permissions
- Audit logs -- Track who did what and when
- Self-hosted -- Run entirely on your infrastructure
Advanced: When to Use WaxellContext
For batch loops or orchestration that opens and closes runs at arbitrary points (rather than once per function call), Waxell exposes a lower-level WaxellContext async context manager. It is not the recommended migration target -- it is an escape hatch for the small set of cases where the decorator model does not fit. See the Context Manager reference.
Next Steps
- Quickstart -- Get started with Waxell Observe in 5 minutes
- Installation & Configuration -- All configuration options
- Instrument OpenAI Directly -- Deep dive into instrumentation approaches
- Cost Optimization -- Take advantage of Waxell's cost management
- Policy & Governance -- Set up governance policies