Query with JQL
Retrieve, aggregate, and present your data quickly with Judgment's expressive DSL
JQL is Judgment's structured query language for traces, spans, and sessions. It provides composable builders for expressive queries, from simple filters to aggregations, pipelines, tables, and charts. The authenticated Judgeval API validates and executes each query for the project configured on the client.
Quick start
Install Judgeval
uv add judgevalpip install judgevalnpm install judgevalyarn add judgevalpnpm add judgevalbun add judgevalConfigure the client
Set your Judgment API key and organization ID in the environment:
JUDGMENT_API_KEY="your_api_key"
JUDGMENT_ORG_ID="your_organization_id"Then 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",
});The API key authenticates the request. Organization and project scope come from the authenticated SDK context, not from JQL JSON.
Run a query
This query returns the IDs of traces associated with a session:
from judgeval import Judgeval
from judgeval.jql import eq, traces
client = Judgeval(project_name="my-project")
query = traces().where(eq("session", "session-123")).ids()
result = client.query(query)import { Judgeval } from "judgeval";
import { eq, traces } from "judgeval/jql";
const client = await Judgeval.create({
projectName: "my-project",
});
const query = traces().where(eq("session", "session-123")).ids();
const result = await client.query(query);Query structure
A JQL query has four parts: choose a source, narrow its scope, describe the result, and execute it.
| Part | Purpose | Public SDK surface |
|---|---|---|
| Source | Choose the grain to query. | traces(), spans(), or sessions() |
| Scope | Filter rows and bound time. | .where(), .last(), .since(), or .between() |
| Result | Select rows or construct a pipeline. | .rows(), .ids(), .count(), .agg(), or .pipe() |
| Execution | Send the structured query through Judgeval. | client.query(), client.present(), or client.discover() |
The order is visible directly in a builder chain:
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); // executionPython and TypeScript use different method spelling where required, but emit the same canonical JSON. For example, the quick-start query emits:
{
"op": "query",
"source": "traces",
"filter": { "op": "eq", "field": "session", "value": "session-123" },
"select": { "op": "ids" }
}The server performs final validation. A builder producing JSON does not prove that every referenced field is available for the selected source and project.
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. |
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(). Multiple calls are combined, or you can compose
filters explicitly with all, any, and not. Python uses the safe spellings
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();Use one of these time bounds before selecting a result:
| 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) |
Bound production queries whenever possible so they do not scan unnecessary project history.
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);Tables and charts
Wrap a query or pipeline with .table() or .chart(), then execute the
presentation with client.present(...).
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 and availability
- 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.
If the API reports that public JQL is unavailable for an otherwise valid request, contact Judgment to enable it for the organization. Do not attempt to bypass the feature restriction.