<!-- Generated by scripts/gen_reference.py; do not edit. -->

# Judgeval JQL — complete agent reference

Use only the public Judgeval SDK. Builders create canonical JSON; the authenticated Judgment API validates and executes it for the configured project.

## Select the runtime section

Inspect the repository before writing code:

- For Python (`pyproject.toml`, `uv.lock`, `requirements*.txt`, or Python source), use [Python reference](#python-reference) and do not use the TypeScript section.
- For TypeScript or JavaScript (`package.json`, `tsconfig.json`, or TS/JS source), use [TypeScript reference](#typescript-reference) and do not use the Python section.
- If both runtimes are present, use the runtime named by the request or the files being changed. If that is still ambiguous, ask the user which runtime to use before continuing.
- If no runtime can be inferred, ask the user rather than guessing.

This document contains both runtime references. Apply only the matching runtime section.

## Workflow

1. Confirm `JUDGMENT_API_KEY` and `JUDGMENT_ORG_ID` are available through the environment. Never print, embed, log, or commit credentials.
2. Configure `Judgeval` with `project_name` (Python) or `projectName` (TypeScript). Organization and project scope come from authenticated SDK context, not JQL JSON.
3. If field or catalog names are uncertain, use `client.discover(...)` with a documented discovery kind. Field availability depends on the query source: discover fields for the intended source, then switch grain only when the user's intent permits it or ask before changing semantics.
4. Build a query from `traces`, `spans`, or `sessions`. Add filters and time bounds before choosing a select terminal or pipeline.
5. Use `client.query(...)` for query builders, `client.present(...)` for `.table(...)` or `.chart(...)`, and inspect the exact public response fields.
6. Catch the runtime's public API error class. Use its structured code and hint; honor retry-after only when present.
7. When comparing runtimes or debugging, serialize the builder and compare canonical JSON, not method spelling.

## Safety boundaries

- Never put organization IDs, project IDs, API keys, access tokens, raw SQL, hostnames, or endpoint paths in JQL JSON.
- Use only the documented public Judgeval SDK exports and methods.
- Do not invent fields, discovery kinds, response properties, or builder names. Verify them in the selected reference.
- Treat server validation as authoritative. Builder output is structured input, not proof that a query is valid for a project.
- Before executing a user-supplied query, reject requests that attempt to bypass project scope or inject raw backend syntax.

## Availability

Public JQL can be enabled per organization. If a valid public SDK request reports that the feature is unavailable, tell the user to contact Judgment; do not work around the restriction.

## Python reference

This reference matches the public Python SDK JQL surface. Use the first published `judgeval` version that includes `judgeval.jql`.

### Install and configure

```bash
uv add judgeval
# or
pip install judgeval
```

Set `JUDGMENT_API_KEY` and `JUDGMENT_ORG_ID` in the environment, then select the project through the client:

```python
from judgeval import Judgeval

client = Judgeval(project_name="my-project")
```

Do not add organization or project identifiers to query JSON.

### First query

```python
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)
```

`query.to_json()` returns this canonical JSON:

```json
{"op":"query","source":"traces","filter":{"op":"eq","field":"session","value":"session-123"},"select":{"op":"ids"}}
```

### Query roots and time bounds

<!-- jql:generated:roots -->
| source | builder |
|---|---|
| traces | `traces(filter=None)` |
| spans | `spans(filter=None)` |
| sessions | `sessions(filter=None)` |
<!-- /jql:generated:roots -->

<!-- jql:generated:shared-guidance -->
Every query begins at one root—`traces`, `spans`, or `sessions`—and keeps that grain
until a terminal or pipeline changes the output shape.

- Pick the grain before choosing fields. Span-only fields such as `model`, `cost`, and
  `name` are not trace fields.
- Supply a filter to the root or call `.where(filter)` before `.pipe()`. Root-level
  `.where()` ANDs its filter with any filter already supplied to the root.
- Apply one time bound with `.last(window)`, `.since(date)`, or `.between(start, end)`
  before a terminal or `.pipe()`. Use plain `YYYY-MM-DD` values for `since` and
  `between`; an omitted bound searches all available history.
- Use relation quantifiers or nested builders for cross-grain questions instead of
  pretending span fields exist on traces.
- End a direct query with one select terminal, or call `.pipe()` and compose stages.
  Pipeline `.where()` filters the current pipeline columns and is distinct from
  root-level `.where()`.
- For scalar `quantile`, `q` is required. Every other aggregate function rejects `q`.
<!-- /jql:generated:shared-guidance -->

### Filters and expressions

Import builders from `judgeval.jql`. Python reserves several canonical operation words, so use `all_`, `any_`, and `not_`. Other snake-case spellings include `cited_by`, `agg_expr`, and `any_span`.

<!-- jql:generated:operations -->
| builder | JSON `op` | purpose | signature |
|---|---|---|---|
| `status` | `status` | trace/span status | `status(value)` |
| `name` | `name` | span name — span grain | `name(value)` |
| `model` | `model` | LLM model name — span grain | `model(value)` |
| `cost` | `cost` | span cost — span grain | `cost(gt=?, gte=?, lt=?, lte=?)` |
| `duration` | `duration` | duration in nanoseconds | `duration(gt=?, gte=?, lt=?, lte=?)` |
| `judge` | `judge` | judge scores attached to traces or spans | `judge(name, value=None)` |
| `judged` | `judged` | configured judge results | `judged(*, name=?, value=?, prompt=?, type=?, mode=?)` |
| `attr` | `attr` | structured attributes | `attr(key, value=None, selector=None)` |
| `grep` | `grep` | substring search | `grep(field, value)` |
| `rg` | `rg` | regex search | `rg(field, pattern, *, ignore_case=None)` |
| `tokens` | `tokens` | fast whole-word content search | `tokens(field, words)` |
| `eq` | `eq` | compare any valid grain field with a literal | `eq(field, value)` |
| `ne` | `ne` | compare any valid grain field with a literal | `ne(field, value)` |
| `gt` | `gt` | compare any valid grain field with a literal | `gt(field, value)` |
| `gte` | `gte` | compare any valid grain field with a literal | `gte(field, value)` |
| `lt` | `lt` | compare any valid grain field with a literal | `lt(field, value)` |
| `lte` | `lte` | compare any valid grain field with a literal | `lte(field, value)` |
| `cited_by` | `cited_by` | spans cited as evidence by a judge | `cited_by(judge, value=None)` |
| `all_` | `all` | every filter matches (AND) | `all_(first, *rest)` |
| `any_` | `any` | at least one filter matches (OR) | `any_(first, *rest)` |
| `not_` | `not` | the filter does not match (NOT) | `not_(filter)` |
| `any_span` | `any_span` | at least one child span matches | `any_span(filter)` |
| `every_span` | `every_span` | every child span matches | `every_span(filter)` |
| `no_span` | `no_span` | no child span matches | `no_span(filter)` |
| `any_trace` | `any_trace` | at least one trace in the session matches | `any_trace(filter)` |
| `every_trace` | `every_trace` | every trace in the session matches | `every_trace(filter)` |
| `no_trace` | `no_trace` | no trace in the session matches | `no_trace(filter)` |
| `descendant_of` | `descendant_of` | spans below a matching span in the trace tree | `descendant_of(filter, depth=1)` |
| `ancestor_of` | `ancestor_of` | spans above a matching span in the trace tree | `ancestor_of(filter, depth=1)` |
| `over_spans` | `over_spans` | an aggregate over a row's child spans meets a threshold | `over_spans(agg, cmp, value, where=None)` |
| `over_traces` | `over_traces` | an aggregate over a session's traces meets a threshold | `over_traces(agg, cmp, value, where=None)` |
| `over_scores` | `over_scores` | an aggregate over a row's judge scores meets a threshold (no where) | `over_scores(agg, cmp, value)` |
| `at_least` | `at_least` | at least k related rows match — between any_ (k=1) and every_ | `at_least(k, of, where=None)` |
| `col` | `col` | a column reference operand | `col(name)` |
| `agg_expr` | `agg_expr` | an aggregate expression; `per` makes it a per-group window, `where` a conditional aggregate | `agg_expr(func, field=None, *, q=?, per=?, where=?)` |
| `arith` | `arith` | arithmetic over expressions | `arith(fn, left, right)` |
| `bucket` | `bucket` | time-bucket key for summarize (outputs column `bucket`) | `bucket(field, every)` |
<!-- /jql:generated:operations -->

### Select terminals

Call exactly one select terminal. `.rows()` accepts explicit fields and an optional per-query limit.

<!-- jql:generated:terminals -->
| JSON `op` | purpose | public SDK signature |
|---|---|---|
| `rows` | rows with optional explicit fields and limit | `.rows(*, fields=None, limit=None)` |
| `ids` | matching IDs — cap with the request-level `limit` option | `.ids()` |
| `count` | a total or counts grouped by one field | `.count(by=None)` |
| `recent` | the n most recent rows | `.recent(n)` |
| `top` | the n largest rows by a numeric field | `.top(n, by)` |
| `ranked` | positional rows globally or within a field | `.ranked(*, by=?, pick=?, within=?)` |
| `agg` | one scalar aggregate value; q is required for quantile and rejected for every other func | `.agg(func, field, q=None)` |
| `trend` | time buckets for count or rate | `.trend(*, metric=None, bucket=None)` |
<!-- /jql:generated:terminals -->

Example:

```python
from judgeval.jql import all_, eq, spans, status

result = client.query(
    spans()
    .where(all_(status("error"), eq("model", "gpt-4.1")))
    .last("7d")
    .rows(fields=["span_id", "span_name", "model", "cost"], limit=100)
)
```

### Pipeline stages

Call `.pipe()` instead of a select terminal. Pipeline stages execute in call order.

<!-- jql:generated:stages -->
| JSON `op` | purpose | public SDK signature |
|---|---|---|
| `where` | keep rows passing a comparison (after summarize: HAVING) | `.where(filter)` |
| `pick` | keep the first/last n rows per group (adds rank column `_rn`) | `.pick(*, by=?, n=?, per=?, reverse=?)` |
| `derive` | add computed columns to every row (rows unchanged) | `.derive(cols)` |
| `summarize` | collapse to one row per group with computed aggregates (grain change) | `.summarize(aggs, *, by=None)` |
| `sort` | order rows; append ` desc` for descending | `.sort(by)` |
| `take` | keep the first n rows (after sort), optionally skipping offset rows | `.take(n, offset=None)` |
<!-- /jql:generated:stages -->

```python
from judgeval.jql import agg_expr, spans

query = (
    spans()
    .last("7d")
    .pipe()
    .summarize({"total_cost": agg_expr("sum", "cost")}, by="model")
    .sort("total_cost desc")
    .take(10)
)
result = client.query(query)
```

### Tables and charts

Presentation terminals use canonical snake-case option names and are executed with `client.present(...)`.

<!-- jql:generated:presentations -->
| JSON `op` | purpose | public SDK signature |
|---|---|---|
| `chart` | chart-ready rows with explicit axes and a bounded chart type | `.chart(*, chart_type, title, x_axis, y_axis, series_by=?, description=?)` |
| `table` | table-ready rows projected to an explicit ordered column list | `.table(*, title, columns, description=None)` |
<!-- /jql:generated:presentations -->

```python
from judgeval.jql import agg_expr, spans

summary = (
    spans()
    .last("7d")
    .pipe()
    .summarize({"total_cost": agg_expr("sum", "cost")}, by="model")
    .sort("total_cost desc")
    .take(10)
)

table_result = client.present(
    summary.table(
        title="Cost by model",
        columns=[{"key": "model"}, {"key": "total_cost"}],
    )
)

chart_result = client.present(
    summary.chart(
        chart_type="bar",
        title="Cost by model",
        x_axis={"key": "model"},
        y_axis={"key": "total_cost"},
    )
)
```

### Discovery

<!-- jql:generated:shared-discovery-guidance -->
Names are data. Judge names, behavior values, span names, model names, and attribute keys
must be **discovered before they are used** in substantive queries — a filter on a
misspelled or guessed name silently matches nothing.
<!-- /jql:generated:shared-discovery-guidance -->

Only use these generated `DiscoveryKind` values:

<!-- jql:generated:discovery -->
| kind | returns | options |
|---|---|---|
| `judges` | current judge names/config | `history`, `limit` |
| `behaviors` | behavior names/values | `history`, `limit` |
| `span_names` | span names seen in facts | `time`, `limit` |
| `models` | model names seen in facts | `time`, `limit` |
| `fields` | standard fields usable for filters, groups, and aggregates | `source` (required), `limit` |
| `citations` | spans cited by a judge decision | `judge`, `value`, `time`, `limit` |
| `rules` | rule configs | `limit` |
| `judge_prompts` | judge prompt configs | `limit` |
| `judge_config` | judge configuration | `limit` |
<!-- /jql:generated:discovery -->

```python
fields = client.discover("fields", source="traces")
models = client.discover("models", time={"last": "7d"}, limit=100)
judges = client.discover("judges", limit=100)
```

### Analysis workflow

<!-- jql:generated:shared-analysis-workflow -->
Use JQL as an aggregate analysis surface, not just a trace browser.

1. Discover names before filtering on them.
2. Pick the grain: `traces`, `spans`, or `sessions`.
3. Query aggregates first (`.count(by)`).
4. Sample raw rows only after the aggregate shape is known (`.recent`, `.top`, `.rows`).
5. Run a disconfirming query before answering.
6. State the time range, filters, and coverage used in the final answer.
<!-- /jql:generated:shared-analysis-workflow -->

### Responses and errors

`client.query(...)` and `client.discover(...)` return `JqlQueryResponse` with exactly:

- `query_id: str`
- `rows: list[dict[str, Any]] | None`
- `row_count: int | None`
- `elapsed_ms: float`

These typed responses are mappings: access fields as `result["rows"]`, not as attributes.

`client.present(...)` returns `JqlPresentationResponse` with exactly:

- `query_id: str`
- `presentation: Any`
- `frame: Any | None`
- `elapsed_ms: float`

Errors use `JudgmentAPIError` from `judgeval.exceptions`. Its public structured attributes include `status_code`, `code`, `hint`, and `retry_after_seconds`.

```python
import time

from judgeval.exceptions import JudgmentAPIError

try:
    result = client.query(query)
except JudgmentAPIError as error:
    if error.retry_after_seconds is not None:
        time.sleep(error.retry_after_seconds)
        result = client.query(query)
    else:
        raise RuntimeError(
            f"JQL request failed: code={error.code!r}; hint={error.hint!r}"
        ) from error
```

Retry only when that behavior is appropriate for the application. Do not assume a retry-after value exists or match undocumented error-code strings.

## TypeScript reference

This reference matches `judgeval@1.3.0`.

### Install and configure

```bash
npm install judgeval
# or: yarn add judgeval
# or: pnpm add judgeval
# or: bun add judgeval
```

Set `JUDGMENT_API_KEY` and `JUDGMENT_ORG_ID` in the environment, then select the project through the client:

```typescript
import { Judgeval } from "judgeval";

const client = await Judgeval.create({ projectName: "my-project" });
```

Do not add organization or project identifiers to query JSON.

### First query

```typescript
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.toJSON()` returns this canonical JSON:

```json
{"op":"query","source":"traces","filter":{"op":"eq","field":"session","value":"session-123"},"select":{"op":"ids"}}
```

### Query roots and time bounds

<!-- jql:generated:roots -->
| source | builder |
|---|---|
| traces | `traces(options?)` |
| spans | `spans(options?)` |
| sessions | `sessions(options?)` |
<!-- /jql:generated:roots -->

<!-- jql:generated:shared-guidance -->
Every query begins at one root—`traces`, `spans`, or `sessions`—and keeps that grain
until a terminal or pipeline changes the output shape.

- Pick the grain before choosing fields. Span-only fields such as `model`, `cost`, and
  `name` are not trace fields.
- Supply a filter to the root or call `.where(filter)` before `.pipe()`. Root-level
  `.where()` ANDs its filter with any filter already supplied to the root.
- Apply one time bound with `.last(window)`, `.since(date)`, or `.between(start, end)`
  before a terminal or `.pipe()`. Use plain `YYYY-MM-DD` values for `since` and
  `between`; an omitted bound searches all available history.
- Use relation quantifiers or nested builders for cross-grain questions instead of
  pretending span fields exist on traces.
- End a direct query with one select terminal, or call `.pipe()` and compose stages.
  Pipeline `.where()` filters the current pipeline columns and is distinct from
  root-level `.where()`.
- For scalar `quantile`, `q` is required. Every other aggregate function rejects `q`.
<!-- /jql:generated:shared-guidance -->

### Filters and expressions

Import builders from `judgeval/jql`. Method names are camelCase where the canonical JSON operation uses snake_case, such as `citedBy`, `aggExpr`, and `anySpan`.

<!-- jql:generated:operations -->
| builder | JSON `op` | purpose | signature |
|---|---|---|---|
| `status` | `status` | trace/span status | `status(value)` |
| `name` | `name` | span name — span grain | `name(value)` |
| `model` | `model` | LLM model name — span grain | `model(value)` |
| `cost` | `cost` | span cost — span grain | `cost({ gt?, gte?, lt?, lte? })` |
| `duration` | `duration` | duration in nanoseconds | `duration({ gt?, gte?, lt?, lte? })` |
| `judge` | `judge` | judge scores attached to traces or spans | `judge(judgeName, value?)` |
| `judged` | `judged` | configured judge results | `judged(options)` |
| `attr` | `attr` | structured attributes | `attr(key, value?, selector?)` |
| `grep` | `grep` | substring search | `grep(field, value)` |
| `rg` | `rg` | regex search | `rg(field, pattern, { ignoreCase? }?)` |
| `tokens` | `tokens` | fast whole-word content search | `tokens(field, words)` |
| `eq` | `eq` | compare any valid grain field with a literal | `eq(field, value)` |
| `ne` | `ne` | compare any valid grain field with a literal | `ne(field, value)` |
| `gt` | `gt` | compare any valid grain field with a literal | `gt(field, value)` |
| `gte` | `gte` | compare any valid grain field with a literal | `gte(field, value)` |
| `lt` | `lt` | compare any valid grain field with a literal | `lt(field, value)` |
| `lte` | `lte` | compare any valid grain field with a literal | `lte(field, value)` |
| `citedBy` | `cited_by` | spans cited as evidence by a judge | `citedBy(judgeName, value?)` |
| `all` | `all` | every filter matches (AND) | `all(first, ...rest)` |
| `any` | `any` | at least one filter matches (OR) | `any(first, ...rest)` |
| `not` | `not` | the filter does not match (NOT) | `not(filter)` |
| `anySpan` | `any_span` | at least one child span matches | `anySpan(filter)` |
| `everySpan` | `every_span` | every child span matches | `everySpan(filter)` |
| `noSpan` | `no_span` | no child span matches | `noSpan(filter)` |
| `anyTrace` | `any_trace` | at least one trace in the session matches | `anyTrace(filter)` |
| `everyTrace` | `every_trace` | every trace in the session matches | `everyTrace(filter)` |
| `noTrace` | `no_trace` | no trace in the session matches | `noTrace(filter)` |
| `descendantOf` | `descendant_of` | spans below a matching span in the trace tree | `descendantOf(filter, depth?)` |
| `ancestorOf` | `ancestor_of` | spans above a matching span in the trace tree | `ancestorOf(filter, depth?)` |
| `overSpans` | `over_spans` | an aggregate over a row's child spans meets a threshold | `overSpans(agg, cmp, value, where?)` |
| `overTraces` | `over_traces` | an aggregate over a session's traces meets a threshold | `overTraces(agg, cmp, value, where?)` |
| `overScores` | `over_scores` | an aggregate over a row's judge scores meets a threshold (no where) | `overScores(agg, cmp, value)` |
| `atLeast` | `at_least` | at least k related rows match — between any_ (k=1) and every_ | `atLeast(k, of, where?)` |
| `col` | `col` | a column reference operand | `col(name)` |
| `aggExpr` | `agg_expr` | an aggregate expression; `per` makes it a per-group window, `where` a conditional aggregate | `aggExpr(options)` |
| `arith` | `arith` | arithmetic over expressions | `arith(fn, left, right)` |
| `bucket` | `bucket` | time-bucket key for summarize (outputs column `bucket`) | `bucket(field, every)` |
<!-- /jql:generated:operations -->

### Select terminals

Call exactly one select terminal. `.rows()` accepts explicit fields and an optional per-query limit.

<!-- jql:generated:terminals -->
| JSON `op` | purpose | public SDK signature |
|---|---|---|
| `rows` | rows with optional explicit fields and limit | `.rows({ fields?, limit? }?)` |
| `ids` | matching IDs — cap with the request-level `limit` option | `.ids()` |
| `count` | a total or counts grouped by one field | `.count(by?)` |
| `recent` | the n most recent rows | `.recent(n)` |
| `top` | the n largest rows by a numeric field | `.top(n, by)` |
| `ranked` | positional rows globally or within a field | `.ranked({ by?, pick?, within? }?)` |
| `agg` | one scalar aggregate value; q is required for quantile and rejected for every other func | `.agg({ func, field, q? })` |
| `trend` | time buckets for count or rate | `.trend({ metric?, bucket? }?)` |
<!-- /jql:generated:terminals -->

Example:

```typescript
import { all, eq, spans, status } from "judgeval/jql";

const result = await client.query(
  spans()
    .where(all(status("error"), eq("model", "gpt-4.1")))
    .last("7d")
    .rows({
      fields: ["span_id", "span_name", "model", "cost"],
      limit: 100,
    }),
);
```

### Pipeline stages

Call `.pipe()` instead of a select terminal. Pipeline stages execute in call order.

<!-- jql:generated:stages -->
| JSON `op` | purpose | public SDK signature |
|---|---|---|
| `where` | keep rows passing a comparison (after summarize: HAVING) | `.where(filter)` |
| `pick` | keep the first/last n rows per group (adds rank column `_rn`) | `.pick({ by?, n?, per?, reverse? }?)` |
| `derive` | add computed columns to every row (rows unchanged) | `.derive(cols)` |
| `summarize` | collapse to one row per group with computed aggregates (grain change) | `.summarize(by, aggs) / .summarize(aggs)` |
| `sort` | order rows; append ` desc` for descending | `.sort(by)` |
| `take` | keep the first n rows (after sort), optionally skipping offset rows | `.take(n, offset?)` |
<!-- /jql:generated:stages -->

```typescript
import { aggExpr, spans } from "judgeval/jql";

const query = spans()
  .last("7d")
  .pipe()
  .summarize("model", {
    total_cost: aggExpr({ func: "sum", field: "cost" }),
  })
  .sort("total_cost desc")
  .take(10);

const result = await client.query(query);
```

### Tables and charts

Presentation options use canonical snake-case JSON names and are executed with `client.present(...)`.

<!-- jql:generated:presentations -->
| JSON `op` | purpose | public SDK signature |
|---|---|---|
| `chart` | chart-ready rows with explicit axes and a bounded chart type | `.chart({ chart_type, title, x_axis, y_axis, series_by?, description? })` |
| `table` | table-ready rows projected to an explicit ordered column list | `.table({ title, columns, description? })` |
<!-- /jql:generated:presentations -->

```typescript
import { aggExpr, spans } from "judgeval/jql";

const summary = spans()
  .last("7d")
  .pipe()
  .summarize("model", {
    total_cost: aggExpr({ func: "sum", field: "cost" }),
  })
  .sort("total_cost desc")
  .take(10);

const tableResult = await client.present(
  summary.table({
    title: "Cost by model",
    columns: [{ key: "model" }, { key: "total_cost" }],
  }),
);

const chartResult = await client.present(
  summary.chart({
    chart_type: "bar",
    title: "Cost by model",
    x_axis: { key: "model" },
    y_axis: { key: "total_cost" },
  }),
);
```

### Discovery

<!-- jql:generated:shared-discovery-guidance -->
Names are data. Judge names, behavior values, span names, model names, and attribute keys
must be **discovered before they are used** in substantive queries — a filter on a
misspelled or guessed name silently matches nothing.
<!-- /jql:generated:shared-discovery-guidance -->

Only use these generated `DiscoveryKind` values:

<!-- jql:generated:discovery -->
| kind | returns | options |
|---|---|---|
| `judges` | current judge names/config | `history`, `limit` |
| `behaviors` | behavior names/values | `history`, `limit` |
| `span_names` | span names seen in facts | `time`, `limit` |
| `models` | model names seen in facts | `time`, `limit` |
| `fields` | standard fields usable for filters, groups, and aggregates | `source` (required), `limit` |
| `citations` | spans cited by a judge decision | `judge`, `value`, `time`, `limit` |
| `rules` | rule configs | `limit` |
| `judge_prompts` | judge prompt configs | `limit` |
| `judge_config` | judge configuration | `limit` |
<!-- /jql:generated:discovery -->

```typescript
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 });
```

### Analysis workflow

<!-- jql:generated:shared-analysis-workflow -->
Use JQL as an aggregate analysis surface, not just a trace browser.

1. Discover names before filtering on them.
2. Pick the grain: `traces`, `spans`, or `sessions`.
3. Query aggregates first (`.count(by)`).
4. Sample raw rows only after the aggregate shape is known (`.recent`, `.top`, `.rows`).
5. Run a disconfirming query before answering.
6. State the time range, filters, and coverage used in the final answer.
<!-- /jql:generated:shared-analysis-workflow -->

### Responses and errors

`client.query(...)` and `client.discover(...)` return `JqlQueryResponse` with exactly:

- `query_id: string`
- `rows: Array<Record<string, unknown>> | null`
- `row_count: number | null`
- `elapsed_ms: number`

`client.present(...)` returns `JqlPresentationResponse` with exactly:

- `query_id: string`
- `presentation: unknown`
- `frame: unknown | null`
- `elapsed_ms: number`

Errors use `JudgevalAPIError` from `judgeval`. Its public structured properties include `status`, `code`, `hint`, and `retryAfterSeconds`.

```typescript
import { JudgevalAPIError } from "judgeval";

const execute = () => client.query(query);
let result: Awaited<ReturnType<typeof execute>>;

try {
  result = await execute();
} catch (error) {
  if (error instanceof JudgevalAPIError) {
    if (error.retryAfterSeconds != null) {
      await new Promise((resolve) =>
        setTimeout(resolve, error.retryAfterSeconds * 1000),
      );
      result = await execute();
    } else {
      throw new Error(
        `JQL request failed: code=${error.code}; hint=${error.hint}`,
        { cause: error },
      );
    }
  } else {
    throw error;
  }
}
```

Retry only when that behavior is appropriate for the application. Do not assume a retry-after value exists or match undocumented error-code strings.
