Skip to main content
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

IDOwnerWhen knownPurpose
trace_idSDK / callerBefore uploadCorrelates telemetry events and trajectories for the same application trace.
trajectory_idBackendAfter uploadStable backend identifier for the persisted trajectory row.
session_idSDK / callerBefore uploadBroader grouping across traces; defaults to trace_id in TraceContext.
conversation_idCaller / source systemBefore uploadSource-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.
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.
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.
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.
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:
KeyTypeDescription
uploaddictRaw response from tj.upload() (counts, errors, per-trajectory items).
pushPushResultCounts of pushed / skipped events plus any error messages.
eventslist[TelemetryEvent]The events as actually sent, after trajectory_id stamping.
unstamped_eventslist[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.
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.
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.):
trace = tj.start_trace("req_01HXYZ123")
If you omit it, the SDK generates a UUID4 hex string.

Telemetry Events

What TelemetryEvent is, how to construct one, and how push_events works.

Bring Your Own Data

Build trajectories from CSV / JSONL / OpenAI / Anthropic / Vercel formats.

API Reference

Full signatures for start_trace, upload_trace, push_events, and friends.

Core Concepts

Trajectories, Steps, Turns, and Messages.