Skip to main content

Why

By default POST /api/traces/search returns the full trace shape. For an ETL pipeline that only needs a handful of fields across thousands of traces, that is both more data than required and forces a second call per trace to read event-level feedback. The projection DSL lets a caller declare the columns of interest up front; the server returns exactly that shape — with nested events, annotations, and evaluations joined server-side — in one paginated response. The feature is fully additive: a request without from/select behaves exactly as before.

The contract

Two optional fields on the search request body:

How paths map to the response

Paths group by their root, so the response mirrors the selection:
  • Scalar fields (trace_id, started_at, input, …) stay at the top level of each row.
  • metadata.* nests under a metadata object.
  • metrics.* nests under a metrics object.
  • events.*, annotations.*, evaluations.* return as nested arrays — one row per trace, with each child record projected to the requested sub-fields.
A request for the select above yields rows shaped like:

The schema response field

When select is present, the response envelope gains a top-level schema object listing the resolved columns — their dotted path, value type, and whether they belong to a nested collection. Use it to pre-allocate a typed reader (pandas, a Parquet writer, a warehouse table) without inferring types from the data:

Field catalog

Every selectable path is listed below. A path outside this catalog is rejected at request time with HTTP 400 and the offending path named in the error, so you can correct the whole select in one round-trip.

Trace scalars

metadata.<key>

Any metadata key (e.g. metadata.user_id, metadata.customer_id, metadata.thread_id, or your own custom keys). Returned under a metadata object.

metrics.<key>

evaluations.<field> (nested array)

events.<field> (nested array)

annotations.<field> (nested array)

Annotations are manual reviews captured in the LangWatch UI (thumbs, scores, comments). They live alongside SDK-emitted events and are joined into the same response — no separate annotations call needed.

Choosing the date axis

By default the startDate/endDate window filters traces by when they occurred. For incremental ETL you usually want everything changed since your last pull — a trace can occur weeks before it gains a later evaluation or annotation. Set dateField to switch the axis:
Delivery contract on the updated axis: at-least-once across pulls, at-most-once within a single scroll. A trace modified again while you are paging moves past the cursor and is not re-delivered in the current scroll — but its new updated_at lands in your next pull’s window (which starts at your last watermark), so no change is ever lost across pulls. A CDC loop that advances its watermark to the previous pull’s start time gets every change.
The updated watermark advances on every ClickHouse-side change to a trace — evaluations, metadata updates, spans, and annotation creates and deletes. One gap today: an in-place edit to an existing annotation (e.g. changing a score from 3 to 5) writes Postgres only and does not advance the trace’s ClickHouse updated_at, so dateField: "updated" does not pick it up until the trace changes again. New and removed annotations are caught. See #4736 for the cross-store watermark follow-up.

Performance

The projection drives column selection, so asking for only lightweight fields keeps the query lightweight. In particular, the heavy captured input/output columns are only read when you select them — omit them and a wide window stays fast. Each page is capped at 1000 rows; keep following pagination.scrollId until it is absent to sweep a full window.

Backwards compatibility

A request with neither from nor select returns the existing full-trace shape and no schema field. Existing integrations are unaffected.