Saved Queries

A saved query is a named, persisted record of an SQL query bound to a connection. It backs dashboard tiles and provides a stable reference for queries you want to reuse across users in your organization.

Required scope: query:execute for all operations. update and delete are additionally restricted to the saved query’s creator.

Visibility

Each saved query has a visibility value that controls who can see it:

VisibilityWho can see it
private (default)The creator only.
orgAnyone authenticated for the organization.
embedAnyone authenticated for the organization, plus embed contexts.

sql and connection_id can be updated by the saved query’s owner or an organization admin. Updating either field clears approval because the reviewed query text or source connection has changed.

List

GET /api/v1/saved-queries

Returns saved queries the caller can see — their own private rows plus everything visibility=org or visibility=embed for the organization. Admins see all rows including other users’ private ones.

List by dashboard

GET /api/v1/dashboards/{dashboard_id}/saved-queries

Returns the id and name of every saved query referenced by a tile on the given dashboard.

For embedded dashboard rendering, prefer GET /api/v1/dashboards/{dashboard_id}/manifest and dashboard-scoped tile data. The manifest exposes tile keys, visualization encodings, and each tile’s data_url; saved-query discovery is primarily for administration and legacy integrations.

Tiles backed by ad-hoc inquiry turns are skipped. Soft-deleted saved queries are skipped. Results are de-duplicated when the same saved query backs multiple tiles. Standard dashboard read permissions apply (admins see everything in the org; dashboard-only users see only assigned dashboards).

Response

{
  "saved_queries": [
    { "id": "5f1c7e3a-…", "name": "Revenue by region" },
    { "id": "8a4b9d12-…", "name": "Active users (7d)" }
  ],
  "total": 2
}

Create from SQL

POST /api/v1/saved-queries

Returns 201 with the created saved query.

Request body

FieldTypeRequiredDescription
sqlstringYesThe SQL to persist
connection_idstring (UUID)YesConnection the SQL targets
namestringYesHuman-readable label, max 200 chars
descriptionstringNoLong-form description
visibility"private" | "org" | "embed"NoDefault "private"

Example

curl -X POST https://app.answerlayer.io/api/v1/saved-queries \
  -H "X-API-Key: $ANSWERLAYER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Top customers by revenue",
    "sql": "SELECT customer_id, SUM(amount) FROM orders GROUP BY customer_id ORDER BY 2 DESC LIMIT 10",
    "connection_id": "d290f1ee-6c54-4b01-90e6-d701748f0851",
    "visibility": "org"
  }'

Create from an inquiry turn

POST /api/v1/saved-queries/from-inquiry-turn

Promotes the SQL the agent generated for an existing inquiry turn into a saved query. Useful for “save this answer” UX after a user finds a question they want to keep.

Request body

FieldTypeRequiredDescription
inquiry_turn_idstring (UUID)YesThe turn whose SQL to promote
namestringYesHuman-readable label, max 200 chars
descriptionstringNoLong-form description
visibility"private" | "org" | "embed"NoDefault "private"

The connection is inherited from the turn’s session.

Read

GET /api/v1/saved-queries/{saved_query_id}

Returns 404 if the row doesn’t exist or the caller’s visibility doesn’t include it.

Execute

POST /api/v1/saved-queries/{saved_query_id}/execute

Resolves the saved query, then runs its SQL against its connection in one call. Use this when you’d otherwise have to GET the saved query and then POST to /query/{connection_id} yourself.

The connection_id and sql come from the saved query record, not the request body.

Returns 404 if the saved query is missing or the caller’s visibility doesn’t include it, or if the underlying connection has been deleted. The audit log records resource_type=saved_query, resource_id=<saved_query_id> so the saved query is the audit subject — not a free-form SQL string.

Request body

FieldTypeRequiredDescription
paramsobjectNoOptional parameter map for parameterized queries
row_limitintNoCaps the rows fetched from the source. Default 1000, max 100000.
timeoutintNoSeconds. Default 30, max 120.

Response

The response is a paginated envelope:

{
  "columns": ["customer_id", "revenue"],
  "rows": [["c_1029", 84210.55], ["c_0837", 79110.10]],
  "row_count": 2,
  "total_rows": 2,
  "execution_time_ms": 412,
  "next_cursor": null,
  "result_handle": null,
  "expires_at": null
}

A result at or below 1,000 rows is returned inline — every row in this response, with next_cursor and result_handle both null. A larger result is materialized: the response carries the first page plus a result_handle and a next_cursor. Page through the rest with GET /api/v1/query-results/{handle}.

Example

curl -X POST https://app.answerlayer.io/api/v1/saved-queries/$SAVED_QUERY_ID/execute \
  -H "X-API-Key: $ANSWERLAYER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"row_limit": 100}'

Update

PATCH /api/v1/saved-queries/{saved_query_id}

Partial update. name, description, visibility, sql, and connection_id are mutable. Updating sql or connection_id clears approval.

Only the saved query’s owner or an organization admin may update it.

Delete

DELETE /api/v1/saved-queries/{saved_query_id}

Soft-deletes the row. Saved queries referenced by dashboard tiles remain referenced; the tile starts returning a not found error until the reference is updated. Returns 204.

Only the owner or an organization admin may delete.