Skip to main content
Metadata enriches your traces with contextual information, who made the request, which conversation it belongs to, and any custom data relevant to your application. Labels help you categorize and filter traces in the dashboard. This guide provides a unified reference for sending metadata across all integration methods. For SDK-specific details, see the tutorials linked below.

Quick Reference

ConceptOTEL AttributeREST APIDescription
Thread/Conversationgen_ai.conversation.idmetadata.thread_idGroups messages in a conversation
User IDlangwatch.user.idmetadata.user_idIdentifies the end user
Customer IDlangwatch.customer.idmetadata.customer_idYour platform’s customer/tenant
Labelslangwatch.labelsmetadata.labelsCategorization tags
Custom Metadatametadata attributemetadata.*Any additional context
For OTEL, gen_ai.conversation.id follows the OpenTelemetry GenAI semantic conventions. The legacy langwatch.thread.id attribute is also supported.

SDK Examples

For detailed SDK-specific tutorials, see:
import { setupObservability } from "langwatch/observability/node";
import { getLangWatchTracer } from "langwatch";

setupObservability();
const tracer = getLangWatchTracer("my-service");

async function handleUserMessage(userId: string, conversationId: string) {
  return await tracer.withActiveSpan("HandleMessage", async (span) => {
    // Thread/conversation ID (OTEL semconv)
    span.setAttribute("gen_ai.conversation.id", conversationId);

    // User and customer identification
    span.setAttribute("langwatch.user.id", userId);
    span.setAttribute("langwatch.customer.id", "tenant-123");

    // Labels for filtering (JSON array)
    span.setAttribute("langwatch.labels", JSON.stringify(["production", "premium-user"]));

    // Custom metadata (JSON object)
    span.setAttribute("metadata", JSON.stringify({
      feature_flags: ["new-ui", "beta-model"],
      request_source: "mobile-ios"
    }));

    // Your application logic...
  });
}
import langwatch

@langwatch.trace()
def handle_request(user_id: str, thread_id: str):
    langwatch.get_current_trace().update(
        metadata={
            "user_id": user_id,
            "thread_id": thread_id,
            "labels": ["production", "premium"],
            "custom_field": "any value"
        }
    )

    # Your logic here...

Raw OpenTelemetry

If you’re using vanilla OpenTelemetry without the LangWatch SDK:
import { trace } from "@opentelemetry/api";

const tracer = trace.getTracer("my-service");

tracer.startActiveSpan("operation", (span) => {
  // OTEL semconv for conversation/thread
  span.setAttribute("gen_ai.conversation.id", "conv-456");

  // LangWatch-specific attributes
  span.setAttribute("langwatch.user.id", "user-123");
  span.setAttribute("langwatch.customer.id", "customer-789");
  span.setAttribute("langwatch.labels", JSON.stringify(["urgent", "support"]));

  // Custom metadata as JSON string
  span.setAttribute("metadata", JSON.stringify({
    priority: "high",
    department: "engineering"
  }));

  // ... your code ...
  span.end();
});
import json
from opentelemetry import trace

tracer = trace.get_tracer("my-service")

with tracer.start_as_current_span("operation") as span:
    # OTEL semconv for conversation/thread
    span.set_attribute("gen_ai.conversation.id", "conv-456")

    # LangWatch-specific attributes
    span.set_attribute("langwatch.user.id", "user-123")
    span.set_attribute("langwatch.customer.id", "customer-789")
    span.set_attribute("langwatch.labels", '["urgent", "support"]')

    # Custom metadata as JSON string
    span.set_attribute("metadata", json.dumps({
        "priority": "high",
        "department": "engineering"
    }))

    # ... your code ...
Exporter configuration:
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";

const exporter = new OTLPTraceExporter({
  url: "https://app.langwatch.ai/api/otel/v1/traces",
  headers: {
    Authorization: `Bearer ${process.env.LANGWATCH_API_KEY}`,
  },
});
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter

exporter = OTLPSpanExporter(
    endpoint="https://app.langwatch.ai/api/otel/v1/traces",
    headers={"Authorization": f"Bearer {os.environ['LANGWATCH_API_KEY']}"},
)
The OTEL endpoint is /api/otel/v1/traces (not /v1/traces).

REST API

Send traces directly via HTTP. See REST API for full details.
curl -X POST "https://app.langwatch.ai/api/collector" \
  -H "X-Auth-Token: $LANGWATCH_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "trace_id": "trace-123",
    "spans": [
      {
        "type": "llm",
        "span_id": "span-456",
        "name": "chat-completion",
        "model": "gpt-4",
        "input": {"type": "text", "value": "Hello"},
        "output": {"type": "text", "value": "Hi there!"},
        "timestamps": {
          "started_at": 1699900000000,
          "finished_at": 1699900001000
        }
      }
    ],
    "metadata": {
      "user_id": "user-123",
      "thread_id": "conversation-456",
      "customer_id": "customer-789",
      "labels": ["production", "premium"],
      "any_custom_field": "any value"
    }
  }'
import os
import requests

requests.post(
    "https://app.langwatch.ai/api/collector",
    headers={
        "X-Auth-Token": os.environ["LANGWATCH_API_KEY"],
        "Content-Type": "application/json",
    },
    json={
        "trace_id": "trace-123",
        "spans": [
            {
                "type": "llm",
                "span_id": "span-456",
                "name": "chat-completion",
                "model": "gpt-4",
                "input": {"type": "text", "value": "Hello"},
                "output": {"type": "text", "value": "Hi there!"},
                "timestamps": {
                    "started_at": 1699900000000,
                    "finished_at": 1699900001000,
                },
            }
        ],
        "metadata": {
            "user_id": "user-123",
            "thread_id": "conversation-456",
            "customer_id": "customer-789",
            "labels": ["production", "premium"],
            "any_custom_field": "any value",
        },
    },
)
const response = await fetch("https://app.langwatch.ai/api/collector", {
  method: "POST",
  headers: {
    "X-Auth-Token": process.env.LANGWATCH_API_KEY!,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    trace_id: "trace-123",
    spans: [
      {
        type: "llm",
        span_id: "span-456",
        name: "chat-completion",
        model: "gpt-4",
        input: { type: "text", value: "Hello" },
        output: { type: "text", value: "Hi there!" },
        timestamps: {
          started_at: 1699900000000,
          finished_at: 1699900001000,
        },
      },
    ],
    metadata: {
      user_id: "user-123",
      thread_id: "conversation-456",
      customer_id: "customer-789",
      labels: ["production", "premium"],
      any_custom_field: "any value",
    },
  }),
});

Reserved vs Custom Fields

In the REST API metadata object:
FieldTypeDescription
user_idstringEnd user identifier
thread_idstringConversation/session ID
customer_idstringYour tenant/customer ID
labelsstring[]Categorization tags
other keysanyStored as custom metadata

Timings

Beyond metadata, spans carry timing information that powers the latency views: the Duration column, the Time to First Token (TTFT) column, and the latency analytics.
FieldWhereDescription
timestamps.started_atSpanWhen the operation started (epoch ms). Set automatically by the SDKs
timestamps.first_token_atSpanWhen the first streamed token arrived (epoch ms). Powers TTFT
timestamps.finished_atSpanWhen the operation finished (epoch ms). Set automatically by the SDKs
gen_ai.server.time_to_first_tokenOTEL attributeTTFT as a duration in milliseconds, alternative to first_token_at
Most streaming instrumentations report TTFT automatically. To set it manually, or to understand how it rolls up to the trace level, see the dedicated tutorials:

Best Practices

Always set user_id

Required for user-level analytics and filtering by specific users.

Use thread_id for conversations

Groups related messages together. Essential for chatbots and multi-turn interactions.

Labels for categorization

Use consistent labels like production, staging, support for filtering.

Custom metadata for context

Add any relevant context: feature flags, A/B variants, request sources.

What You Get

Once traces include metadata:
  • Filter by user: Find all traces for a specific user
  • View conversations: See all messages in a thread grouped together
  • Filter by labels: Quickly filter to specific categories
  • Search custom fields: Find traces by any custom metadata value
  • User analytics: View per-user metrics and patterns