Judgment Labs Logo

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.

Have your coding agent use JQL

Copy this setup prompt into your coding agent. It helps you choose where to install the public Judgeval SDK, connects it to your organization and project, and runs a read-only smoke test.

Quick start

Install Judgeval

uv add judgeval
pip install judgeval
npm install judgeval
yarn add judgeval
pnpm add judgeval
bun add judgeval

Configure the client

Set your Judgment API key and organization ID in the environment:

.env
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.

PartPurposePublic SDK surface
SourceChoose the grain to query.traces(), spans(), or sessions()
ScopeFilter rows and bound time..where(), .last(), .since(), or .between()
ResultSelect rows or construct a pipeline..rows(), .ids(), .count(), .agg(), or .pipe()
ExecutionSend 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)                    # execution
import { 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);       // execution

Python 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:

SourceUse 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:

MethodMeaningExample
.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:

TerminalResult
.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.

StagePurpose
.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.

KindReturns
fieldsFields available for a required source.
modelsModel names observed in project facts.
span_namesSpan names observed in project facts.
judgesCurrent judge names and configuration.
behaviorsBehavior names and values.
citationsSpans cited by a judge decision.
rulesRule configurations.
judge_promptsJudge prompt configurations.
judge_configJudge 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:

FieldTypeDescription
query_idstringIdentifier for the query execution.
rowsarray or nullResult rows, when the operation returns rows.
row_countnumber or nullResult row count, when provided by the operation.
elapsed_msnumberServer execution time in milliseconds.

Presentation responses contain exactly these public fields:

FieldTypeDescription
query_idstringIdentifier for the presentation execution.
presentationAny (Python) / unknown (TypeScript)Presentation metadata returned by the server.
frameAny | None (Python) / unknown | null (TypeScript)Presentation data returned by the server.
elapsed_msnumberServer 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

JQL is rolling out behind an organization feature flag. If JQL has not been enabled for your organization, contact Judgment to request access.

  • The API key authenticates the user.
  • JUDGMENT_ORG_ID selects 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.