IntegrationsExperiments

Ingest experiment spans with OpenTelemetry

Using Python or TypeScript? Use Experiments via SDK with the Langfuse SDKs. They handle these attributes automatically. This guide is for direct OpenTelemetry ingestion, primarily when no Langfuse SDK is available for your language.

An experiment 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:

LevelPurposeHow to apply itAttributes
Experiment contextPropagates the experiment's identity to every span in every item trace, so Langfuse can synthesize them as one experiment.Baggagelangfuse.experiment.id, langfuse.experiment.name, langfuse.experiment.dataset.id, langfuse.experiment.description (optional), langfuse.experiment.metadata.* (optional), langfuse.environment (optional)
Experiment item rootIdentifies one item trace root and stores its input, expected output, and actual output.Attributes directly on the root spanlangfuse.observation.input, langfuse.observation.output, langfuse.experiment.item.expected_output (optional), langfuse.experiment.item.metadata.* (optional)
Item contextPropagates one item's identity to the root and child spans in that item's trace.Root attributes, then Baggagelangfuse.experiment.item.id, langfuse.experiment.item.root_observation_id, langfuse.experiment.item.version (optional)

Configure a BaggageSpanProcessor as described in the Baggage propagation pattern. 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.

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.

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, 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, 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.

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, 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 for details.


Was this page helpful?

Last edited