Saved Queries
Save any SQL query and it becomes an instant REST API endpoint with auth, caching, and parameters.
Quick Start
Create a saved query, then call it via curl or the CLI:
# Run a saved querycurl -X POST /api/v1/workspaces/{workspace_id}/saved-queries/{name}/run \ -H "Authorization: Bearer rq_your_api_key"
# With parameterscurl -X POST /api/v1/workspaces/{workspace_id}/saved-queries/{name}/run \ -H "Authorization: Bearer rq_your_api_key" \ -H "Content-Type: application/json" \ -d '{"parameters": {"min_revenue": 1000}}'# Run a saved querycurl -X POST /api/v1/workspaces/{workspace_id}/saved-queries/{name}/run \ -H "Authorization: Bearer rq_your_api_key"
# With parameterscurl -X POST /api/v1/workspaces/{workspace_id}/saved-queries/{name}/run \ -H "Authorization: Bearer rq_your_api_key" \ -H "Content-Type: application/json" \ -d '{"parameters": {"min_revenue": 1000}}'Creating a Saved Query
From the Dashboard
- Write your SQL in the Query Editor
- Click Save in the toolbar
- Give it a name, optional description, and cache TTL
- Parameters are auto-detected from
:param_nameplaceholders in the SQL
Via the API
curl -X POST /api/v1/workspaces/{workspace_id}/saved-queries \ -H "Authorization: Bearer rq_your_api_key" \ -H "Content-Type: application/json" \ -d '{ "name": "top-customers", "sql": "SELECT name, SUM(amount) as revenue FROM my_stripe.charges GROUP BY name ORDER BY revenue DESC LIMIT 10", "description": "Top 10 customers by revenue", "cache_ttl_seconds": 300 }'curl -X POST /api/v1/workspaces/{workspace_id}/saved-queries \ -H "Authorization: Bearer rq_your_api_key" \ -H "Content-Type: application/json" \ -d '{ "name": "top-customers", "sql": "SELECT name, SUM(amount) as revenue FROM my_stripe.charges GROUP BY name ORDER BY revenue DESC LIMIT 10", "description": "Top 10 customers by revenue", "cache_ttl_seconds": 300 }'Parameters
Saved queries support typed parameters. Define them when creating the query, then pass values at runtime.
{ "name": "revenue-by-country", "sql": "SELECT country, SUM(amount) as total FROM my_stripe.charges WHERE country = :country AND amount > :min_amount GROUP BY country", "parameters": [ { "name": "country", "type": "string", "required": true, "default": null }, { "name": "min_amount", "type": "integer", "required": false, "default": 0 } ]}{ "name": "revenue-by-country", "sql": "SELECT country, SUM(amount) as total FROM my_stripe.charges WHERE country = :country AND amount > :min_amount GROUP BY country", "parameters": [ { "name": "country", "type": "string", "required": true, "default": null }, { "name": "min_amount", "type": "integer", "required": false, "default": 0 } ]}| Type | Description |
|---|---|
| string | Text value |
| integer | Whole number |
| float | Decimal number |
| boolean | true / false |
| timestamp | A point in time. Accepts now, now-7d, ISO 8601, or epoch. Resolved server-side in UTC. |
Pass parameters when running:
curl -X POST /api/v1/workspaces/{workspace_id}/saved-queries/revenue-by-country/run \ -H "Authorization: Bearer rq_your_api_key" \ -H "Content-Type: application/json" \ -d '{"parameters": {"country": "FR", "min_amount": 5000}}'curl -X POST /api/v1/workspaces/{workspace_id}/saved-queries/revenue-by-country/run \ -H "Authorization: Bearer rq_your_api_key" \ -H "Content-Type: application/json" \ -d '{"parameters": {"country": "FR", "min_amount": 5000}}'Time filter
Give a query a time range and any chart or page built on it shows a time picker when published (presets from 15 minutes to 90 days, plus a custom range).
The quick way, no SQL: in the query editor, open Time filter, pick a date or timestamp column, and rawquery adds the range to your SQL for you. The rewritten query appears so you can review it, and two timestamp params named from and to are created.
By hand it is one line: name your bounds :from and :to and use a half-open range so adjacent windows never double-count a row.
SELECT date_trunc('day', created_at) AS day, count(*) AS nFROM eventsWHERE created_at >= :from AND created_at < :toGROUP BY 1SELECT date_trunc('day', created_at) AS day, count(*) AS nFROM eventsWHERE created_at >= :from AND created_at < :toGROUP BY 1A query that declares both from and to gets them typed as timestamp automatically (defaults now-7d and now). Relative bounds re-resolve on every request, so now-24h slides as the page refreshes. See Charts and Pages for the published picker.
Caching
Set cache_ttl_seconds to
cache results. Cached responses include "cached": true in
the response.
{ "columns": [{ "name": "revenue", "type": "DECIMAL" }], "rows": [[12580.50]], "row_count": 1, "execution_time_ms": 2, "cached": true, "cached_at": "2026-02-14T12:00:00+00:00"}{ "columns": [{ "name": "revenue", "type": "DECIMAL" }], "rows": [[12580.50]], "row_count": 1, "execution_time_ms": 2, "cached": true, "cached_at": "2026-02-14T12:00:00+00:00"}Cache is keyed on query name + parameter values. Different parameter combinations are cached separately.
API Reference
GET /api/v1/workspaces/:workspace_id/saved-queries
List all saved queries in a workspace.
[ { "id": "uuid", "name": "top-customers", "description": "Top 10 by revenue", "parameter_count": 0, "created_at": "2026-02-13T12:00:00Z" }][ { "id": "uuid", "name": "top-customers", "description": "Top 10 by revenue", "parameter_count": 0, "created_at": "2026-02-13T12:00:00Z" }]GET /api/v1/workspaces/:workspace_id/saved-queries/:name
Get a saved query by name, including its SQL and parameter definitions.
POST /api/v1/workspaces/:workspace_id/saved-queries
Create a new saved query.
| Field | Type | Required | Description |
|---|---|---|---|
| name | string | Yes | URL-safe name (used in the endpoint path) |
| sql | string | Yes | SQL query (DuckDB syntax) |
| description | string | No | Human-readable description |
| parameters | array | No | Parameter definitions (name, type, required, default) |
| cache_ttl_seconds | integer | No | Cache lifetime in seconds (null = no cache) |
PATCH /api/v1/workspaces/:workspace_id/saved-queries/:name
Update a saved query. All fields optional - only provided fields are changed.
DELETE /api/v1/workspaces/:workspace_id/saved-queries/:name
Delete a saved query. Returns 204 on success.
POST /api/v1/workspaces/:workspace_id/saved-queries/:name/run
Execute a saved query and return results.
Request Body (optional)
{ "parameters": { "country": "FR", "min_amount": 5000 }}{ "parameters": { "country": "FR", "min_amount": 5000 }}Response
{ "columns": [ { "name": "country", "type": "VARCHAR" }, { "name": "total", "type": "DECIMAL" } ], "rows": [["FR", 48250.00]], "row_count": 1, "execution_time_ms": 42, "cached": false}{ "columns": [ { "name": "country", "type": "VARCHAR" }, { "name": "total", "type": "DECIMAL" } ], "rows": [["FR", 48250.00]], "row_count": 1, "execution_time_ms": 42, "cached": false}CLI
# List saved queriesrq queries
# Run a saved queryrq queries run top-customers
# Run with parametersrq queries run revenue-by-country --param country=FR --param min_amount=5000# List saved queriesrq queries
# Run a saved queryrq queries run top-customers
# Run with parametersrq queries run revenue-by-country --param country=FR --param min_amount=5000