# Probara Query Language (PQL) reference

> Every entity, field, operator, function, limit and error of Probara Query Language (PQL), with worked examples for common QA questions.

**Probara Query Language (PQL)** asks questions of your test data. A query names an entity, such as cases or results, filters its rows with `WHERE`, and can group and aggregate them. Write PQL in [Queries](https://docs.probara.net/en/guides/queries/), or send it to the [PQL API](https://docs.probara.net/en/developers/api/pql/).

## Quick path

1. Name the entity: `FROM cases`.
2. Filter the rows: `WHERE priority = "High"`.
3. Choose the page: `LIMIT 50`.

```text
FROM cases WHERE priority = "High" AND status = "Active" ORDER BY title LIMIT 50
```

## Query shape

| Clause               | Purpose                | Notes                                                                 |
| -------------------- | ---------------------- | --------------------------------------------------------------------- |
| `SELECT`             | Columns and aggregates | Optional. Without it, a query returns the entity's default columns.   |
| `FROM`               | The entity             | Required. One of the nine entities in [Entities](#entities).          |
| `WHERE`              | Conditions on each row | Runs before grouping.                                                 |
| `GROUP BY`           | Groups                 | Fields, expressions or positions, such as `GROUP BY 1`.               |
| `HAVING`             | Conditions on groups   | Uses aggregates, such as `HAVING count() > 5`.                        |
| `ORDER BY`           | Sort order             | Fields, aliases, positions or aggregates. Add `DESC` for descending.  |
| `LIMIT` and `OFFSET` | Page size and start    | `LIMIT` defaults to 50 and takes up to 500. `OFFSET` follows `LIMIT`. |

Rules:

- Keywords, function names, entity names and field names are not case-sensitive.
- A line comment starts with `--`.
- Line breaks and extra spaces do not matter.
- A query with `SELECT` and an aggregate returns grouped rows. Any other query returns rows.
- A `SELECT` item that is not aggregated must appear in `GROUP BY`.

## Literals

| Kind                 | How to write it                                                                                   | Example                                |
| -------------------- | ------------------------------------------------------------------------------------------------- | -------------------------------------- |
| Text                 | In double or single quotes.                                                                       | `title ~ "login"`                      |
| Number               | An integer or a decimal.                                                                          | `steps >= 3`                           |
| Boolean              | `true` or `false`.                                                                                | `archived = false`                     |
| List                 | Values in parentheses, separated by commas. At most 200 values.                                   | `priority in ("High", "Critical")`     |
| Fixed duration       | A number and a unit: `m` minutes, `h` hours, `d` days, `w` weeks.                                 | `duration > 30m`                       |
| Relative duration    | A signed duration in a time function. Units also include `mo` months, `q` quarters and `y` years. | `now(-3mo)`, `startOf(month, -1mo)`    |
| Date                 | `"YYYY-MM-DD"` or `"YYYY-MM-DD HH:mm"`, in quotes.                                                | `created > "2026-10-01"`               |
| Enum or option value | A bare word or a quoted value. Matching ignores capital letters.                                  | `status = failed`, `priority = "High"` |

Notes:

- Text values must be quoted. `title = Login` is an error, and `title = "Login"` works.
- Names of milestones, suites, environments, tags and projects are text. Quote them when they contain spaces: `milestone = "Release 1.4"`.
- Fixed-duration fields, such as `runs.duration`, accept `m`, `h`, `d` and `w` only. Use `mo`, `q` and `y` only in relative time.

## Operators

| Operator                           | What it does                                                                          | Field types                                      |
| ---------------------------------- | ------------------------------------------------------------------------------------- | ------------------------------------------------ |
| `=`, `!=`                          | Equal, or not equal                                                                   | All fields except `lastResult`                   |
| `<`, `<=`, `>`, `>=`               | Before, after, less than, greater than                                                | Number, duration, date                           |
| `between a and b`                  | From `a` to `b`                                                                       | Number, duration, date                           |
| `~`, `!~`                          | Contains, or does not contain, ignoring capital letters                               | Text                                             |
| `startsWith`                       | Begins with the text                                                                  | Text                                             |
| `in (...)`, `not in (...)`         | One of a list, or none of it                                                          | All fields except boolean and `lastResult`       |
| `in query("Name")`                 | Matches the rows of a saved query. See [In query() references](#in-query-references). | `id` only                                        |
| `is empty`, `is not empty`         | No value, or a value                                                                  | Fields marked as optional in the entity tables   |
| `under "Suite"`                    | The suite and every suite below it                                                    | Suite references: `cases.suite`, `suites.parent` |
| `exists(...)`, `count(...) <op> n` | Related records. See [Relations](#relations).                                         | Entities with relations                          |
| `was`, `changed`                   | Change history. See [History](#history).                                              | Fields marked as history-tracked                 |

Boolean fields accept only `=` and `!=`, so write `archived = true`, not `archived`.

## Entities

Every entity has an `id`, which is its display ID, and a `project`. The tables list the fields you can use. A field marked **Optional** can be empty, so `is empty` applies to it. A field marked **History** supports `was` and `changed`.

### Cases

Default columns: `id`, `title`, `suite`, `priority`, `status`.

| Field           | Type                | Meaning                                                                                                        | History |
| --------------- | ------------------- | -------------------------------------------------------------------------------------------------------------- | ------- |
| `id`            | ID                  | The case's display ID                                                                                          |         |
| `project`       | Reference           | The project                                                                                                    |         |
| `title`         | Text                | The title                                                                                                      |         |
| `description`   | Text, optional      | The description                                                                                                |         |
| `suite`         | Reference, optional | The suite. Use `under` for its tree.                                                                           |         |
| `milestone`     | Reference, optional | The milestone                                                                                                  |         |
| `tags`          | Tags, optional      | The tags. A case counts in the group of each of its tags.                                                      |         |
| `author`        | User                | Who created the case                                                                                           |         |
| `updatedBy`     | User, optional      | Who last updated the case                                                                                      |         |
| `created`       | Date                | When the case was created                                                                                      |         |
| `updated`       | Date                | When the case was last updated                                                                                 |         |
| `archived`      | Boolean             | Whether the case is archived. See [Archived cases](#archived-cases).                                           |         |
| `automationKey` | Text, optional      | The automation key                                                                                             |         |
| `automated`     | Boolean             | Whether the case has an automation key                                                                         |         |
| `steps`         | Number              | The number of steps                                                                                            |         |
| `revision`      | Number              | The revision number                                                                                            |         |
| `reviewStatus`  | Enum                | `not_reviewed`, `in_review` or `approved`. See [Test case review](https://docs.probara.net/en/guides/test-case-review/#review-status). |         |
| `jira`          | Text, optional      | The first linked Jira issue key                                                                                |         |
| `priority`      | Option, optional    | The priority, from your organization's options                                                                 | Yes     |
| `severity`      | Option, optional    | The severity, from your organization's options                                                                 | Yes     |
| `status`        | Option, optional    | The status, from your organization's options                                                                   | Yes     |
| `type`          | Option, optional    | The type, from your organization's options                                                                     | Yes     |
| `layer`         | Option, optional    | The layer, from your organization's options                                                                    | Yes     |
| `behavior`      | Option, optional    | The behavior, from your organization's options                                                                 | Yes     |
| `isFlaky`       | Boolean             | Whether the case is marked as flaky                                                                            |         |
| `lastResult`    | Result              | The most recent result. Use `lastResult.field`. See [lastResult](#lastresult).                                 |         |

Relations: `results`, `runCases`, `defects`, `jira`. Custom fields: `cf[...]`.

### Suites

Default columns: `id`, `name`, `parent`, `cases`.

| Field     | Type                | Meaning                                     |
| --------- | ------------------- | ------------------------------------------- |
| `id`      | ID                  | The suite's display ID                      |
| `project` | Reference           | The project                                 |
| `name`    | Text                | The name                                    |
| `parent`  | Reference, optional | The parent suite. Use `under` for its tree. |
| `path`    | Text                | The suite's path                            |
| `cases`   | Number              | The number of cases that are not archived   |
| `created` | Date                | When the suite was created                  |
| `updated` | Date                | When the suite was last updated             |

Relations: `cases`.

### Runs

Default columns: `id`, `name`, `state`, `status`, `milestone`, `passRate`.

| Field                                                | Type                | Meaning                                                                                                                                                                  |
| ---------------------------------------------------- | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `id`                                                 | ID                  | The run's display ID                                                                                                                                                     |
| `project`                                            | Reference           | The project                                                                                                                                                              |
| `name`                                               | Text                | The name                                                                                                                                                                 |
| `state`                                              | Enum                | `open` or `closed`                                                                                                                                                       |
| `status`                                             | Enum                | `in_progress`, `passed`, `failed` or `aborted`. An open run is in progress, and an aborted run is aborted.                                                               |
| `environment`                                        | Reference, optional | The environment                                                                                                                                                          |
| `milestone`                                          | Reference, optional | The milestone                                                                                                                                                            |
| `plan`                                               | Reference, optional | The test plan                                                                                                                                                            |
| `author`                                             | User                | Who created the run                                                                                                                                                      |
| `tags`                                               | Tags, optional      | The tags                                                                                                                                                                 |
| `started`, `finished`, `closed`, `aborted`           | Date, optional      | When the run started, finished, was closed, or was aborted                                                                                                               |
| `duration`                                           | Duration, optional  | The total time of the run                                                                                                                                                |
| `passed`, `failed`, `blocked`, `skipped`, `untested` | Number              | The number of cases with each outcome                                                                                                                                    |
| `passRate`                                           | Number              | The share of executed cases that passed, as a percentage from 0 to 100, rounded to one decimal. It cannot be averaged or summed. Use `passRate()` in an aggregate query. |
| `branch`                                             | Text, optional      | The branch                                                                                                                                                               |
| `commit`                                             | Text, optional      | The commit                                                                                                                                                               |

Relations: `results`, `runCases`, `defects`. Custom fields: `cf[...]`.

### Run cases

Run cases are the cases inside a run, including the ones not yet tested. Default columns: `run`, `case`, `status`, `assignee`, `elapsed`.

| Field        | Type               | Meaning                                                                                                                                                                                                   |
| ------------ | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id`         | ID                 | The display ID                                                                                                                                                                                            |
| `project`    | Reference          | The project                                                                                                                                                                                               |
| `run`        | Reference          | The run. Use `run.field` for its fields.                                                                                                                                                                  |
| `case`       | Reference          | The case. Use `case.field` for its fields.                                                                                                                                                                |
| `status`     | Enum               | `untested`, `retested`, `passed`, `failed`, `skipped` or `blocked`                                                                                                                                        |
| `assignee`   | User, optional     | Who the case is assigned to                                                                                                                                                                               |
| `elapsed`    | Duration, optional | The time spent on the case                                                                                                                                                                                |
| `parameters` | Text, optional     | The instance’s combination as name=value pairs joined with ", ". `~` matches a pair or a value, for example `FROM runCases WHERE parameters ~ "Browser=Firefox"`. A run case without parameters has none. |

No relations.

A run case of a parameterized case is one instance: a case with two parameters of three values each has nine run cases in a run. Count them with `count()` over the run cases, and see [Parameterized test cases](https://docs.probara.net/en/guides/parameterized-test-cases/).

### Results

Default columns: `id`, `run`, `case`, `status`, `executedBy`, `executed`.

| Field        | Type               | Meaning                                    |
| ------------ | ------------------ | ------------------------------------------ |
| `id`         | ID                 | The display ID, such as `RES-42`           |
| `project`    | Reference          | The project                                |
| `case`       | Reference          | The case. Use `case.field` for its fields. |
| `run`        | Reference          | The run. Use `run.field` for its fields.   |
| `status`     | Enum               | `passed`, `failed`, `skipped` or `blocked` |
| `duration`   | Duration, optional | How long the execution took                |
| `executedBy` | User, optional     | Who executed the case                      |
| `executed`   | Date               | When the case was executed                 |
| `comment`    | Text, optional     | The comment                                |

No relations. Custom fields: none.

### Defects

Default columns: `id`, `title`, `status`, `severity`, `priority`, `assignee`.

| Field        | Type                | Meaning                                                | History |
| ------------ | ------------------- | ------------------------------------------------------ | ------- |
| `id`         | ID                  | The display ID                                         |         |
| `project`    | Reference           | The project                                            |         |
| `title`      | Text                | The title                                              |         |
| `status`     | Enum                | `open`, `in_progress`, `resolved` or `closed`          | Yes     |
| `resolution` | Enum, optional      | `fixed`, `wont_fix`, `duplicate` or `cannot_reproduce` |         |
| `severity`   | Option, optional    | The severity, from your organization's options         | Yes     |
| `priority`   | Option, optional    | The priority, from your organization's options         | Yes     |
| `reporter`   | User                | Who reported the defect                                |         |
| `assignee`   | User, optional      | Who the defect is assigned to                          | Yes     |
| `milestone`  | Reference, optional | The milestone                                          |         |
| `tags`       | Tags, optional      | The tags                                               |         |
| `created`    | Date                | When the defect was created                            |         |
| `updated`    | Date                | When the defect was last updated                       |         |
| `resolved`   | Date, optional      | When the defect was resolved                           |         |
| `closed`     | Date, optional      | When the defect was closed                             |         |
| `jira`       | Text, optional      | The linked Jira issue key                              |         |

Relations: `cases`, `results`. Custom fields: `cf[...]`.

### Milestones

Default columns: `id`, `name`, `status`, `due`.

| Field       | Type                | Meaning                             |
| ----------- | ------------------- | ----------------------------------- |
| `id`        | ID                  | The display ID                      |
| `project`   | Reference           | The project                         |
| `name`      | Text                | The name                            |
| `status`    | Enum                | `upcoming`, `active` or `completed` |
| `parent`    | Reference, optional | The parent milestone                |
| `start`     | Date, optional      | The start date                      |
| `due`       | Date, optional      | The due date                        |
| `completed` | Date, optional      | When the milestone was completed    |

Relations: `runs`, `cases`, `defects`.

### Plans

Default columns: `id`, `name`, `status`, `milestone`.

| Field       | Type                | Meaning                          |
| ----------- | ------------------- | -------------------------------- |
| `id`        | ID                  | The display ID                   |
| `project`   | Reference           | The project                      |
| `name`      | Text                | The name                         |
| `status`    | Enum                | `draft`, `active` or `completed` |
| `milestone` | Reference, optional | The milestone                    |
| `created`   | Date                | When the plan was created        |
| `updated`   | Date                | When the plan was last updated   |

Relations: `runs`, `cases`.

### Shared steps

Default columns: `id`, `title`, `folder`, `updated`.

| Field     | Type                | Meaning                                                        |
| --------- | ------------------- | -------------------------------------------------------------- |
| `id`      | ID                  | The display ID                                                 |
| `project` | Reference, optional | The project. Empty means the shared step is organization-wide. |
| `title`   | Text                | The title                                                      |
| `folder`  | Reference, optional | The folder                                                     |
| `created` | Date                | When the shared step was created                               |
| `updated` | Date                | When the shared step was last updated                          |

No relations.

## Custom fields

Cases, runs and defects have your organization's custom fields. Write the field's title in brackets:

```text
FROM cases WHERE cf["Risk"] = "High"
```

The title is matched ignoring capital letters and surrounding spaces. A title that no custom field of that entity has returns `unknown_custom_field`.

| Custom field type           | Works as | Operators                                                      |
| --------------------------- | -------- | -------------------------------------------------------------- |
| Number                      | Number   | Comparisons, `between`, lists                                  |
| Short text, paragraph, URL  | Text     | `~`, `!~`, `startsWith`, lists, `=`                            |
| Checkbox                    | Boolean  | `=`, `!=`                                                      |
| Date picker                 | Date     | Comparisons, `between`, lists                                  |
| User picker                 | User     | `=`, lists, and the member functions                           |
| Select, multi-select, radio | Option   | `=`, lists. A multi-select counts a row in each of its values. |

## Paths to related records

Results and run cases link to a run and a case. Reach into them with a dot path:

```text
FROM results WHERE run.environment = "staging" AND case.priority = "Critical" AND status = failed
```

Paths work in `WHERE` and in `GROUP BY`, for example `GROUP BY run.environment`.

## lastResult

`lastResult` is the most recent result of a case, by execution time and then by ID. It is available on `cases` at the top level of a query.

- `lastResult.status` reads the status of the latest result.
- `lastResult(environment = "staging").status` reads the latest result among the runs that match the condition. The condition uses run fields, such as `environment`, `milestone` and `plan`.
- A case with no matching result has no value, so `=` does not match it.

```text
FROM cases WHERE lastResult(environment = "staging").status = failed ORDER BY title
```

`lastResult` cannot be compared directly, and it has no history.

## Relations

`exists` and `count` look at the related records of each row.

| Entity       | Relations                                |
| ------------ | ---------------------------------------- |
| `cases`      | `results`, `runCases`, `defects`, `jira` |
| `suites`     | `cases`                                  |
| `runs`       | `results`, `runCases`, `defects`         |
| `milestones` | `runs`, `cases`, `defects`               |
| `plans`      | `runs`, `cases`                          |
| `defects`    | `cases`, `results`                       |
| Others       | None                                     |

```text
FROM cases WHERE exists(results where status = failed)
FROM cases WHERE NOT exists(results)
FROM cases WHERE count(results where status = failed) >= 3
```

Rules:

- Use `exists` and `count` in `WHERE`. `exists` in `HAVING` returns `relation_placement`.
- Compare `count(relation)` with a number.
- Sub-queries nest at most two levels deep. `FROM suites WHERE exists(cases where exists(results where status = failed))` is two levels.
- Related rows count only in projects you can access, and each related record counts once.
- Archived cases are left out of a related-record condition over cases unless the condition names `archived`. See [Archived cases](#archived-cases).

## Functions

| Function                   | Returns                                                          | Notes                                                                                      |
| -------------------------- | ---------------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
| `currentUser()`            | The signed-in user                                               | Use it on user fields and in `by`.                                                         |
| `membersOf("Group")`       | The members of a group                                           | Use it inside `in (...)` on a user field.                                                  |
| `activeMembers()`          | The current members of the organization                          | Use it inside `in (...)` on a user field.                                                  |
| `formerMembers()`          | Former members of the organization                               | Use it inside `in (...)` on a user field.                                                  |
| `now([offset])`            | The current time, optionally shifted                             | The offset is a signed duration, such as `now(-7d)`.                                       |
| `startOf(unit[, offset])`  | The start of the current hour, day, week, month, quarter or year | `startOf(week)`, `startOf(month, -1mo)`                                                    |
| `endOf(unit[, offset])`    | The end of the same units                                        | `endOf(day)`                                                                               |
| `count()`                  | The number of rows in each group                                 | `count(*)` also works.                                                                     |
| `count(distinct field)`    | The number of distinct values                                    | `count(distinct executedBy)`                                                               |
| `sum(field)`, `avg(field)` | The total, or the average, of a number or duration               | `avg(duration)`                                                                            |
| `min(field)`, `max(field)` | The smallest, or the largest, value                              | Numbers, durations and dates.                                                              |
| `passRate()`               | The percentage of passed results                                 | Results and runs only. From 0 to 100, rounded to one decimal. Empty when nothing executed. |
| `bucket(dateField, unit)`  | The start of the time bucket that a date falls in                | Units: `hour`, `day`, `week`, `month`, `quarter`, `year`. Weeks start on Monday.           |

`passRate()` counts the following:

- On results, passed results divided by all results.
- On runs, passed cases divided by passed, failed, blocked and skipped cases. This is the same figure as `runs.passRate`.

```text
SELECT bucket(executed, week) AS period, passRate() AS rate, count() AS results FROM results GROUP BY 1 ORDER BY 1
```

## Time and time zones

Dates and relative times are read in the **Timezone** of your [profile](https://docs.probara.net/en/guides/user-profile/). With **Auto-detected**, UTC is used. `now`, `startOf`, `endOf`, `bucket` and the date comparisons all follow this zone.

Relative time accepts every unit: `m`, `h`, `d`, `w`, `mo`, `q` and `y`. Fixed-duration fields accept `m`, `h`, `d` and `w`.

## Aggregation rules

- **`GROUP BY`** takes fields, expressions and positions, such as `GROUP BY 1`. It does not take a `SELECT` alias. Repeat the expression, or use its position.
- **`HAVING`** filters groups with aggregates, such as `HAVING count() > 5`. It does not take aliases, so `HAVING n > 5` fails.
- **`ORDER BY`** takes aliases (`ORDER BY n DESC`), positions counted from 1 (`ORDER BY 2`) and aggregates (`ORDER BY count() DESC`). Aggregates need a group or an aggregate in `SELECT`. User and tag fields cannot be sorted.
- **Grouping** works on number, boolean, enum, option, user, reference and tag fields. Text and dates cannot be grouped directly. Group a date with `bucket()`.
- **Tags and multi-select fields** count a row in the group of each of its values. Two such fields grouped together give one group per combination, so the groups can add up to more than the number of rows.
- **No value.** Rows without a value form a group named **No value**. It sorts first.
- **`count(field)`** counts the rows where the field has a value.
- **`HAVING` on a tag or multi-select field** is refused. Filter in `WHERE` instead.
- **Without `GROUP BY`**, an aggregate returns one row for all matching rows.
- **Time buckets** start at the start of the unit in your time zone. Empty buckets are not returned.
- **Date pickers** compare by local day.
- **Groups** are shown up to 1,000 per page. The result says so when there are more.

## History

Change history records changes to some fields. Two operators read it:

- `status was in_progress` is true when the value is the current value, or was one of the values recorded in the history.
- `status changed` is true when the history holds at least one change. Add modifiers to narrow it:

| Modifier   | Meaning                                                                     |
| ---------- | --------------------------------------------------------------------------- |
| `from v`   | The change started from `v`.                                                |
| `to v`     | The change ended at `v`.                                                    |
| `by user`  | The change was made by `currentUser()`, or by a member of `membersOf(...)`. |
| `after t`  | The change happened after `t`.                                              |
| `before t` | The change happened before `t`.                                             |

All the modifiers you write must match the same change. `t` is a date or a relative time, read in your time zone.

```text
FROM defects WHERE status changed from resolved to open after now(-14d)
FROM cases WHERE priority changed by currentUser() after now(-7d)
```

### Fields with history

| Entity    | Fields                                                        |
| --------- | ------------------------------------------------------------- |
| `cases`   | `priority`, `severity`, `status`, `type`, `layer`, `behavior` |
| `defects` | `status`, `assignee`, `severity`, `priority`                  |

Change history is not available for run state, milestone and suite moves, custom fields (`cf[...]`), or linked fields. A query that uses them returns `history_not_available`.

Rules:

- Use `was` and `changed` in `WHERE`. In `HAVING` they return `history_placement`.
- Change history keeps records for your plan's history retention window. See [Data retention](https://docs.probara.net/en/guides/test-runs/#data-retention).
- Changes made before the record was audited are not known, so a value can be missing from its history.

## In query() references

`id in query("Saved name")` keeps the rows that a saved query returns:

```text
FROM cases WHERE id in query("Critical failures")
```

- The saved query must return the same entity, and in rows mode.
- Its `ORDER BY` and `LIMIT` are ignored.
- A saved query can refer to another query up to three levels deep. A cycle is refused.
- You can refer only to queries you can see. An unknown or hidden name returns `unknown_saved_query`.

## Archived cases

A query over cases leaves out archived cases unless the query names `archived` itself. The same rule applies to a related-record condition over cases, such as `exists(cases ...)`.

```text
FROM cases WHERE title ~ "login"                 -- archived cases are left out
FROM cases WHERE archived = true                 -- the condition decides
```

A query over results is different. Archiving keeps a case's execution history, so `FROM results` (and `FROM runCases`) includes the results of archived cases. To leave them out, name the case as live:

```text
FROM results WHERE case.archived = false         -- the results of archived cases are left out
```

## Limits

| Limit                                   | Value                                               |
| --------------------------------------- | --------------------------------------------------- |
| Query text                              | 4,000 characters                                    |
| Values in one `in (...)` list           | 200                                                 |
| `LIMIT` (rows per page)                 | 50 by default, 500 at most                          |
| `OFFSET`                                | 10,000 at most                                      |
| Rows counted                            | Exact up to 10,000, then **10,000+**                |
| Groups per page                         | 1,000 at most                                       |
| Relation nesting (`exists` and `count`) | Two levels                                          |
| `in query()` references                 | Three levels. Cycles are refused.                   |
| CSV export                              | 10,000 rows, or 1,000 groups for an aggregate query |
| Saved query name                        | 120 characters, unique in the organization          |
| Saved query description                 | 2,000 characters                                    |

## Errors

Each error has a code, a message, and the place in the query where it occurs. Through the API, each error appears in `details.errors` with the same code. See [Errors](https://docs.probara.net/en/developers/api/pql/#errors).

### Syntax errors

| Code                      | What it means                                                                                                      | What to do                                             |
| ------------------------- | ------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------ |
| `unexpected_character`    | The query has a character PQL does not use, such as `#`.                                                           | Remove it, or put it in quotes if it is text.          |
| `unterminated_string`     | A quote is not closed.                                                                                             | Add the closing quote.                                 |
| `missing_from`            | The query does not start with `FROM`.                                                                              | Start the query with `FROM` and an entity.             |
| `unexpected_token`        | A word or symbol is in the wrong place. A clause repeated twice, or a decimal `LIMIT` or `OFFSET`, shows this too. | Check the order of the clauses, and use whole numbers. |
| `unexpected_end_of_query` | The query ends too early, often after `AND`.                                                                       | Finish the condition.                                  |
| `expected_expression`     | A value is missing, as in `in ()`.                                                                                 | Add the value or the field.                            |
| `unclosed_parenthesis`    | A `(` has no matching `)`.                                                                                         | Add the `)`.                                           |
| `nesting_too_deep`        | The parentheses are nested too deeply.                                                                             | Simplify the condition.                                |
| `invalid_duration`        | A duration uses a unit PQL does not have, such as `3x`.                                                            | Use `m`, `h`, `d`, `w`, `mo`, `q` or `y`.              |
| `number_out_of_range`     | A number is too large or not finite.                                                                               | Use a smaller number.                                  |
| `query_too_long`          | The query is longer than 4,000 characters.                                                                         | Shorten it, or move part of it into a saved query.     |

### Names and functions

| Code                        | What it means                                                                     | What to do                                               |
| --------------------------- | --------------------------------------------------------------------------------- | -------------------------------------------------------- |
| `unknown_entity`            | The name after `FROM` is not an entity.                                           | Use one of the nine entities.                            |
| `unknown_field`             | The field is not on this entity. The message can suggest the closest name.        | Check the entity's table in [Entities](#entities).       |
| `unknown_custom_field`      | No custom field of this entity has the title.                                     | Check the title in your custom field settings.           |
| `unknown_function`          | The name is not a PQL function.                                                   | Check the [functions](#functions) table.                 |
| `not_a_function`            | A field name was written as a function.                                           | Remove the parentheses.                                  |
| `function_arity`            | The function has the wrong number of arguments.                                   | Check the function's form in the table.                  |
| `invalid_argument`          | An argument has the wrong form, such as a `startOf` offset with no unit.          | Use a signed duration, such as `-1mo`.                   |
| `function_not_available`    | The function does not apply to this entity, such as `passRate()` on cases.        | Use `passRate()` on results or runs.                     |
| `unknown_time_unit`         | The time unit is not one PQL knows.                                               | Use `hour`, `day`, `week`, `month`, `quarter` or `year`. |
| `unknown_group`             | No group in the organization has this name.                                       | Check the group name.                                    |
| `set_function_placement`    | `membersOf()`, `activeMembers()` or `formerMembers()` is used outside `in (...)`. | Put it inside `in (...)` on a user field.                |
| `unknown_relation`          | The relation is not on this entity.                                               | Check the [relations](#relations) table.                 |
| `relation_nesting_too_deep` | Sub-queries nest more than two levels deep.                                       | Flatten the query.                                       |
| `relation_count_placement`  | `count(relation)` is used without a comparison.                                   | Compare it with a number, such as `count(results) >= 1`. |
| `relation_placement`        | `exists` is used in `HAVING`.                                                     | Move it to `WHERE`.                                      |
| `unknown_saved_query`       | No saved query you can see has this name.                                         | Check the name. You see only the queries you can access. |
| `history_not_available`     | Change history is not kept for this field.                                        | See [Fields with history](#fields-with-history).         |
| `history_placement`         | `was` or `changed` is used in `HAVING`.                                           | Move it to `WHERE`.                                      |

### Values and operators

| Code                        | What it means                                                           | What to do                                   |
| --------------------------- | ----------------------------------------------------------------------- | -------------------------------------------- |
| `operator_not_allowed`      | The operator does not apply to this field.                              | Check the [operators](#operators) table.     |
| `not_nullable`              | `is empty` is used on a field that always has a value.                  | Remove it.                                   |
| `type_mismatch`             | The value's type does not match the field, such as text for a number.   | Use a value of the right type.               |
| `left_side_must_be_field`   | The left side of a comparison is not a field.                           | Put the field on the left.                   |
| `not_boolean`               | A field is used as a condition but is not a yes-or-no field.            | Compare it, such as `archived = true`.       |
| `unquoted_text_value`       | A text value has no quotes.                                             | Put it in quotes.                            |
| `unknown_value`             | The value is not one of the options, members or names the field allows. | Use one of the values the message lists.     |
| `invalid_date_format`       | A date is not `YYYY-MM-DD` or `YYYY-MM-DD HH:mm`.                       | Write the date in one of those forms.        |
| `invalid_date`              | The date does not exist, such as `2026-02-30`.                          | Use a real date.                             |
| `relative_duration_in_date` | A duration is used as a date, such as `7d`.                             | Use a time function, such as `now(-7d)`.     |
| `invalid_duration_unit`     | A month, quarter or year is used on a fixed-duration field.             | Use `m`, `h`, `d` or `w` on duration fields. |
| `in_list_too_long`          | An `in (...)` list has more than 200 values.                            | Split the list.                              |

### Aggregates, grouping and paging

| Code                       | What it means                                                                       | What to do                                            |
| -------------------------- | ----------------------------------------------------------------------------------- | ----------------------------------------------------- |
| `aggregate_not_allowed`    | An aggregate is used in `WHERE` or `GROUP BY`.                                      | Move it to `SELECT` or `HAVING`.                      |
| `not_aggregatable`         | The function does not apply to this field, such as `sum(title)` or `avg(passRate)`. | Use a number, duration or date, or `passRate()`.      |
| `not_groupable`            | The field cannot be grouped, such as text.                                          | Group by another field, or use `bucket()` for a date. |
| `not_grouped`              | A `SELECT` item is neither grouped nor aggregated.                                  | Add it to `GROUP BY`, or wrap it in an aggregate.     |
| `group_by_requires_select` | `GROUP BY` has no `SELECT` list.                                                    | Name the grouped fields and aggregates in `SELECT`.   |
| `having_requires_group_by` | `HAVING` is used with no group and no aggregate.                                    | Add `GROUP BY`, or an aggregate in `SELECT`.          |
| `multi_valued_having`      | `HAVING` filters a tag or multi-select field.                                       | Filter in `WHERE`.                                    |
| `order_requires_group_by`  | `ORDER BY` sorts by an aggregate with no group.                                     | Add `GROUP BY`, or an aggregate in `SELECT`.          |
| `position_out_of_range`    | A position does not match a `SELECT` item.                                          | Use a number from 1 to the number of `SELECT` items.  |
| `not_sortable`             | The field cannot be sorted, such as a user or tag field.                            | Sort by another field.                                |
| `invalid_select_item`      | A `SELECT` item is neither a field nor an aggregate.                                | Use a field, or an aggregate such as `count()`.       |
| `invalid_bucket`           | `bucket()` has no date field, or no unit.                                           | Use `bucket(created, day)`.                           |
| `duplicate_alias`          | The alias is already used in `SELECT`.                                              | Choose another name.                                  |
| `limit_out_of_range`       | `LIMIT` is outside 1 to 500.                                                        | Use a number from 1 to 500.                           |
| `offset_out_of_range`      | `OFFSET` is above 10,000.                                                           | Use a number up to 10,000.                            |

When the server does not support a construct yet, the query returns **Not available yet** in the editor, with the construct named. Remove that part of the query for now.

## Worked examples

Each example answers one common question. Replace the names with your own suites, milestones and environments.

| Question                                                     | Query                                                                                                                                                 |
| ------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| Which cases failed in their latest run on staging?           | `FROM cases WHERE lastResult(environment = "staging").status = failed ORDER BY title`                                                                 |
| Which critical cases failed last on staging?                 | `FROM cases WHERE priority = "Critical" AND lastResult(environment = "staging").status = failed ORDER BY title`                                       |
| Which defects were reopened in the last 14 days?             | `FROM defects WHERE status changed from resolved to open after now(-14d)`                                                                             |
| Which defects assigned to me were reopened in 30 days?       | `FROM defects WHERE assignee = currentUser() AND status changed from resolved to open after now(-30d)`                                                |
| Which flaky cases have no pass in the last 30 days?          | `FROM cases WHERE isFlaky = true AND NOT exists(results where status = passed AND executed > now(-30d))`                                              |
| Which cases have never been executed?                        | `FROM cases WHERE NOT exists(results)`                                                                                                                |
| What is the pass rate per week?                              | `SELECT bucket(executed, week) AS period, passRate() AS rate, count() AS results FROM results GROUP BY 1 ORDER BY 1`                                  |
| How are this month's results split by status?                | `SELECT status, count() AS n FROM results WHERE executed >= startOf(month) GROUP BY status ORDER BY n DESC`                                           |
| Which open defects are there by severity?                    | `SELECT severity, count() AS n FROM defects WHERE status != closed GROUP BY severity ORDER BY n DESC`                                                 |
| How many failed runs did each environment have last month?   | `SELECT environment, count() AS n FROM runs WHERE status = failed AND closed > startOf(month, -1mo) AND closed < startOf(month) GROUP BY environment` |
| Who executed the most results this week?                     | `SELECT executedBy, count() AS n FROM results WHERE executed > now(-7d) GROUP BY executedBy ORDER BY n DESC`                                          |
| Which runs closed this quarter have the best pass rate?      | `FROM runs WHERE state = closed AND closed > startOf(quarter) ORDER BY passRate DESC`                                                                 |
| Which cases in the Checkout tree changed priority this week? | `FROM cases WHERE suite under "Checkout" AND priority changed after startOf(week)`                                                                    |
| Which cases have Risk set to High in the Login suite?        | `FROM cases WHERE cf["Risk"] = "High" AND suite under "Login"`                                                                                        |
| Which milestones are active and past their due date?         | `FROM milestones WHERE status = active AND due < now()`                                                                                               |

## Next step

- Write and run queries in [Queries](https://docs.probara.net/en/guides/queries/).
- Put a saved query on a dashboard in [Dashboards](https://docs.probara.net/en/guides/dashboards/#query-widgets).
- Send queries from code with the [PQL API](https://docs.probara.net/en/developers/api/pql/).
