Skip to main content

Which language answers which question

The trace filter finds traces. It is the language the Trace Explorer’s search bar speaks, it returns whole traces with their spans and evaluations, and it reaches attribute keys, span events and evaluator verdicts. Analytics SQL (LangWatchQL) answers questions about many traces at once: a count, a rate, a percentile, any of them grouped or over time, and joins across views. It returns columns, not traces.

One reference for both

GET /api/v1/query/reference describes both languages in one payload: the SQL views and columns, the filter fields and syntax, worked examples in each, and the decision table above. Every example goes through the real validator and the real translator on every build, so a published example is never one the API would refuse on sight. Whether your key can run a given example is a separate question, and each example answers it. available is false when a column it reads is withheld from you, and requires says what it would take.
The reference carries no values from your project. Values change under you and reading them all is expensive, so they come from a separate call.
trace fields lists what you can filter on. trace facets says what those fields hold in your project right now, which is the question to ask before guessing how a value is spelled.

Filtering traces

Send the filter string as filter on POST /api/traces/search, or pass --filter to the CLI. It combines with the free-text query and the other options, so every condition you send has to hold.
Three namespaces reach arbitrary OpenTelemetry attributes: trace.attribute.<key>, span.attribute.<key> and event.attribute.<key>. The older attribute.<key> and event.<key> spellings still work. A filter naming a field the language does not have answers 422 and names both the offending field and the fields that exist.

Running analytics SQL

A key that reaches several projects must name the projects to read with --projects (the projects field over REST and in run_query). Without them the run is refused with projects_required (422): “Name the projects to query: send projects, a list of project ids, or “all"". A project API key, or any key that can read exactly one project, reads that project and needs none. Send "all" (the string, in any case; --projects all on the CLI) to read every project the key can read. It must stand alone: a list that holds it is refused with a 422: “all” stands alone; send it by itself, or list project ids. Only projects it holds analytics:view on, in its own organization, are read. params.dashboard_context_projects lists the projects that were read, in id order. If the key reads no project and it sends "all" or names no project, the run is refused with projects_unreadable (422); ask an admin to grant it analytics:view on one. A project it names and cannot read is refused with project_not_found (404). A member who reads none gets project_not_found (404). If it reads more than 10,000, the run is refused with a 422 asking you to name the projects. A widget does not store "all"; name its project ids. Every REST answer carries params: each placeholder the statement declares, by its SQL name, with the value it ran with (for example dashboard_context_sources and dashboard_context_projects), plus the period it followed. The CLI returns the same params and the diagnostics beside the rows in -o json and --format json (one object with rows, params and diagnostics). For CSV and JSON lines it prints params as one JSON line on stderr, and the table format adds a line under the table. The placeholder does not filter by itself: write a condition such as WHERE Source IN {dashboard_context_sources:Array(String)}. A statement that never uses {dashboard_context_sources:Array(String)} counts every origin. When the request named sources, the answer says so with a SOURCES_IGNORED diagnostic; a request that named none gets no note. When the period is too long for the step asked, the query runs at a coarser step and the answer carries a GRANULARITY_COARSENED diagnostic. --sources (sources) sets the origins that parameter is bound to; the default is application, which applies only to a statement that uses the parameter. The statement runs exactly as submitted. Filter on the dataset’s time column, as every example does: without that predicate the read touches every partition your project has, including the cold ones. --sql-file reads the statement from a file, --param key=value binds a parameter the statement declares, and --start with --end fills the reserved period parameters.

Exporting rows

--format jsonl writes one JSON object per row and parses JSON-typed columns back into real values, which is the shape a training-data reader expects. --format csv writes a header row with RFC 4180 escaping. --out writes to a file.
The query endpoint has no cursor of its own, so a result larger than one response pages through a predicate you write. Declare the two cursor parameters, order by the same two columns, and --page-by keyset rebinds them between pages without changing a character of your statement.

From an MCP client

discover_schema answers the same two descriptions, as markdown: category filters for the trace filter, lwql for the analytics SQL, all for everything. run_query runs a statement and renders up to 50 rows as a table, saying how many the statement really returned. search_traces takes the same filter string the REST endpoint does.

Permissions

Both query doors need analytics:view; the trace search and the facets endpoint need traces:view. A column your key cannot read is listed with available: false and names the permission it needs, rather than being hidden, so you can see what a wider key would reach.
Last modified on October 30, 2026