Query with JQL
Retrieve, aggregate, and present your data quickly with Judgment's expressive DSL
JQL is Judgment's query builder for live and offline traces, spans, and sessions. You write queries in Python or TypeScript by chaining builder methods, and the Judgeval SDK validates and runs each one against your project. Use JQL to:
- Filter and search live or offline traces and spans, plus sessions — for example, find traces slower than 10 seconds or spans that errored.
- Aggregate and summarize your data — for example, total cost by model or error rate over the last 7 days.
- Build tables and charts from results for dashboards and reports.
- Query from a coding agent so it can pull your project data on your behalf.
If you know SQL, the shape is familiar: the source is your FROM, .where()
is WHERE, .summarize(...) with by is GROUP BY plus aggregates,
.sort() is ORDER BY, and .take() is LIMIT.
Quick start
Configure the client
Select a project when you create the client:
from judgeval import Judgeval
client = Judgeval(project_name="my-project")import { Judgeval } from "judgeval";
const client = await Judgeval.create({
projectName: "my-project",
});Your API key and JUDGMENT_ORG_ID identify you and your organization, and the
project_name you pass to the client selects the project to query. You never
put an organization ID, project ID, credentials, trace IDs, or session IDs inside
a query itself. Organization and project scope come from the client; optional
trace or session scope is a method option when the query runs.
Run a query
This query returns the 20 most recent traces slower than 10 seconds — a filter that works for any project:
from judgeval import Judgeval
from judgeval.jql import gte, traces
client = Judgeval(project_name="my-project")
# duration is in nanoseconds, so 10 seconds is 10_000_000_000
query = traces().where(gte("duration", 10_000_000_000)).last("7d").recent(20)
result = client.query(query)import { Judgeval } from "judgeval";
import { gte, traces } from "judgeval/jql";
const client = await Judgeval.create({
projectName: "my-project",
});
// duration is in nanoseconds, so 10 seconds is 10_000_000_000
const query = traces().where(gte("duration", 10_000_000_000)).last("7d").recent(20);
const result = await client.query(query);Narrow a query to traces or sessions
Pass trace or session scope outside the JQL query object when you want every part of a query—including span rows—to stay within specific traces. The two options are mutually exclusive:
from judgeval.jql import spans
trace_result = client.query(
spans().rows(),
trace_ids=["trace-123"],
)
session_result = client.query(
spans().rows(),
session_ids=["session-123"],
)import { spans } from "judgeval/jql";
const traceResult = await client.query(spans().rows(), {
traceIds: ["trace-123"],
});
const sessionResult = await client.query(spans().rows(), {
sessionIds: ["session-123"],
});Trace IDs narrow the query directly. Judgment resolves session IDs within the
authenticated organization and project, then narrows the query to their traces.
If none resolve, the request fails instead of falling back to the whole project.
Both options work with present and discover.
Query offline traces
Use offline_traces() and offline_spans() to query data captured by an
OfflineTracer. Offline data is stored separately from live monitoring data, so
the live traces() and spans() sources do not return it:
from judgeval.jql import offline_spans, offline_traces
offline_trace_result = client.query(
offline_traces().last("7d").rows(limit=20)
)
offline_span_result = client.query(
offline_spans().last("7d").rows(limit=20)
)import { offline_spans, offline_traces } from "judgeval/jql";
const offlineTraceResult = await client.query(
offline_traces().last("7d").rows({ limit: 20 }),
);
const offlineSpanResult = await client.query(
offline_spans().last("7d").rows({ limit: 20 }),
);Offline sources support the same direct result shapes as their live twins, such as rows, IDs, counts, aggregates, and trends. They do not expose sessions, judge scores, or cross-grain relations.
Query structure
Every JQL query reads top to bottom as a builder chain: pick a source, narrow it down, describe the result you want, then run it. This query does all four — it finds error spans from the last 7 days and returns the 10 costliest models:
from judgeval.jql import agg_expr, spans, status
query = (
spans() # source
.where(status("error")) # filter
.last("7d") # time bound
.pipe() # result pipeline
.summarize(
{"total_cost": agg_expr("sum", "cost")},
by="model",
)
.sort("total_cost desc")
.take(10)
)
result = client.query(query) # executionimport { aggExpr, spans, status } from "judgeval/jql";
const query = spans() // source
.where(status("error")) // filter
.last("7d") // time bound
.pipe() // result pipeline
.summarize("model", {
total_cost: aggExpr({ func: "sum", field: "cost" }),
})
.sort("total_cost desc")
.take(10);
const result = await client.query(query); // executionEach step in the chain maps to a group of builder methods:
| Step | What it does | Builder methods |
|---|---|---|
| Source | Choose the grain to query — the kind of row you get back. | traces(), spans(), sessions(), offline_traces(), or offline_spans() |
| Scope | Filter rows and bound the time range. | .where(), .last(), .since(), or .between() |
| Result | Select rows directly or build a multi-stage pipeline. | .rows(), .ids(), .count(), .agg(), or .pipe() |
| Execution | Send the query to Judgment and get results back. | client.query(), client.present(), or client.discover() |
Python and TypeScript use different method spellings where required, but emit the same canonical JSON. For example, a query that returns trace IDs for a session emits:
{
"op": "query",
"source": "traces",
"filter": { "op": "eq", "field": "session", "value": "session-123" },
"select": { "op": "ids" }
}Sources and fields
Choose the source that matches the grain of the result you need:
| Source | Use it for |
|---|---|
traces() | Trace-level status, sessions, duration, and trace attributes. |
spans() | Individual operations, model calls, span names, cost, and span attributes. |
sessions() | Groups of related traces and session-level analysis. |
offline_traces() | Trace-level data captured by an OfflineTracer. |
offline_spans() | Individual spans captured by an OfflineTracer. |
Field availability depends on the source. Discover valid fields before building a dynamic query or when you are unsure which grain contains a value:
trace_fields = client.discover("fields", source="traces")
span_fields = client.discover("fields", source="spans")const traceFields = await client.discover("fields", {
source: "traces",
});
const spanFields = await client.discover("fields", {
source: "spans",
});Do not silently change sources when a field is unavailable: changing grain can change the meaning and cardinality of the result.
Filters and time bounds
Add filters with .where(). Calling .where() more than once combines the
conditions with AND. To express other logic, compose filters explicitly with
these combinators:
| Combinator | Matches when | Logic | TypeScript | Python |
|---|---|---|---|---|
all | Every condition is true. | AND | all(...) | all_(...) |
any | At least one condition is true. | OR | any(...) | any_(...) |
not | The condition is false. | NOT | not(...) | not_(...) |
all, any, and not are reserved words in Python, so the Python SDK spells
them all_, any_, and not_.
from judgeval.jql import all_, gte, status, traces
slow_errors = (
traces()
.where(
all_(
status("error"),
gte("duration", 1_000_000_000),
)
)
.last("7d")
.count()
)import { all, gte, status, traces } from "judgeval/jql";
const slowErrors = traces()
.where(
all(
status("error"),
gte("duration", 1_000_000_000),
),
)
.last("7d")
.count();Bound production queries with one of these time methods before selecting a result, so a query does not scan unnecessary project history:
| Method | Meaning | Example |
|---|---|---|
.last(window) | A trailing duration. | .last("7d") |
.since(timestamp) | Everything after a timestamp. | .since("2026-01-01T00:00:00Z") |
.between(start, end) | A closed time range. | .between(start, end) |
Select terminals
A non-pipeline query ends with exactly one select terminal:
| Terminal | Result |
|---|---|
.rows(...) | Matching rows with optional explicit fields and a limit. |
.ids() | Matching trace, span, or session IDs. |
.count(by?) | A total count or counts grouped by a field. |
.recent(n) | The n most recent rows. |
.top(n, by) | The n largest rows by a numeric field. |
.ranked(...) | Positional rows globally or within a group. |
.agg(...) | A scalar aggregate such as sum, average, or quantile. |
.trend(...) | Count or rate values over time buckets. |
The following examples select explicit rows, count errors, and aggregate cost:
from judgeval.jql import spans, status
rows = spans().last("7d").rows(
fields=["span_id", "trace_id", "span_name", "model", "cost"],
limit=100,
)
error_count = spans().where(status("error")).last("7d").count()
total_cost = spans().last("7d").agg("sum", "cost")import { spans, status } from "judgeval/jql";
const rows = spans().last("7d").rows({
fields: ["span_id", "trace_id", "span_name", "model", "cost"],
limit: 100,
});
const errorCount = spans()
.where(status("error"))
.last("7d")
.count();
const totalCost = spans()
.last("7d")
.agg({ func: "sum", field: "cost" });Pass a completed builder to client.query(...) to execute it.
Aggregations and pipelines
Use .pipe() instead of a select terminal when the result needs multiple
ordered transformations.
| Stage | Purpose |
|---|---|
.where(...) | Filter the current rows, including after aggregation. |
.pick(...) | Keep positional rows within groups. |
.derive(...) | Add computed columns without changing row count. |
.summarize(...) | Collapse rows into grouped aggregate results. |
.sort(...) | Order the current rows. |
.take(...) | Keep a bounded number of rows, optionally after an offset. |
Stages execute in method-call order. This example summarizes cost by model, sorts the result, and keeps ten rows:
from judgeval.jql import agg_expr, spans
spend_by_model = (
spans()
.last("7d")
.pipe()
.summarize(
{"total_cost": agg_expr("sum", "cost")},
by="model",
)
.sort("total_cost desc")
.take(10)
)
result = client.query(spend_by_model)import { aggExpr, spans } from "judgeval/jql";
const spendByModel = spans()
.last("7d")
.pipe()
.summarize("model", {
total_cost: aggExpr({ func: "sum", field: "cost" }),
})
.sort("total_cost desc")
.take(10);
const result = await client.query(spendByModel);Presentation specifications
Wrap a query or pipeline with .table() or .chart() to describe a
presentation, then execute it with client.present(...).
client.present(...) returns normalized presentation metadata and a typed data
frame. It does not render HTML, an image, or a UI component. Render the returned
data with your application's table or chart library.
from judgeval.jql import agg_expr, spans
spend_by_model = (
spans()
.last("7d")
.pipe()
.summarize(
{"total_cost": agg_expr("sum", "cost")},
by="model",
)
.sort("total_cost desc")
.take(10)
)
table_result = client.present(
spend_by_model.table(
title="Spend by model",
columns=[
{"key": "model", "label": "Model"},
{
"key": "total_cost",
"label": "Cost",
"format": "currency_usd",
},
],
)
)
chart_result = client.present(
spend_by_model.chart(
chart_type="bar",
title="Spend by model",
x_axis={"key": "model", "label": "Model"},
y_axis={
"key": "total_cost",
"label": "Cost",
"format": "currency_usd",
},
)
)import { aggExpr, spans } from "judgeval/jql";
const spendByModel = spans()
.last("7d")
.pipe()
.summarize("model", {
total_cost: aggExpr({ func: "sum", field: "cost" }),
})
.sort("total_cost desc")
.take(10);
const tableResult = await client.present(
spendByModel.table({
title: "Spend by model",
columns: [
{ key: "model", label: "Model" },
{
key: "total_cost",
label: "Cost",
format: "currency_usd",
},
],
}),
);
const chartResult = await client.present(
spendByModel.chart({
chart_type: "bar",
title: "Spend by model",
x_axis: { key: "model", label: "Model" },
y_axis: {
key: "total_cost",
label: "Cost",
format: "currency_usd",
},
}),
);Discovery
Discovery returns project-scoped catalog values that help construct valid
queries. Only use kinds present in the generated DiscoveryKind contract.
| Kind | Returns |
|---|---|
fields | Fields available for a required source. |
models | Model names observed in project facts. |
span_names | Span names observed in project facts. |
judges | Current judge names and configuration. |
behaviors | Behavior names and values. |
citations | Spans cited by a judge decision. |
rules | Rule configurations. |
judge_prompts | Judge prompt configurations. |
judge_config | Judge configuration details. |
fields = client.discover("fields", source="traces")
models = client.discover("models", time={"last": "7d"}, limit=100)
judges = client.discover("judges", limit=100)const fields = await client.discover("fields", {
source: "traces",
});
const models = await client.discover("models", {
time: { last: "7d" },
limit: 100,
});
const judges = await client.discover("judges", { limit: 100 });Responses and errors
Query and discovery responses contain exactly these public fields:
| Field | Type | Description |
|---|---|---|
query_id | string | Identifier for the query execution. |
rows | array or null | Result rows, when the operation returns rows. |
row_count | number or null | Result row count, when provided by the operation. |
elapsed_ms | number | Server execution time in milliseconds. |
Presentation responses contain exactly these public fields:
| Field | Type | Description |
|---|---|---|
query_id | string | Identifier for the presentation execution. |
presentation | Any (Python) / unknown (TypeScript) | Presentation metadata returned by the server. |
frame | Any | None (Python) / unknown | null (TypeScript) | Presentation data returned by the server. |
elapsed_ms | number | Server execution time in milliseconds. |
Python responses are typed mappings, so access fields as result["rows"].
TypeScript responses use normal property access, such as result.rows.
TypeScript raises JudgevalAPIError; Python raises JudgmentAPIError. Both
preserve the public error code, an actionable hint when supplied, and a numeric
retry-after value when available.
from judgeval.exceptions import JudgmentAPIError
try:
result = client.query(traces().last("7d").count())
except JudgmentAPIError as error:
print(error.code)
print(error.hint)
if error.retry_after_seconds is not None:
print(f"Retry after {error.retry_after_seconds} seconds")import { JudgevalAPIError } from "judgeval";
try {
const result = await client.query(traces().last("7d").count());
} catch (error) {
if (!(error instanceof JudgevalAPIError)) throw error;
console.error(error.code);
console.error(error.hint);
if (error.retryAfterSeconds !== undefined) {
console.error(`Retry after ${error.retryAfterSeconds} seconds`);
}
}Do not match undocumented error-code strings or assume a retry-after value is always present.
Tenant safety
- The API key authenticates the user.
JUDGMENT_ORG_IDselects an organization that the authenticated user must belong to.- The configured project must belong to that organization.
- JQL query payloads use supported query operators and values. They must not contain organization IDs, project IDs, API keys, or raw SQL.
Keep credentials in environment variables or your secret manager. Never place them in a builder, log them, or commit them with query examples.
API reference
Use the runtime-specific SDK reference when you need the complete function inventory, accepted parameters, return fields, or an example for a particular builder.