Judgment Labs Logo

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.

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

Have your coding agent use JQL

Copy this prompt into your coding agent to install the SDK and start querying.

Quick start

Configure the client

Make sure you have Judgeval installed and your JUDGMENT_API_KEY and JUDGMENT_ORG_ID set in your environment. See Get started for installation and credential setup.

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)                    # 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

Each step in the chain maps to a group of builder methods:

StepWhat it doesBuilder methods
SourceChoose the grain to query — the kind of row you get back.traces(), spans(), sessions(), offline_traces(), or offline_spans()
ScopeFilter rows and bound the time range..where(), .last(), .since(), or .between()
ResultSelect rows directly or build a multi-stage pipeline..rows(), .ids(), .count(), .agg(), or .pipe()
ExecutionSend 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:

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

CombinatorMatches whenLogicTypeScriptPython
allEvery condition is true.ANDall(...)all_(...)
anyAt least one condition is true.ORany(...)any_(...)
notThe condition is false.NOTnot(...)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:

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)

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);

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.

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

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

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.