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

> ## Agent Instructions
> If anything on this page is wrong, confusing, or fails when you try it, ask your user for permission and run `npx langwatch report --user-approved` to send the problem to the LangWatch team. No login is needed and secrets are redacted locally. `npx langwatch report --help` has the details.

# Metadata and Labels

> Add custom metadata, user IDs, conversation threads, and labels to your traces for filtering, analytics, and debugging.

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.

The reference below covers every integration method. For SDK-specific details, see the tutorials linked below.

## Quick Reference

| Concept | OTEL Attribute | REST API | Description |
| - | - | - | - |
| **Thread/Conversation** | `gen_ai.conversation.id` | `metadata.thread_id` | Groups messages in a conversation |
| **User ID** | `langwatch.user.id` | `metadata.user_id` | Identifies the end user |
| **Customer ID** | `langwatch.customer.id` | `metadata.customer_id` | Your platform's customer/tenant |
| **Labels** | `langwatch.labels` | `metadata.labels` | Categorization tags |
| **Custom Metadata** | `metadata` attribute | `metadata.*` | Any additional context |

> [!NOTE]
>
> For OTEL, `gen_ai.conversation.id` follows the [OpenTelemetry GenAI semantic
> conventions](https://opentelemetry.io/docs/specs/semconv/gen-ai/). The legacy
> `langwatch.thread.id` attribute is also supported.

## SDK Examples

For detailed SDK-specific tutorials, see:

* **TypeScript:** [Capturing Metadata](https://langwatch.ai/docs/integration/typescript/tutorials/capturing-metadata.md) · [Tracking Conversations](https://langwatch.ai/docs/integration/typescript/tutorials/tracking-conversations.md) · [Full example](https://github.com/langwatch/langwatch/tree/main/sdks/typescript/examples/metadata)
* **Python:** [Capturing Metadata](https://langwatch.ai/docs/integration/python/tutorials/capturing-metadata.md) · [Tracking Conversations](https://langwatch.ai/docs/integration/python/tutorials/tracking-conversations.md) · [Full example](https://github.com/langwatch/langwatch/blob/main/sdks/python/examples/metadata_example.py)
* **Go:** [Go SDK guide](https://langwatch.ai/docs/integration/go/guide.md) · [Conversation grouping example](https://github.com/langwatch/langwatch/tree/main/sdks/go/e2e/openai-threads)

```typescript TypeScript SDK theme={null}
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...
  });
}
```

```python Python SDK theme={null}
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...
```

```go Go SDK theme={null}
package main

import (
	"context"

	langwatch "github.com/langwatch/langwatch/sdks/go"
	"go.opentelemetry.io/otel/attribute"
)

func handleUserMessage(ctx context.Context, userID, conversationID string) {
	tracer := langwatch.Tracer("my-service")
	ctx, span := tracer.Start(ctx, "HandleMessage")
	defer span.End()

	span.
		// Thread/conversation grouping (gen_ai.conversation.id)
		SetThreadID(conversationID).
		// User and customer identification
		SetUserID(userID).
		SetCustomerID("tenant-123").
		// Labels for filtering
		SetLabels("production", "premium-user")

	// Custom metadata, hoisted to the trace as metadata.<key> attributes
	span.SetTraceMetadata(
		attribute.StringSlice("feature_flags", []string{"new-ui", "beta-model"}),
		attribute.String("request_source", "mobile-ios"),
	)

	// Your application logic...
}
```

## Raw OpenTelemetry

If you're using vanilla OpenTelemetry without the LangWatch SDK:

```typescript TypeScript theme={null}
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();
});
```

```python Python theme={null}
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 ...
```

```go Go theme={null}
package main

import (
	"context"
	"encoding/json"

	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/attribute"
)

func operation(ctx context.Context) {
	tracer := otel.Tracer("my-service")
	ctx, span := tracer.Start(ctx, "operation")
	defer span.End()

	// OTEL semconv for conversation/thread
	span.SetAttributes(attribute.String("gen_ai.conversation.id", "conv-456"))

	// LangWatch-specific attributes
	span.SetAttributes(
		attribute.String("langwatch.user.id", "user-123"),
		attribute.String("langwatch.customer.id", "customer-789"),
		attribute.String("langwatch.labels", `["urgent", "support"]`),
	)

	// Custom metadata as JSON string
	metadata, _ := json.Marshal(map[string]string{
		"priority":   "high",
		"department": "engineering",
	})
	span.SetAttributes(attribute.String("metadata", string(metadata)))

	// ... your code ...
}
```

**Exporter configuration:**

```typescript TypeScript theme={null}
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}`,
  },
});
```

```python Python theme={null}
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']}"},
)
```

```go Go theme={null}
import (
	"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
)

exporter, err := otlptracehttp.New(ctx,
	otlptracehttp.WithEndpointURL("https://app.langwatch.ai/api/otel/v1/traces"),
	otlptracehttp.WithHeaders(map[string]string{
		"Authorization": "Bearer " + os.Getenv("LANGWATCH_API_KEY"),
	}),
)
```

> [!WARNING]
>
> The OTEL endpoint is `/api/otel/v1/traces` (not `/v1/traces`).

## REST API

Send traces directly via HTTP. See [REST API](https://langwatch.ai/docs/integration/rest-api.md) for full details.

```bash cURL theme={null}
curl -X POST "https://app.langwatch.ai/api/v1/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-6-luna",
        "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"
    }
  }'
```

```python Python theme={null}
import os
import requests

requests.post(
    "https://app.langwatch.ai/api/v1/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-6-luna",
                "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",
        },
    },
)
```

```typescript TypeScript theme={null}
const response = await fetch("https://app.langwatch.ai/api/v1/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-6-luna",
        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",
    },
  }),
});
```

```go Go theme={null}
package main

import (
	"bytes"
	"encoding/json"
	"net/http"
	"os"
)

func sendTrace() error {
	payload := map[string]any{
		"trace_id": "trace-123",
		"spans": []map[string]any{
			{
				"type":    "llm",
				"span_id": "span-456",
				"name":    "chat-completion",
				"model":   "gpt-6-luna",
				"input":   map[string]any{"type": "text", "value": "Hello"},
				"output":  map[string]any{"type": "text", "value": "Hi there!"},
				"timestamps": map[string]any{
					"started_at":  1699900000000,
					"finished_at": 1699900001000,
				},
			},
		},
		"metadata": map[string]any{
			"user_id":          "user-123",
			"thread_id":        "conversation-456",
			"customer_id":      "customer-789",
			"labels":           []string{"production", "premium"},
			"any_custom_field": "any value",
		},
	}

	body, err := json.Marshal(payload)
	if err != nil {
		return err
	}

	req, err := http.NewRequest(http.MethodPost,
		"https://app.langwatch.ai/api/collector", bytes.NewReader(body))
	if err != nil {
		return err
	}
	req.Header.Set("X-Auth-Token", os.Getenv("LANGWATCH_API_KEY"))
	req.Header.Set("Content-Type", "application/json")

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	return nil
}
```

### Reserved vs Custom Fields

In the REST API `metadata` object:

| Field | Type | Description |
| - | - | - |
| `user_id` | string | End user identifier |
| `thread_id` | string | Conversation/session ID |
| `customer_id` | string | Your tenant/customer ID |
| `labels` | string\[] | Categorization tags |
| *other keys* | any | Stored 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.

| Field | Where | Description |
| - | - | - |
| `timestamps.started_at` | Span | When the operation started (epoch ms). Set automatically by the SDKs |
| `timestamps.first_token_at` | Span | When the first streamed token arrived (epoch ms). Powers TTFT |
| `timestamps.finished_at` | Span | When the operation finished (epoch ms). Set automatically by the SDKs |
| `gen_ai.server.time_to_first_token` | OTEL attribute | TTFT 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:

* **Python:** [Tracking Time to First Token](https://langwatch.ai/docs/integration/python/tutorials/tracking-time-to-first-token.md)
* **TypeScript:** [Tracking Time to First Token](https://langwatch.ai/docs/integration/typescript/tutorials/tracking-time-to-first-token.md)

## Best Practices

> [card] Always set user\_id
>
> Required for user-level analytics and filtering by specific users.

> [card] Use thread\_id for conversations
>
> Groups related messages together. Essential for chatbots and multi-turn interactions.

> [card] Labels for categorization
>
> Use consistent labels like `production`, `staging`, `support` for filtering.

> [card] 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


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.