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

# Langfuse

> Forward Langfuse traces to calado with a webhook. No changes to your agent code.

## Overview

If your agent is already instrumented with [Langfuse](https://langfuse.com), calado can consume the traces Langfuse is already collecting. You do not re-instrument your app and you do not add the calado SDK. Langfuse posts each finished trace to calado, and the `format: "langfuse"` adapter normalizes it into the canonical conversation and per-step structure the rest of calado uses.

Use this page if your agent runs on **Langfuse**. For provider clients with no tracer, see the [Node.js SDK](/ingestion/node-sdk) or [Direct API](/ingestion/direct-api). For LangChain or LangGraph agents, see [LangChain](/ingestion/langchain) and [LangSmith](/ingestion/langsmith).

There are two ways in:

<CardGroup cols={2}>
  <Card title="Webhook" icon="bolt">
    Langfuse's built-in webhook posts every finished trace to calado in real time. Zero code in your runtime. Recommended.
  </Card>

  <Card title="Forwarder script" icon="terminal">
    A small script paginates Langfuse traces on a cursor and POSTs each batch. Use it for backfill or where webhooks are not an option.
  </Card>
</CardGroup>

Both paths hit `POST /api/ingest` with the same payload shape. Pick one.

## Webhook setup

LangFuse only sends a webhook when a **trigger** fires. Pointing the URL at calado does nothing on its own. This is the step customers most often miss.

<Steps>
  <Step title="Get your calado API key">
    In the calado dashboard, open your agent's **Settings** page and create an API key. It is prefixed with `cl_`. You paste it into Langfuse in the next step.
  </Step>

  <Step title="Create the webhook">
    In Langfuse, open your project and go to **Settings → Webhooks → Create webhook**.

    * **URL** — `https://app.calado.ai/api/ingest`
    * **HTTP headers** — add `Authorization: Bearer cl_<your_calado_api_key>`
    * **Body format** — JSON, using Langfuse's **full trace** payload option.
  </Step>

  <Step title="Add a trigger">
    Create a trigger on **Trace finished**. Without a trigger the webhook stays silent. Optionally add filters (project, environment tag, `release`) so noisy dev traces are not forwarded.
  </Step>

  <Step title="Send a test trace">
    Run your agent once. Langfuse's webhook delivery log should show a `202`, and the conversation appears in calado within a few seconds.
  </Step>
</Steps>

### Payload envelope

calado expects the trace wrapped in an envelope:

```json theme={null}
{
  "format": "langfuse",
  "conversations": [
    { "trace": { ... }, "observations": [ ... ] }
  ]
}
```

If your Langfuse webhook sends `{trace, observations}` directly, without the outer `format` and `conversations` envelope, put a small relay in front of calado that wraps the body. A single Cloudflare Worker or Vercel function is enough. The [forwarder script](#forwarder-script) below also does this and can run as the relay.

## Verifying it works

In calado, open **Agents → (your agent) → Conversations**. A forwarded trace shows up as a conversation whose `externalId` is the Langfuse `trace.id`. Any system prompt or tool schema Langfuse captured appears under **Agent definitions**.

Raw conversations appear immediately. Analysis does not start automatically — click **Run Analysis** on the agent page when you are ready to classify.

## Multi-agent traces

Langfuse traces with nested observations (an orchestrator and its sub-agents, linked via `parentObservationId`) map cleanly onto calado's step model. Each sub-agent span becomes a step on one conversation, and the dashboard renders the orchestrator and its sub-agents as a tree.

For aggregation to work, every step of one trace has to share a session id. The adapter uses `trace.sessionId` when Langfuse has one, and falls back to `trace.id` when it does not, so multi-agent traces collapse into a single conversation row either way. You do not need to set anything.

## What gets captured

| calado field                    | Langfuse source                                                                                                                                   |
| ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| `conversation.externalId`       | `trace.id`                                                                                                                                        |
| `conversation.sessionId`        | `trace.sessionId`, falling back to `trace.id`                                                                                                     |
| `conversation.messages`         | `GENERATION` observation `input` and `output` messages                                                                                            |
| Step name and hierarchy         | Observation `name`, `id`, `parentObservationId`                                                                                                   |
| System prompt                   | System-role messages in a `GENERATION` `input`                                                                                                    |
| Tool schemas                    | `GENERATION` `input.tools` and `modelParameters.tools`                                                                                            |
| Per-step inline definition      | A child observation's system-role `input` message, attached as that step's `inlineDefinition` so the sub-agent is analyzed against its own prompt |
| Tool calls                      | `SPAN` observations with tool execution                                                                                                           |
| Trace events                    | All observations, with timestamps and durations                                                                                                   |
| Token usage and model           | `GENERATION` `usage` and model metadata                                                                                                           |
| Tags, release, version, user id | `conversation.metadata`                                                                                                                           |

Anthropic-shaped and OpenAI-shaped message content are both normalized. Multimodal blocks (images, audio) pass through untouched.

A trace with no LLM activity is stored but marked as skipped for classification, so empty traces do not create noise in the dashboard.

## Forwarder script

Use the forwarder for backfills, or where the webhook route is not viable. It polls the Langfuse API on a cursor and POSTs each batch to calado.

```bash theme={null}
npm install langfuse
export LANGFUSE_PUBLIC_KEY=pk_lf_...
export LANGFUSE_SECRET_KEY=sk_lf_...
export LANGFUSE_BASE_URL=https://cloud.langfuse.com   # or your self-hosted URL
export CALADO_API_KEY=cl_...
export CALADO_BASE_URL=https://app.calado.ai

npx tsx forward.ts
```

The script keeps a local cursor file (`.langfuse-cursor.json`) so re-runs forward only new traces. The first run backfills everything; later runs are incremental. The full script is in the [`examples/langfuse-forwarder/`](https://github.com/calado-ai/calado/tree/main/examples/langfuse-forwarder) directory of the calado repo, ready to copy.

## Caps

| Limit                     | Value |
| ------------------------- | ----- |
| Request body size         | 5 MB  |
| Observations per trace    | 950   |
| Conversations per request | 1,000 |
| Message content           | 1 MB  |

If a single trace exceeds the observation cap, calado returns a structured `400 too_many_child_runs` naming the trace. The practical limit is usually the 5 MB body cap, which a verbose trace reaches well before 950 observations. If you hit either, reduce trace verbosity (drop noisy `EVENT` observations) or split long-running work into shorter sessions.

## Security

The webhook setup puts your calado API key into Langfuse's webhook configuration UI, as a header value. Anyone with access to your Langfuse project settings can read it. That is a slightly wider exposure surface than the direct-SDK path, where the key only ever lives in your runtime environment.

<Warning>
  Treat the webhook key like any credential stored in a third-party tool. Create a dedicated calado key for the Langfuse webhook so you can rotate just that one, and rotate it whenever someone with Langfuse settings access leaves your team. calado keys are one key per agent, so a leaked key can only POST to that single agent's stream.
</Warning>

Webhook-scoped sub-keys, where a leaked webhook key cannot be used for anything else, are on the calado roadmap. Until then, the rotation discipline above is the mitigation.

## Troubleshooting

**The webhook returns `400 too_many_child_runs`.** One trace exceeded the per-trace observation cap. Reduce verbosity or split the trace into shorter sessions.

**The webhook returns `401`.** The bearer token was rejected. Re-paste the key into the Langfuse webhook header. The usual cause is a stray space or a missing `Bearer ` prefix.

**No webhook activity at all.** The trigger is not configured. A webhook URL with no trigger never fires. See [Webhook setup](#webhook-setup), step 3.

<Card title="Next: redacting sensitive data" icon="arrow-right" href="/ingestion/redaction">
  Strip PII before traces leave your infrastructure.
</Card>
