Skip to main content

Where does the statement come from

Every run executes one LangWatchQL statement. Send a target and your questions instead of a statement and LangWatch writes one for you, stores it on the run, and run prints it under a Statement heading. Take that statement when you need more than the shorthand covers. Edit it and submit it back with --sql, or run it through the query endpoint to see the rows before you judge them. The statement takes two parameters, start_at and end_at, so supply both when you run it again:
run prints these two values beside the statement. Change them to move the window.

What each target expands into

Every statement projects TraceId, whatever other key columns the target has, and one column per question. The window is bound as two instants rather than written as a relative expression, because a run executes its statement several times and a moving window would page a selection that was never counted.

traces

threads

One row per conversation, addressed by the conversation’s last trace. A trace carrying no conversation id is left out, so a project that does not set one should ask for traces instead. conversation carries no budget of its own, so an ordinary conversation reaches the judge whole. See The token budget for what happens to one that does not fit, and for how to put a budget back.

llm-spans

The token budget

traces and llm-spans are written with a budget of 8,000 tokens, which is the default the function catalog documents. A typical product trace renders to well under it; a coding session renders to many times it. threads is written without one. A conversation is the thing people most often want read end to end, and cutting it at 8,000 tokens to save a fraction of a cent is the wrong trade, so the only ceiling it meets is the judge’s own state: about 31,000 tokens once your questions are in. Expect a long conversation to cost more than a bounded one would, up to roughly four times on the longest threads. Read the estimate before the run: it prices the rows as they actually are. A conversation past that ceiling is still judged. It is cut to fit and the row comes back marked truncated, and the cut keeps the opening and the close and writes a marker naming how many turns went from the middle, so a question about how a conversation ended reads the ending. To set the budget yourself rather than take the judge’s, write the statement with conversation_bounded, which cuts the same way at the size you name:
A long list of questions leaves less room, and a budget written into a statement is then capped at whatever the questions leave.

What a shorthand filter can ask for

--filter takes the same syntax as the trace explorer’s search bar. A target supports the fields of the trace row itself: AND, OR, NOT and parentheses work as they do in the explorer, and so do * wildcards on model and the range spellings cost:>0.01 and duration:[100 TO 500]. Every other field the explorer filters on reaches outside the trace row, and a shorthand refuses it by name rather than dropping the condition. A dropped condition would charge you for judging rows you meant to exclude. You can filter on status:error, because the error flag is a column of the trace row. status:ok and status:warning read the evaluation rows instead, so ask for those with --sql. Ask any of those with a statement instead, using --sql: LangWatchQL reaches every table, and the trace filter is only a shortcut for the common case. When a field is refused you get back the field you wrote and the fields you can use instead.

Where does the filter go

Over traces, LangWatch puts your filter in the statement’s own WHERE clause. Over threads and llm-spans it goes into a subquery on the trace view, because the trace’s attributes are not columns of the metrics view or the span view. You get a conversation when any of its traces matches, and a model call when its trace matches. The same time bounds apply inside each subquery, so you never pay for rows outside your window.

Sending a statement instead

A statement needs two things to be a run: it has to project TraceId, and it has to project at least one eval function column. Everything else is yours.
Send either sql or target, not both and not neither. Use --param only with your own statement: a target writes its own parameters, so you have none left to fill.
Last modified on September 19, 2026