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.
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.
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:
db_query calls from recent traces into drafts;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.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"
}
name. Use 3–64 lower-case letters, digits and _, starting with a letter. A name is unique per datasource.description. One line of up to 200 characters, with no control characters. It is what the agent reads, so state what the query answers. Business terms such as "active" belong in the body, not in a parameter the agent has to choose. The headings goclaw writes into tool descriptions are refused here, and so are they in enum values.wiki_slug (optional). Names the org wiki page that defines the terms the query uses.body. One read-only query: SELECT, WITH, SHOW, DESCRIBE or EXPLAIN. Write each parameter as {name}.{name} inside a string literal, a quoted identifier or a comment is left alone.{name:Type} yourself, or { name } with spaces. The type comes from the parameter's declaration.$1 on PostgreSQL, ? on MySQL. PostgreSQL's jsonb ? operator is fine.# starts a comment and a backslash escapes inside quotes, so a {name} in a MySQL comment is not a placeholder.params. Each parameter is either required or has a default. The types are:| 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.
string and enum are bound without a cast, so the server reads them as the column's own type, as it would a literal. An enum, uuid or citext column compares as itself. Where PostgreSQL cannot tell the type (SELECT {q}, {q} IS NULL), the Test run shows the error; write {q}::text in the body.string or enum value is refused.date must fall between 1970-01-01 and 2149-06-06, and a datetime between 1970 and 2105.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().
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"}
test. Runs any version, draft or approved, through the same path as db_query and returns at most 20 rows. The query is stopped there, not left to run to the end. No hook or trace sees a test run; the gateway log records it.approve. Takes the hash that the save or the listing (GET /api/saved-queries?datasource=…) showed. If the version differs from what you saw, the approval is refused with 409.db_query runs a query, it works out the hash again. A query whose content changed in the database, or whose datasource now reaches another database, does not run. That includes a datasource that was deleted and added again under the same name, and a snapshot imported where the name means another database.approval_holds: false. To use it on the new database, save it again and approve that version.revoke. Withdraws the approval.deprecate. Stops offering and running a query but keeps its approval. Approving a version again undoes it.delete. Withdraws the query and hides it from the listing. Its versions are kept. If a query of the same name is made again, its versions go on from the next number, so a version that an old trace names is never other SQL.mine. Scans agent_traces for repeated successful db_query calls on the datasource, parameterizes their filter literals and saves any new shapes as drafts.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.
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.
A SOP step can bind one or more approved saved queries directly in its queries array ([{template, datasource, params, as, max_rows}]):
queries_shadow is false (the default), the step executes those saved queries directly through the worker pool and db_query with zero LLM calls and zero tokens.<as> (the Markdown table, defaulting to the template name when as is omitted) and <as>_ref (the data_ref ID in the output store, ready for spreadsheet(action: "create", data_ref: ...)).params may be literals, {{run.date±Nd}}, {{run.time±Nh|m}} (evaluated against the run's StartedAt in the SOP's timezone, or the server's local timezone when unset), or {{steps.<id>.outputs.<name>}}.queries_shadow: true runs the bound queries in shadow mode alongside the normal LLM step and records the outcome (version, hash, rows, bytes, ms, error) in the step result's query_runs without altering the step's output.sop_only. A saved query marked sop_only: true (via the SOP only checkbox in the editor or {"action": "sop_only", "sop_only": true} on /api/saved-queries) is hidden from chat tool descriptions, does not pin db_query in chat, and runs only inside a SOP step.GET /api/saved-queries and lifecycle responses (save, approve, revoke, deprecate, sop_only, delete) include used_by, the list of SOP IDs referencing that query. Approving, revoking, deprecating or deleting a saved query appends a saved_query_<action> entry to each referencing SOP's change_log.cron_auto_paused entry is recorded in the SOP's change_log, and a single alert is sent.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:
"project = {p}") is refused, so a query-language expression (JQL, UQL, PromQL, Lucene) is never built from a parameter value.gitlab, git_workspace) every value a parameter can take must keep the call read-only: a preset whose enum parameter has a value that would make it a write is refused when saved, and a call rendered from any preset is checked again when it runs.save is a new version; test runs one version with values; approve takes the version and its hash. The hash covers the kind, server, target, name, description, arguments, parameters and, for an MCP preset, the server's transport and address as configured, so a preset approved against one server does not run against another. revoke, deprecate and delete work as they do for saved queries.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:
save_preset_draft: {"name": "...", "description": "...", "args": {...}, "params": [...]} to it. args is the template with its placeholders; the draft is version 1, unapproved.POST /api/tool-presets {"action": "mine"} reads the last seven days of successful chat traces (agent_traces; the 500 newest, cron, webhook, SOP and subagent sessions left out), groups read-only tool calls and MCP call_tool calls by shape, with literal values turned into parameters, and saves a draft for every shape seen in at least two sessions by at least two people. db_query calls are left to saved-query mining. An existing or deleted preset of the mined name is never written over.max_rows_to_read, max_bytes_to_read). A run stops reading at the row cap, and the database stops the query at its time limit.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.