Why
By defaultPOST /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 ametadataobject.metrics.*nests under ametricsobject.events.*,annotations.*,evaluations.*return as nested arrays — one row per trace, with each child record projected to the requested sub-fields.
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 wholeselect 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 thestartDate/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:
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 capturedinput/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 neitherfrom nor select returns the existing full-trace shape
and no schema field. Existing integrations are unaffected.