Skip to main content

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-observe installed (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

LangfuseWaxell ObserveNotes
TraceAgentExecutionRunTop-level execution unit. Created by @waxell.observe.
GenerationLlmCallRecordIndividual LLM call with model, tokens, cost. Auto-captured by waxell.init() -- no manual recording.
SpanStepSub-operation within a run. Use @waxell.step_dec or waxell.step().
SessionSession (session_id)Group of related runs. Pass session_id as a kwarg at call time.
ScoreScoreQuality metrics (numeric, categorical, boolean). Via waxell.score().
PromptPrompt + PromptVersionVersioned prompt management with labels. Fetched via client.get_prompt().
TagTagSearchable key-value labels. Via waxell.tag().
UserUser (user_id)End-user tracking. Pass user_id as a kwarg at call time.
MetadataMetadata + TagsTags are searchable key-value pairs; metadata supports complex values via waxell.metadata().
info

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 @observe decorator 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=False to 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:

  1. Export your prompts from Langfuse (Settings > Prompts)
  2. Create them in Waxell via the API or dashboard
  3. 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.observe with @waxell.observe(agent_name="...")
  • Remove all trace.generation() calls -- waxell.init() auto-captures LLM calls
  • Replace trace.score() with inline waxell.score(name, value, data_type=...)
  • Replace metadata/tags with inline waxell.metadata() / waxell.tag()
  • Move session_id / user_id from constructor args to call-time kwargs
  • Replace langfuse.get_prompt() with client.get_prompt()
  • Replace LangChain CallbackHandler with WaxellLangChainHandler
  • Remove langfuse.flush() -- the @observe decorator handles flushing
  • Remove langfuse from 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