---
title: Ingest experiment spans with OpenTelemetry
sidebarTitle: Experiments
description: Send experiment traces directly to Langfuse with OpenTelemetry experiment and item attributes.
---

# Ingest experiment spans with OpenTelemetry

  **Using Python or TypeScript?** Use [Experiments via SDK](/docs/evaluation/experiments/experiments-via-sdk)
  with the [Langfuse SDKs](/docs/observability/sdk/overview). They handle these
  attributes automatically. This guide is for direct OpenTelemetry ingestion,
  primarily when no Langfuse SDK is available for your language. In the
  evaluation docs, start from [Experiments via
  OpenTelemetry](/docs/evaluation/experiments/experiments-via-opentelemetry).

An [experiment](/docs/evaluation/core-concepts#experiments) runs your
application against test data. Each input is an experiment item, optionally
with an expected output. Each item corresponds to one trace that shows how your
application produced the actual output from that input, with child spans for
model calls, tool calls, and other work.

To make experiment data show correctly in Langfuse, model three levels of
context in this order:

| Level                | Purpose                                                                                                                    | How to apply it                      | Attributes                                                                                                                                                                                                           |
| -------------------- | -------------------------------------------------------------------------------------------------------------------------- | ------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Experiment context   | Propagates the experiment's identity to every span in every item trace, so Langfuse can synthesize them as one experiment. | Baggage                              | `langfuse.experiment.id`, `langfuse.experiment.name`, `langfuse.experiment.dataset.id`, `langfuse.experiment.description` (optional), `langfuse.experiment.metadata.*` (optional), `langfuse.environment` (optional) |
| Experiment item root | Identifies one item trace root and stores its input, expected output, and actual output.                                   | Attributes directly on the root span | `langfuse.observation.input`, `langfuse.observation.output`, `langfuse.experiment.item.expected_output` (optional), `langfuse.experiment.item.metadata.*` (optional)                                                 |
| Item context         | Propagates one item's identity to the root and child spans in that item's trace.                                           | Root attributes, then Baggage        | `langfuse.experiment.item.id`, `langfuse.experiment.item.root_observation_id`, `langfuse.experiment.item.version` (optional)                                                                                         |

Configure a `BaggageSpanProcessor` as described in the [Baggage propagation
pattern](/integrations/native/opentelemetry#recommended-use-opentelemetry-baggage-for-propagation).
It copies Baggage entries to each newly created span. The following sections
show how each level is represented.

  **Baggage is sent to downstream services:** OpenTelemetry injects Baggage
  into outbound request headers. This is usually not relevant for experiment
  context, but do not add sensitive values to it.

## 1. Experiment context: baggage shared by all item traces

Create experiment Baggage once. Start every item execution with this Baggage
and no active parent span: each execution must remain a separate trace. Do not
create an enclosing "experiment" span.

```text
experimentBaggage = baggageFromAttributes({
  "langfuse.experiment.id": experiment.id, // unique per experiment
  "langfuse.experiment.name": experiment.name, // unique per experiment
  "langfuse.experiment.dataset.id": experiment.datasetId, // recommended: Manage datasets in Langfuse
  "langfuse.experiment.description": experiment.description, // optional
  "langfuse.experiment.metadata.prompt_version": "v4", // optional
  "langfuse.environment": "experiment", // optional: separate environment for experiment traffic
})
```

## 2. Experiment item root: one trace per item

An experiment item has an input, an expected output, and the actual output from
your application. Its trace captures the execution that produces the actual
output from that input. Langfuse expects exactly one trace per experiment item.

Langfuse needs a clear root span to identify the item trace and its data. The
root's `langfuse.observation.input` should be the experiment item input, and
its `langfuse.observation.output` should be the actual output. Create the root
with the experiment Baggage, then set the expected output and metadata directly
on it. Its
`langfuse.experiment.item.root_observation_id` must equal its own OTel `spanId`.
Prefer this span to also be the OTel trace root, with no parent span or Langfuse
parent observation.

```text
for each item:
  root = tracer.startSpan(
    "experiment-item",
    withBaggage(ROOT_CONTEXT, experimentBaggage),
  )
  rootId = root.spanContext().spanId

  itemAttributes = {
    "langfuse.experiment.item.id": item.id, // DatasetItem ID when linking a Langfuse-managed dataset
    "langfuse.experiment.item.version": item.version, // optional: Langfuse-managed datasets only
    "langfuse.experiment.item.root_observation_id": rootId,
  }

  root.setAttributes({
    ...itemAttributes,
    "langfuse.experiment.item.expected_output": jsonEncode(item.expectedOutput), // optional
    "langfuse.experiment.item.metadata.country": item.metadata.country, // optional
  })
```

To link a [Langfuse-managed dataset](/docs/evaluation/experiments/datasets),
set the experiment dataset ID to `Dataset.id` and
`langfuse.experiment.item.id` to the `DatasetItem.id` returned by Langfuse. If
you fetch a
[specific dataset version](/docs/evaluation/experiments/datasets#fetch-dataset-at-a-specific-version),
set the item version to the same version timestamp.

When using local data, you can also set the experiment dataset ID and item IDs
as local correlation identifiers. Use stable IDs to correlate the same local
dataset across repeated experiment runs. Do not set an item version for local
data.

## 3. Item context: baggage shared within one item trace

Before starting child spans, add the item identity and root ID to Baggage. This
lets Langfuse link model calls, tool calls, and other work to the experiment
item.

```text
itemBaggage = mergeBaggage(
  experimentBaggage,
  baggageFromAttributes(itemAttributes),
)

withContext(withActiveSpan(withBaggage(ROOT_CONTEXT, itemBaggage), root)):
  runInstrumentedTask(item)

root.end()
```

  **Scores:** Attach experiment-item scores to the root observation. When using
  the [Scores API](/docs/evaluation/evaluation-methods/scores-via-sdk#trace-or-observation-level-scores),
  use the root span's `traceId` as `traceId` and its `spanId` as
  `observationId`.

## Choose an environment for experiment runs

Experiment runs often deserve a separate environment such as `experiment` so
they do not mix with production traffic. If the exporter only sends experiment
runs, set `langfuse.environment=experiment` as a resource attribute. If the
same exporter also sends production traffic, add `langfuse.environment` to the
experiment Baggage. This applies it to every item trace and span in the
experiment. See [Environments](/docs/observability/features/environments) for
details.

<!-- agent-instructions -->

---

## Agent Instructions

This page is part of the [Langfuse](https://langfuse.com) documentation, published as plain Markdown for AI agents. Every page is available as Markdown by appending `.md` to its URL, or by sending an `Accept: text/markdown` header. This page: `https://langfuse.com/integrations/native/opentelemetry/experiments.md`.

### Querying these docs

If the answer is not on this page, query the documentation instead of guessing:

- **Semantic search** across all Langfuse docs, returning an answer with the relevant pages and excerpts. Ask a specific, self-contained question:

  ```bash
  curl -sG "https://langfuse.com/api/search-docs" --data-urlencode "query=How do I trace a LangGraph agent?"
  ```

- **Index of every page**: <https://langfuse.com/llms.txt>, with per-section indexes [llms-docs.txt](https://langfuse.com/llms-docs.txt), [llms-integrations.txt](https://langfuse.com/llms-integrations.txt), and [llms-self-hosting.txt](https://langfuse.com/llms-self-hosting.txt).

### Before writing Langfuse code

- **Install the [Langfuse Agent Skill](https://langfuse.com/docs/api-and-data-platform/features/agent-skill).** It encodes Langfuse's own best practices for instrumentation, prompt management, and evaluation, and materially improves results.
- **Read [What does a good trace look like?](https://langfuse.com/docs/observability/best-practices.md)** before instrumenting an application.
- **Verify endpoints, parameters, and response fields** against the [API reference](https://api.reference.langfuse.com) instead of inferring them from code examples.
- **Use the [Langfuse CLI](https://langfuse.com/docs/api-and-data-platform/features/cli)** (`npx langfuse-cli api <resource> <action>`) to read or write traces, prompts, datasets, and scores from the terminal.

Found an error in these docs? Please open an issue at <https://github.com/langfuse/langfuse-docs/issues>.
