🏠 Home ← Docs Index
Open Beta 📖 Interactive Reader Raw .md llms.txt

Saved queries

A saved query is SQL that an admin wrote and approved for one datasource. It has typed parameters, and db_query runs it by name. When a saved query covers a question, the agent fills in values and writes no SQL, so it has no table or column name to guess. A report built on one returns the same answer every run.

db_query(datasource: "reports-db", template: "active_orgs",
         params: '{"start_date": "2026-10-01", "end_date": "2026-11-01"}')

Questions that no saved query covers still go through ordinary db_query SQL.

How the agent finds them

When at least one saved query is approved, db_query is always offered (pinned). Its description lists the approved queries with their datasource, description and parameters, for example:

Saved queries, approved by an admin. When one answers the question, run it by name instead of writing SQL: …
- active_orgs [reports-db]: Organisations with at least min_requests events in a period — params: start_date date required, end_date date required, min_requests int ≥1 =100

The list shows at most 20 queries, or about 3000 characters; any further queries are listed by name only (30 names, then a count). It shows only the queries this run can actually run. In a delegated run, that means PostgreSQL datasources with a delegated_role. The agent picks one from the list. It does not depend on the question sharing words with the query, so a question in Mongolian finds an English description too.

Give the agent either query (its own SQL) or template, not both. A call with both is refused.

Writing or drafting one

Saved queries run on PostgreSQL, MySQL and ClickHouse datasources, not on MongoDB. A saved query enters the list as an unapproved draft in three ways:

  1. On the dashboard. Open Integrations, and click Saved queries on a datasource's card. The dialog lists the datasource's queries and shows each one's status: draft, approved version, approval that no longer holds, or deprecated. From there you can:
    • write a new query, or a new version of an existing one;
    • click Mine from traces to turn repeated successful db_query calls from recent traces into drafts;
    • Test a version with values (20 rows at most);
    • Approve it, which shows the exact SQL, parameters and hash first;
    • revoke, deprecate or delete it.
  2. Directly from the agent (save_draft). In a conversation, the agent can pass save_draft: "<name>" and description: "<one line>" to db_query (writing parameters as {name} in query and passing sample values in params, or passing a plain query whose filter literals are extracted as parameters automatically). db_query runs the query against the database first; only if it succeeds is it saved as a draft version.
  3. Automatically from repeated runs. When the same read-only query shape (excluding schema probes such as SHOW, DESCRIBE, information_schema and LIMIT 1) succeeds at least 3 times or across at least 2 sessions, GoClaw extracts its date, datetime, string and numeric filter literals into typed {name} parameters (with the observed values as defaults) and saves it as a draft. The daily trace_prune maintenance job also mines recurring queries before pruning old traces. Deleting a draft permanently vetoes that SQL shape so automatic mining does not recreate it.

Through the API. The dialog uses the API below, which is also open to any client with the gateway token.

Make saved queries through the dashboard API, /api/saved-queries. That API takes the gateway token.

POST /api/saved-queries
{
  "action": "save",
  "datasource": "reports-db",
  "name": "active_orgs",
  "description": "Organisations with at least min_requests events in a period",
  "wiki_slug": "metrics/active-organisation",
  "body": "SELECT org_id, count(*) AS events FROM events WHERE day >= {start_date} AND day < {end_date} GROUP BY org_id HAVING count(*) >= {min_requests} ORDER BY events DESC",
  "params": [
    {"name": "start_date", "type": "date", "required": true},
    {"name": "end_date", "type": "date", "required": true},
    {"name": "min_requests", "type": "int", "default": 100, "min": 1}
  ],
  "note": "first version"
}
type value PostgreSQL ClickHouse MySQL
date YYYY-MM-DD $n::date {p:Date} ?
datetime YYYY-MM-DD HH:MM:SS (UTC) or RFC 3339 (converted to UTC) $n::timestamptz {p:DateTime('UTC')} ?, as UTC text
int a whole number; min, max. Above 2^53, give it as text in params $n::bigint {p:Int64} ?
number a number; min, max. Text is bound exactly $n::numeric {p:Float64} ?
string text, at most max_len (default 200) $n {p:String} ?
enum one of values $n {p:String} ?
bool true / false $n::boolean {p:Bool} ?

Values are always passed to the driver as bound parameters, never spliced into the text. A value that is not of its parameter's type is refused, and the error names what is allowed.

When you save, the body is checked the same way db_query checks SQL. Every placeholder must be declared, and every declared parameter must be used. The query must be read-only, and it must not call anything that reaches past the database, such as url(), pg_read_file() or query_to_xml().

Versions and approval

Every save adds a version, and versions are never changed. A new version is a draft, and db_query keeps running the approved version until the new one is approved.

POST /api/saved-queries {"action": "test", "datasource": "reports-db", "name": "active_orgs", "version": 2,
                         "values": {"start_date": "2026-10-01", "end_date": "2026-11-01"}}
POST /api/saved-queries {"action": "approve", "datasource": "reports-db", "name": "active_orgs", "version": 2, "hash": "<hash>"}
POST /api/saved-queries {"action": "mine", "datasource": "reports-db"}

No tool can approve a saved query. Only this API can, behind the gateway token. That token is shared by the dashboard and any other client of the API, so the approval records approved_via: "api" and not a person.

What a run looks like

db_query resolves the saved query before the admin's hooks run. A hook on db_query therefore sees the query's SQL in query, and args_match works on saved queries as it does on SQL the agent writes. The cache key includes the template, its version and the values, and the trace records the template and the values.

The result starts with Saved query <name> v<N> #<hash>: on its first line. That line stays when the result is spilled or has no rows. If a saved query fails, the agent is told that it failed and not to write its own SQL in its place, because the failure is an admin's to fix.

A delegated run (a SOP acting for a person) runs a saved query exactly as it runs other SQL. That means PostgreSQL only, under the datasource's delegated_role, reading only the granted views. See the SOP virtual level.

A saved query is held to the same limits on the database as other db_query SQL: a time limit the server enforces, and read-only where the engine can enforce it. See Database Connections. A ClickHouse saved query cannot have a SETTINGS clause; one saved before this rule stops running until it is saved again without it.

Binding to a SOP step

A SOP step can bind one or more approved saved queries directly in its queries array ([{template, datasource, params, as, max_rows}]):

Tool and MCP presets

A preset does for a read-only tool call what a saved query does for SQL: an admin approves the call once, with its arguments as a template, and the agent runs it by name with parameter values. Presets are managed through POST /api/tool-presets and the Presets dialog on the Tools, MCP and Integrations pages; db_query has saved queries instead.

A preset is:

Field Meaning
kind tool for a registered tool, mcp for a tool on an MCP server.
server For mcp: the MCP server's name in config.yaml.
target The tool the preset calls: the registered tool's name, or the MCP tool's name on that server.
name What the agent calls it by. Unique per tool (tool) or per server (mcp).
description One line on what the call answers.
args The call's arguments as a JSON object. A string value may be a placeholder, {name}.
params What the placeholders take: name, type (date, datetime, int, number, string, enum, bool), required, default, min/max or values, as for a saved query.

Rules:

Running one: a tool with an approved preset gets two more arguments, preset and preset_params, and its description lists the presets it has. The arguments the preset fixes are not given; preset with raw arguments (arguments for mcp, or another action) is refused.

gitlab(preset: "open_blockers", preset_params: {"project": "platform/api"})
mcp(server: "jira", preset: "my_open_issues", preset_params: {"assignee": "alice"})

For mcp, server is needed only when the same preset name exists on more than one server.

Drafts come from two places, and an admin approves them as any version:

Not yet

Storage

Saved queries are stored in saved_queries and saved_query_versions. Tool and MCP presets are stored in tool_presets and tool_preset_versions. All four tables are included in every .gclaw snapshot.