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

# Linking Events to Trajectories

> How TraceContext and trace_id correlate telemetry events with uploaded trajectories.

Telemetry events and trajectories are linked by a caller-owned `trace_id`. The SDK creates it
before upload, stamps it on every `TelemetryEvent` you produce in the same trace context, and
stores it on the `Trajectory`. After upload, the backend returns its own `trajectory_id`, which
the SDK can stamp onto the buffered events for you.

## The Mental Model

| ID                | Owner                  | When known    | Purpose                                                                      |
| ----------------- | ---------------------- | ------------- | ---------------------------------------------------------------------------- |
| `trace_id`        | SDK / caller           | Before upload | Correlates telemetry events and trajectories for the same application trace. |
| `trajectory_id`   | Backend                | After upload  | Stable backend identifier for the persisted trajectory row.                  |
| `session_id`      | SDK / caller           | Before upload | Broader grouping across traces; defaults to `trace_id` in `TraceContext`.    |
| `conversation_id` | Caller / source system | Before upload | Source-system conversation identifier — not a correlation key.               |

Use one `trace_id` for the things you want correlated together. Usually that means one
`trace_id` per trajectory. If a single product trace intentionally produces multiple trajectory
records, reusing the same `trace_id` links them and their telemetry into one application trace.

<Note>
  You should rarely need to think about `trajectory_id` directly. The SDK fetches it from the
  upload response and stamps it onto matching events for you — see [Upload Both Together](#upload-both-together).
</Note>

## Recommended Workflow

Start with `tj.start_trace()` when instrumenting a product or agent run. The returned
`TraceContext` owns the `trace_id` and a buffer of `TelemetryEvent` objects.

```python theme={null}
import trajectory_sdk as tj

tj.init(project_id="acme", trajectory_api_key="tj_key_...")

trace = tj.start_trace()

trace.event("tool_call", {"tool": "search"})

trajectory = trace.build_trajectory(
    messages=messages,
    conversation_id="conv-1",
    data_source="prod_agent",
)
```

At this point `trajectory.trace_id` and every event in `trace.events` share the same `trace_id`.

## Upload Both Together

`tj.upload_trace()` is the simplest path when you have a trajectory and its buffered telemetry
events at the same time. It uploads the trajectory first, reads the returned `trajectory_id`,
stamps matching events that share the same `trace_id`, then pushes telemetry — atomically.

```python theme={null}
import trajectory_sdk as tj

tj.init(project_id="acme", trajectory_api_key="tj_key_...")

trace = tj.start_trace()
trace.event("tool_call", {"tool": "search"})

trajectory = trace.build_trajectory(
    messages=messages,
    conversation_id="conv-1",
    data_source="prod_agent",
)

result = tj.upload_trace(trajectory, trace.events, "prod")

upload_result = result["upload"]
push_result = result["push"]
```

The return value is a dict with four keys:

| Key                | Type                   | Description                                                                   |
| ------------------ | ---------------------- | ----------------------------------------------------------------------------- |
| `upload`           | `dict`                 | Raw response from `tj.upload()` (counts, errors, per-trajectory items).       |
| `push`             | `PushResult`           | Counts of pushed / skipped events plus any error messages.                    |
| `events`           | `list[TelemetryEvent]` | The events as actually sent, after `trajectory_id` stamping.                  |
| `unstamped_events` | `list[TelemetryEvent]` | Events that were pushed without a `trajectory_id` because no match was found. |

### Atomic guarantee

If any input trajectory's upload comes back with `status="error"` (or `status="skipped"` with a
non-null `error` — the hash-conflict case), and there are unstamped events tied to that
`trace_id`, `upload_trace` raises `RuntimeError` and **pushes no events**. Partners who want
best-effort pushes should call `upload` and `push_events` separately.

## Upload Events Before Trajectory

Events can be buffered locally before the trajectory exists. Once `upload` returns the backend
`trajectory_id`, stamp it onto the buffered events before pushing.

```python theme={null}
import dataclasses
import trajectory_sdk as tj

tj.init(project_id="acme", trajectory_api_key="tj_key_...")

trace = tj.start_trace()
trace.event("tool_call", {"tool": "search"})
trace.event("tool_result", {"ok": True})

trajectory = trace.build_trajectory(
    messages=messages,
    conversation_id="conv-1",
    data_source="prod_agent",
)

result = tj.upload(trajectory, dataset="prod")
trajectory_id = result["trajectories"][0]["trajectory_id"]

events = [
    dataclasses.replace(event, trajectory_id=trajectory_id)
    for event in trace.events
]

tj.push_events(events)
```

## Upload Trajectory Before Events

If the trajectory is uploaded first, read the returned `trajectory_id` and pass it when
creating later events.

```python theme={null}
import trajectory_sdk as tj

tj.init(project_id="acme", trajectory_api_key="tj_key_...")

trace = tj.start_trace()

trajectory = trace.build_trajectory(
    messages=messages,
    conversation_id="conv-1",
    data_source="prod_agent",
)

result = tj.upload(trajectory, dataset="prod")
trajectory_id = result["trajectories"][0]["trajectory_id"]

trace.event("tool_call", {"tool": "search"}, trajectory_id=trajectory_id)
trace.event("tool_result", {"ok": True}, trajectory_id=trajectory_id)

tj.push_events(trace.events)
```

## Bring Your Own Trace ID

`start_trace()` accepts an explicit `trace_id` when you want to use an ID that already exists
in your system (a request ID, an OpenTelemetry trace ID, your own UUID, etc.):

```python theme={null}
trace = tj.start_trace("req_01HXYZ123")
```

If you omit it, the SDK generates a UUID4 hex string.

## Related

<CardGroup cols={2}>
  <Card title="Telemetry Events" icon="signal-stream" href="/sdk/telemetry-events">
    What `TelemetryEvent` is, how to construct one, and how `push_events` works.
  </Card>

  <Card title="Bring Your Own Data" icon="file-import" href="/sdk/bring-your-own-data">
    Build trajectories from CSV / JSONL / OpenAI / Anthropic / Vercel formats.
  </Card>

  <Card title="API Reference" icon="square-terminal" href="/sdk/api-reference">
    Full signatures for `start_trace`, `upload_trace`, `push_events`, and friends.
  </Card>

  <Card title="Core Concepts" icon="lightbulb" href="/concepts">
    Trajectories, Steps, Turns, and Messages.
  </Card>
</CardGroup>
