# Saved queries API

> Create, list, read, change, delete and run organization saved queries, and decide who sees each one, over the integrator API.

A saved query is a [PQL](https://docs.probara.net/en/developers/api/pql/) query with a name, an optional description, and a visualization that says how its result is drawn. It belongs to the organization, not to a project. Each one has a server-assigned ULID, a creator, and a visibility: `public` (every member) or `private` (its creator, the members and [user groups](https://docs.probara.net/en/developers/api/user-groups/) granted access, and organization owners and admins).

Saved queries are available on the **Pro** and **Enterprise** plans, the same plans as queries and dashboards. On any other plan every endpoint on this page returns `403 forbidden`.

| Method and path                              | What it does                                                                       |
| -------------------------------------------- | ---------------------------------------------------------------------------------- |
| `GET /api/v1/queries`                        | Lists the saved queries you can see, most recently updated first.                  |
| `POST /api/v1/queries`                       | Creates a saved query.                                                             |
| `GET /api/v1/queries/{savedQueryUlid}`       | Reads one saved query, with its text and drawing.                                  |
| `PUT /api/v1/queries/{savedQueryUlid}`       | Replaces the whole state: text, drawing, name, description, visibility and grants. |
| `DELETE /api/v1/queries/{savedQueryUlid}`    | Deletes the saved query and its grants. Answers `204`.                             |
| `POST /api/v1/queries/{savedQueryUlid}/run`  | Runs the saved text with your own project access.                                  |
| `GET /api/v1/queries/{savedQueryUlid}/usage` | Lists the dashboards that draw the query, for its delete confirmation.             |

## Who sees and who changes a saved query

A member sees a saved query when any of these holds:

- it is `public`;
- they created it;
- they, or one of their user groups, were granted access to a private one.

Organization owners and admins see every saved query.

Creating a query needs the `queries.write` permission: owners, admins and members have it, and viewers can read queries but not create them. Only the creator and organization owners and admins can change, delete, or change the visibility and grants of a query, whatever other access a member holds. If the creator's account is deleted, the query stays and only owners and admins can change it.

A query you cannot see answers `404 not_found` on every call, so a private query never confirms that it exists. A query you can see but cannot change answers `403 forbidden` on `PUT` and `DELETE`.

## The name

A name is unique in the organization, ignoring case: `Staging failures` and `staging FAILURES` are the same name. The rule covers every saved query of the organization, including private ones you cannot see, so a `409 conflict` on create or on a rename can show that a name is taken.

The name is 1 to 120 characters after trimming spaces at both ends.

## Create and replace

`POST /api/v1/queries` takes:

| Field           | Rule                                                                          |
| --------------- | ----------------------------------------------------------------------------- |
| `name`          | Required, 1 to 120 characters.                                                |
| `description`   | Optional, up to 2,000 characters.                                             |
| `pql`           | Required, 1 to 4,000 characters of PQL.                                       |
| `visualization` | Optional. `{ "render": "table" }` by default. See below.                      |
| `accessMode`    | `public` (default) or `private`.                                              |
| `members`       | Optional. The user ULIDs of organization members who may see a private query. |
| `groups`        | Optional. The ULIDs of user groups who may see a private query.               |

`members` and `groups` are up to 200 entries each, without duplicates, and must be empty on a public query. A private query can start with its first grants, saved in the same request.

`PUT /api/v1/queries/{savedQueryUlid}` takes the same fields, and every one is required; send `description` as `null` to clear it. A missing `members` or `groups` list is refused, so it never clears grants by accident. A public query takes empty lists, which removes every grant it held.

The `visualization` object has a `render` and an optional `series`:

- `render` is one of `table`, `bar`, `line`, `donut` or `value`.
- `series` names the result column that splits a chart into one series per value, or is `null`.

The query text is checked when you save it, with the same checks a run makes against your own view of the organization. A text that does not parse or validate answers `422 pql_invalid`, with every error and its span, as the [query endpoint](https://docs.probara.net/en/developers/api/pql/#errors) does. A construct the engine does not run yet answers `422 pql_unsupported`. A reference to another saved query, `in query("name")`, is checked the same way: a name the author cannot see answers `422 pql_invalid`, and a save that would close a cycle of references is refused. Either way nothing is saved.

## Who may see the grant lists

A query returns its grants, `members` and `groups`, only to a caller who can edit it. Anyone else receives `null` for both. Every response carries `canEdit`, so a client knows whether to offer changes.

## List

`GET /api/v1/queries` pages the queries you can see, most recently updated first. Use `page` (from 1) and `pageSize` (up to 200, 50 by default). `q` keeps the queries whose name contains it, ignoring case. A list item carries the summary fields: `ulid`, `name`, `description`, `ownerUserUlid`, `accessMode`, `createdAt`, `updatedAt` and `canEdit`. Read one query for its `pql`, `visualization` and grants.

## Run

`POST /api/v1/queries/{savedQueryUlid}/run` takes an optional `offset`, from 0 to 10,000, in a JSON body; send `{}` to run the first page. It answers the page the [query endpoint](https://docs.probara.net/en/developers/api/pql/) answers for the same text: the same rows or groups, computed with your own project access, with `scope.hiddenProjectCount` counting the projects you cannot open. A text that no longer passes the checks, because the organization's fields changed, answers `422 pql_invalid` or `422 pql_unsupported`.

## Export

`POST /api/v1/queries/{savedQueryUlid}/export` answers the rows of the saved query as CSV, for the caller, under the caller's own project access. It takes no body. The CSV follows the rules of the [query export](https://docs.probara.net/en/developers/api/pql/#export-as-csv). A query the caller cannot see answers `404`, and a text that no longer passes the checks answers `422`.

## Usage

`GET /api/v1/queries/{savedQueryUlid}/usage` answers the dashboards with a widget that draws the query, so you can see what a deletion would leave behind. `dashboards` names the ones you can open, by ULID and name; `hiddenDashboardCount` counts the others, which are never named. A query you cannot see answers `404`. Deleting the query leaves those widgets in their “query deleted” state.

## Errors

| Status | Code                | When                                                                                                                               |
| ------ | ------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| `403`  | `forbidden`         | The plan does not include queries, your role does not include `queries.write` on create, or you may see a query but not change it. |
| `404`  | `not_found`         | No saved query with this ULID that you can see.                                                                                    |
| `409`  | `conflict`          | Another saved query of the organization has this name, ignoring case.                                                              |
| `422`  | `validation_failed` | The body or a path parameter is malformed, or a limit is broken.                                                                   |
| `422`  | `pql_invalid`       | The query text does not parse or validate. `details.errors` lists each error.                                                      |
| `422`  | `pql_unsupported`   | The query text uses a construct a later release adds. `details.construct` names it.                                                |

## Audit

Every change writes an audit event, visible in the [audit log](https://docs.probara.net/en/developers/api/audit-events/):

- `saved_query.created` when a query is created, with its visibility.
- `saved_query.updated` when its name, description, text or drawing changes. It names the fields that changed, never their values.
- `saved_query.access_changed` for each visibility change and grant, with its reason: `mode_toggle`, `member_granted`, `member_cleared`, `group_granted` or `group_revoked`.
- `saved_query.deleted` when a query is deleted.

A request that changes nothing writes no event.
