> ## Documentation Index
> Fetch the complete documentation index at: https://docs.inference.net/llms.txt
> Use this file to discover all available pages before exploring further.

# Langfuse Traces

> Send Langfuse SDK traces to Catalyst without changing your tracing code.

If your application already uses Langfuse, this is just an env-var switch. Keep all of your existing Langfuse tracing code and point the Langfuse SDK at Catalyst by changing three environment variables. Your spans stream into the Catalyst **Traces** dashboard with no SDK swap and no code changes. Catalyst accepts the Langfuse ingestion and OTEL endpoints and converts traces, generations, spans, usage, costs, inputs, outputs, metadata, users, sessions, and tags into Catalyst spans.

This is the fastest way to try Catalyst: flip the env vars, see your traces, and switch over fully later for more control. Spans show up in the Traces dashboard out of the box but to get the most out of it, set an agent identity on your top-level span so runs group in the **Agents** dashboard and become available to Halo. See [Group by agent](#group-by-agent).

## Configure Langfuse

Use your Catalyst API key as the Langfuse secret key. The public key is only present for Langfuse SDK compatibility and its value is never used. Set it to `pk-catalyst`, but any non-empty value works.

<Metadata text="integrations/traces/langfuse-env" />

```bash theme={"system"}
export LANGFUSE_HOST="https://telemetry.inference.net"
export LANGFUSE_PUBLIC_KEY="pk-catalyst"
export LANGFUSE_SECRET_KEY="<your-catalyst-api-key>"
```

The Catalyst API key must have write access to the project that should receive the traces.

## Python

<Metadata text="integrations/traces/langfuse-python" />

```python Python theme={"system"}
from langfuse import Langfuse, observe, get_client

langfuse = Langfuse(
    public_key="pk-catalyst",
    secret_key="<your-catalyst-api-key>",
    base_url="https://telemetry.inference.net",
)

@observe(as_type="generation", name="answer-question")
def answer_question(question: str) -> str:
    client = get_client()
    # Set identity the Langfuse way. These map to user.id, session.id, and tags in Catalyst.
    client.update_current_trace(
        user_id="user-123",
        session_id="session-abc",
        tags=["prod"],
    )
    client.update_current_generation(
        model="gpt-4o-mini",
        usage_details={"input": 12, "output": 8, "total": 20},
    )
    return f"Answer: {question}"

print(answer_question("What is Catalyst?"))
langfuse.flush()
langfuse.shutdown()
```

## TypeScript

<Metadata text="integrations/traces/langfuse-typescript" />

```typescript TypeScript theme={"system"}
import { observe, getClient } from "@langfuse/tracing";

process.env.LANGFUSE_HOST = "https://telemetry.inference.net";
process.env.LANGFUSE_PUBLIC_KEY = "pk-catalyst";
process.env.LANGFUSE_SECRET_KEY = "<your-catalyst-api-key>";

const answerQuestion = observe(
  async (question: string) => {
    const client = getClient();
    // Set identity the Langfuse way. These map to user.id, session.id, and tags in Catalyst.
    client.updateCurrentTrace({
      userId: "user-123",
      sessionId: "session-abc",
      tags: ["prod"],
    });
    client.updateCurrentGeneration({
      model: "gpt-4o-mini",
      usageDetails: { input: 12, output: 8, total: 20 },
    });
    return `Answer: ${question}`;
  },
  { name: "answer-question", asType: "generation" },
);

console.log(await answerQuestion("What is Catalyst?"));
await getClient().flushAsync();
```

Langfuse's OpenTelemetry-based SDKs are also supported with the same host, public key, and secret key settings above. If you are not using Langfuse and are configuring a generic OpenTelemetry exporter, use Catalyst's standard OTLP endpoint instead.

## Send to Both Langfuse and Catalyst

The setup above points the Langfuse SDK entirely at Catalyst, so all of that
SDK's traffic comes to us. If you want to keep your existing Langfuse pipeline
and mirror the same traces into Catalyst, you can fan out to both at once. This
is the lowest-risk way to evaluate Catalyst: Langfuse keeps working untouched
while Catalyst receives a copy of everything.

### TypeScript: add a second span processor

The Langfuse JS/TS SDK is OpenTelemetry-based, and OpenTelemetry broadcasts
every span to every registered span processor by default. Keep your existing
`LangfuseSpanProcessor` pointed at Langfuse and add a second one pointed at
Catalyst. Your `observe` calls and the rest of your tracing code do not change.

<Metadata text="integrations/traces/langfuse-dual-typescript" />

```typescript instrumentation.ts theme={"system"}
import { LangfuseSpanProcessor } from "@langfuse/otel";
import { NodeSDK } from "@opentelemetry/sdk-node";

const sdk = new NodeSDK({
  spanProcessors: [
    // Your existing Langfuse destination.
    new LangfuseSpanProcessor({
      baseUrl: "https://cloud.langfuse.com",
      publicKey: process.env.LANGFUSE_PUBLIC_KEY,
      secretKey: process.env.LANGFUSE_SECRET_KEY,
    }),
    // Catalyst destination. The secret key is your Catalyst API key. The public
    // key is required by the SDK but ignored by Catalyst, so any non-empty
    // value works.
    new LangfuseSpanProcessor({
      baseUrl: "https://telemetry.inference.net",
      publicKey: "pk-catalyst",
      secretKey: "<your-catalyst-api-key>",
    }),
  ],
});

sdk.start();
```

Every span now streams to both backends. Each processor batches and flushes
independently, so a delay or outage on one does not block the other.

### Python: fan out with an OpenTelemetry Collector

The Python SDK does not broadcast across destinations. Its multi-client mode
routes each span to a single project based on a public-key attribute, so it is
not a true dual-send: you have to thread `langfuse_public_key` through every
`@observe`, OpenAI wrapper, and Langchain handler call to pick the destination,
and spans without that attribute land everywhere by accident. It is fragile and
not the documented path for sending to both.

The clean, SDK-agnostic way to dual-send from Python (or any runtime) is an
OpenTelemetry Collector with two OTLP exporters. Both Langfuse and Catalyst
accept OTLP, so point your app at a local collector and let the collector ship
to both. Your application code does not change.

<Metadata text="integrations/traces/langfuse-dual-collector" />

```yaml otel-collector.yaml theme={"system"}
receivers:
  otlp:
    protocols:
      http:
      grpc:

exporters:
  # base64-encode "<langfuse-public-key>:<langfuse-secret-key>" for the header.
  otlphttp/langfuse:
    endpoint: https://cloud.langfuse.com/api/public/otel
    headers:
      Authorization: "Basic <base64 of langfuse-public-key:langfuse-secret-key>"
  # base64-encode "pk-catalyst:<your-catalyst-api-key>" for the header.
  otlphttp/catalyst:
    endpoint: https://telemetry.inference.net/api/public/otel
    headers:
      Authorization: "Basic <base64 of pk-catalyst:your-catalyst-api-key>"

service:
  pipelines:
    traces:
      receivers: [otlp]
      exporters: [otlphttp/langfuse, otlphttp/catalyst]
```

Point your Langfuse SDK (or any OTLP exporter) at the collector's OTLP endpoint
and every trace is mirrored to both backends. This is the same fan-out pattern
Catalyst uses internally to ship telemetry to multiple sinks.

<Note>
  The collector approach works for any OTLP trace source, not just Langfuse. If
  you already export OpenTelemetry traces, add a second OTLP exporter for
  Catalyst and you are done.
</Note>

## What Maps Into Catalyst

| Langfuse field      | Catalyst field                                                                                                                    |
| ------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| Trace ID            | Stable Catalyst trace ID derived from the Langfuse trace ID                                                                       |
| Observation ID      | Stable Catalyst span ID derived from the Langfuse observation ID                                                                  |
| Trace name          | `langfuse.trace.name` (shown in the Traces view; does **not** create an agent on its own — see [Group by agent](#group-by-agent)) |
| User and session    | `user.id`, `session.id`                                                                                                           |
| Agent name / id     | `agent.name`, `agent.id` — only when you set them explicitly (see [Group by agent](#group-by-agent))                              |
| Generation model    | `llm.model_name`, `llm.provider` when inferable                                                                                   |
| Usage and cost      | Prompt/completion/total token columns and cost columns                                                                            |
| Input and output    | Span input/output and LLM message columns                                                                                         |
| Metadata and tags   | Preserved under `langfuse.*` and `halo.source.*` attributes                                                                       |
| Errors and warnings | Span status and status message                                                                                                    |

<Note>
  This is v1 of the Langfuse integration — scores, evaluations, and richer
  mapping are coming later. Langfuse scores and evaluations are not supported at
  this time. Pointing
  `LANGFUSE_HOST` at Catalyst routes all of that SDK's traffic to us. To keep
  sending to Langfuse as well, see [Send to both Langfuse and
  Catalyst](#send-to-both-langfuse-and-catalyst). Scores emitted through the
  SDK are not ingested at this point in time.
</Note>

## Group by agent

Pointing the base URL at Catalyst sends your spans — but Catalyst does **not**
treat every trace as an agent. A trace name, or even a Langfuse `agent`-type
observation, is not enough on its own; otherwise every traced workload would
flood the Agents dashboard. Agent grouping is **opt-in**: set `agent.name` (and
optionally a stable `agent.id`) on the **top-level span of your agent**, and
Catalyst groups that whole trace — and every child span — under it in the
**Agents** dashboard. This is also what makes the trace available to
[Halo](/platform/gateway/overview), which operates on agents.

The Langfuse SDKs are OpenTelemetry-based and forward raw OTel attributes
untouched, so set the two attributes directly on the active span. Do this
**only on the top-level agent span** — children inherit grouping through the
trace and should not carry their own agent identity.

* `agent.name` — the human-readable label the Agents dashboard groups on.
* `agent.id` — a stable identifier that survives display-name changes. When
  set, it wins over `agent.name`, so renaming the agent doesn't split its
  history. Omit it if you don't need stable grouping.

<Metadata text="integrations/traces/langfuse-agent-typescript" />

```typescript TypeScript theme={"system"}
import { observe } from "@langfuse/tracing";
import { trace } from "@opentelemetry/api";

const runAgent = observe(
  async (question: string) => {
    // Mark this top-level span as an agent. Set ONLY here, not on children.
    trace.getActiveSpan()?.setAttributes({
      "agent.name": "deep-research",
      "agent.id": "deep-research-v1", // stable id; optional
    });
    // ... your agent loop, tool calls, and nested generations ...
    return "answer";
  },
  { name: "deep-research", asType: "agent" },
);
```

<Metadata text="integrations/traces/langfuse-agent-python" />

```python Python theme={"system"}
from langfuse import observe
from opentelemetry import trace

@observe(as_type="agent", name="deep-research")
def run_agent(question: str) -> str:
    # Mark this top-level span as an agent. Set ONLY here, not on children.
    span = trace.get_current_span()
    span.set_attribute("agent.name", "deep-research")
    span.set_attribute("agent.id", "deep-research-v1")  # stable id; optional
    # ... your agent loop, tool calls, and nested generations ...
    return "answer"
```

## View and Analyze

Once traces arrive, open the Catalyst dashboard and use the Traces tab to
inspect individual trace trees. Traces whose top-level span carries an
`agent.name` / `agent.id` (see [Group by agent](#group-by-agent)) are grouped
in the Agents dashboard, where Halo can run analysis over them.

For the best Halo reports, keep `agent.id` (or `agent.name`) stable per agent or
workflow and set `userId` / `sessionId` when available.

## Want More Control?

The Langfuse drop-in is the easy on-ramp: no code changes, just env vars. If you
want finer control over how traces are grouped, such as stable agent IDs,
explicit agent and manual spans, and a fully shaped trace tree, switch to the
first-party Catalyst tracing SDK. Start with the
[Quickstart](/integrations/traces/quickstart), then see
[Agent identity](/integrations/traces/agent-identity) for grouping executions in
the Agents dashboard.
