# Tool reference

> The 17 tools Bonnard can offer an agent, with the console setting, who gets each, its annotations, its description as sent, and its parameters.

Updated: 2026-11-09

A workspace connection offers an agent, the model in an AI client that acts for a person, up to 17 tools. The AI tools settings in the console at `app.bonnard.ai` and the caller's workspace role decide which ones.

Verified 9 Nov 2026.

Each description and input schema is the text the agent receives in `tools/list`, for one PostgreSQL data source with all settings on. The dialect line changes with the data source: go to [SQL dialect by engine](#sql-dialect-by-engine).

Anyone connected means a workspace member, a customer's user, or an API key. Editor or higher is a workspace role. `intent` is in a tool's input only while Call intent is on.

Parameter paths: `a.b` is a field of `a`, and `a[]` is one element of the array `a`. Where a value takes more than one shape, `a[kind=chart]` is the shape whose `kind` is `chart`, and `a[sql]` is the shape that carries `sql`.

## Tools at a glance

Table: The tools at a glance

| Tool | Title | Setting | Who gets it |
| --- | --- | --- | --- |
| `explore_schema` | `Explore schema` | **Explore schema**, always on | Anyone connected |
| `sql_query` | `Run SQL` | **SQL query**, always on | Anyone connected |
| `render_chart` | `Render chart` | **Charts**, on by default | Anyone connected |
| `fetch_view_data` | `Fetch view data` | **Charts** or **Dashboards**, on by default | The widget in the AI client |
| `create_export_file` | `Create export file` | **Export data**, on by default | Anyone connected |
| `save_dashboard` | `Save dashboard` | **Dashboards**, on by default | Anyone connected |
| `delete_dashboard` | `Delete dashboard` | **Dashboards**, on by default | Anyone connected |
| `save_chart` | `Save chart` | **Save charts** and **Export data**, on by default | Anyone connected |
| `explore_charts` | `Explore charts` | **Save charts**, on by default | Anyone connected |
| `delete_chart` | `Delete chart` | **Save charts**, on by default | Anyone connected |
| `explore_docs` | `Explore docs` | **Docs**, always on | Anyone connected, while Charts or Dashboards is on. Editor or higher gets it with both off. |
| `explore_dashboards` | `Explore dashboards` | **Dashboards**, on by default | Anyone connected |
| `render_dashboard` | `Render dashboard` | **Dashboards**, on by default | Anyone connected |
| `search_param_values` | `Search filter values` | **Dashboards**, on by default | The widget in the AI client |
| `get_versions` | `Get context versions` | None | Editor or higher |
| `edit_draft` | `Edit the context draft` | None | Editor or higher |
| `publish_draft` | `Publish the context draft` | **Allow AI clients to publish context changes**, off by default | Editor or higher |

## `explore_schema`

Table: explore_schema at a glance

| Fact | Value |
| --- | --- |
| Setting | **Explore schema**, always on |
| Who gets it | Anyone connected |
| Annotations | `title: Explore schema`, `readOnlyHint: true`, `destructiveHint: false`, `openWorldHint: false` |

Description as sent:

> “Discover the data. Call with no arguments first for an overview of the data source: the most-connected tables and a total count. Pass `search` to find tables related to a concept when you don't know the exact name, `table` for one table's full detail (columns, types, row count, sample rows), or `full: true` to list every table. Table and column names are shown exactly as they must appear in SQL, already quoted where the database requires it. Paste them verbatim; do not add, remove, or change quoting or case.”

Editor or higher gets this text at the end of the description:

> “The user can edit this data source's context, so the overview also reports its gaps and each table's key, and a table's detail gives each description, key, time column and join a from (edit, source or catalog), with \* where the draft changes it, plus the drafted description, key or time column.”

Table: explore_schema parameters

| Parameter | Type | Needed | Description |
| --- | --- | --- | --- |
| `database` | string | No | “Data source key, from explore_schema” |
| `search` | string | No | Quoted below this table. |
| `table` | string | No | “Exact table name to inspect in full: columns, types, row count, and sample rows. Ignored if `search` is also given; takes precedence over `full`.” |
| `full` | boolean | No | “List every table instead of the default overview. Only applies when `search` and `table` are both omitted.” |
| `offset` | integer (min 0) | No | “Start the `full: true` listing at this position instead of the beginning. The listing is paged: each response reports `listing.nextOffset` when more tables remain. Ignored unless `full` is set.” |
| `includeHidden` | boolean | No | “Only on data sources with an event model (PostHog). Skip this source's curation for this call and list every table (and, with `event`, every property). Default false.” |
| `event` | string | No | “Only on data sources with an event model (PostHog). Exact name of one event, for the properties it carries. Not a search: pass a name you already know. To find one, query the event column directly. Takes precedence over `search`, `table` and `full`.” |
| `metric` | string | No | “Key of one defined metric, for its calculation and instructions. The overview lists the keys; a data source without metrics says so. Takes precedence over `search`, `table` and `full`.” |
| `intent` | string (max 300 chars) | No | “Why this call is being made, in 15-25 words, third person. Analytics only; it never changes the result. No names, emails, IDs or row values &mdash; describe the purpose in general terms.” |

`search`, as sent:

> “Find tables when you don't know the exact name: by concept, keyword, or column. Takes precedence over `table` and `full`.”

## `sql_query`

Table: sql_query at a glance

| Fact | Value |
| --- | --- |
| Setting | **SQL query**, always on |
| Who gets it | Anyone connected |
| Annotations | `title: Run SQL`, `readOnlyHint: true`, `destructiveHint: false`, `openWorldHint: false` |

Description as sent:

> “Execute a single read-only SQL SELECT and return rows. Discover names with explore_schema first. Results are capped; add LIMIT/OFFSET to page. Prefer aggregating or filtering in SQL over pulling raw rows: the preview shows at most 250 of them, while the whole bounded result (up to 5000 rows, or 500000 with max_rows) is fetched and captured regardless of what the preview carries. When returned, resultRef identifies the captured bounded rows; pass it to create_export_file instead of rerunning SQL. Set capture_only: true when you only need that reference, to skip the row preview, and max_rows when the capture needs more rows than the standard ceiling holds. Table and column names are shown exactly as they must appear in SQL, already quoted where the database requires it. Paste them verbatim; do not add, remove, or change quoting or case. Dialect: PostgreSQL. Identifier quote: "double quotes". Dates: date_trunc('month', col), now() - interval '7 days'. Cast: col::type. Nulls: coalesce(a, b).”

Table: sql_query parameters

| Parameter | Type | Needed | Description |
| --- | --- | --- | --- |
| `database` | string | No | “Data source key, from explore_schema” |
| `sql` | string | Yes | “A single read-only SELECT statement” |
| `capture_only` | boolean | No | “Return the capture only: resultRef, fields, and row counts, with no rows. Set true when you are capturing a result to hand to create_export_file and will not read the values yourself. Leave unset whenever you intend to inspect, summarise, or chart the rows &mdash; this mode returns none. Pairs with max_rows for a bulk capture.” |
| `max_rows` | integer (greater than 0) | No | “Fetch and capture up to this many rows instead of the standard 5000, for a bulk export. Ceiling: 500000. The preview still never shows more than 250 rows regardless of this value, so pair it with capture_only to skip building one you will not use.” |
| `intent` | string (max 300 chars) | No | “Why this call is being made, in 15-25 words, third person. Analytics only; it never changes the result. No names, emails, IDs or row values &mdash; describe the purpose in general terms.” |

`capture_only` and `max_rows` are in the input only while Export data is on.

## `render_chart`

Table: render_chart at a glance

| Fact | Value |
| --- | --- |
| Setting | **Charts**, on by default |
| Who gets it | Anyone connected |
| Annotations | `title: Render chart`, `readOnlyHint: true`, `destructiveHint: false`, `openWorldHint: false` |
| Renders in | `ui://bonnard/view.html` |

Description as sent:

> “Render one chart, or a carousel of up to 10, and show them to the user. Each entry in `charts` is either a chart the user saved ({ chart_id }) or a chart drawn from a read-only SQL SELECT now ({ sql, chartType?, title?, encode?, fields?, ... }); the two kinds mix freely. The input is always {"charts":[...]}, even for one chart. One entry shows one chart; two or more show a carousel the user pages through in the order given. For an ad-hoc chart, consider running the query through sql_query first to check the shape of the results. Charts take no params: write literal values. The reply summarises what was drawn with a row preview; the user sees the whole chart. Every chart returns its own resultRef and snapshotRef; pass one to create_export_file to download that chart's data or image, or to save_chart to keep the chart. The charts the user saved are listed by explore_charts. Dialect: PostgreSQL. Identifier quote: "double quotes". Dates: date_trunc('month', col), now() - interval '7 days'. Cast: col::type. Nulls: coalesce(a, b).”

Table: render_chart parameters

| Parameter | Type | Needed | Description |
| --- | --- | --- | --- |
| `database` | string | No | “Data source key, from explore_schema” |
| `charts` | array of objects (1 to 10 items) | Yes | Quoted below this table. |
| `charts[chart_id].chart_id` | string | Yes | “A chart the user saved, by the id save_chart returned or explore_charts lists. Runs its stored query again.” |
| `charts[sql].sql` | string | Yes | “A single read-only SQL SELECT statement.” |
| `charts[sql].chartType` | one of `line`, `bar`, `area`, `pie`, `scatter`, `funnel`, `waterfall`, `table` | No | “line, bar, area, pie, scatter, funnel, waterfall or table; omit to infer from the columns. Every option a type takes, with an example: explore_docs topic "charts/&lt;type>", e.g. explore_docs({"topic":"charts/bar"}). Choosing a type: topic "charts".” |
| `charts[sql].title` | string | No | “The chart title.” |
| `charts[sql].encode` | object | No | Quoted below this table. |
| `charts[sql].encode.x` | string | No |  |
| `charts[sql].encode.y` | string or array of strings | No |  |
| `charts[sql].encode.series` | string | No |  |
| `charts[sql].fields` | array of objects | No | “Formats are never inferred from column names: declare percent, currency or compact per column, e.g. [{"name":"revenue","format":"currency","currency":"GBP"},{"name":"gm","format":"percent","label":"Gross margin %"}]. A column alias with spaces or capitals ("Gross margin") is shown exactly as written; a bare snake_case name only has its underscores spaced and its first letter capitalised. Put a title with % or other symbols in label, not in the SQL alias.” |
| `charts[sql].fields[].name` | string | Yes |  |
| `charts[sql].fields[].label` | string | No |  |
| `charts[sql].fields[].format` | one of `number`, `percent`, `currency`, `compact` | No |  |
| `charts[sql].fields[].currency` | string | No |  |
| `preview_rows` | integer (0 to 150) | No | “Rows of each chart the reply previews: default 10, up to 150.” |
| `intent` | string (max 300 chars) | No | “Why this call is being made, in 15-25 words, third person. Analytics only; it never changes the result. No names, emails, IDs or row values &mdash; describe the purpose in general terms.” |

`charts`, as sent:

> “A native JSON array of 1 to 10 charts, never a JSON-encoded string. Each entry is either { chart_id } for a chart the user saved, or { sql, chartType?, title?, encode?, fields?, ... } for a chart drawn from a query now; the two kinds may mix. Stacking, horizontal bars, a secondary axis, a line over bars, the x-axis scale, reference lines, axis titles and the funnel style apply to some types only: the type's docs page lists exactly what it takes, and an option that does not fit the type is rejected with the valid list.”

`charts[sql].encode`, as sent:

> “Map columns to the chart: x is the single column for the x-axis, y one measure or a list, series a column to split one measure by. Set x whenever more than one column could plausibly be it, e.g. SELECT COUNT(\*) AS n, year FROM t GROUP BY year (set x: "year"), where a wrong guess would plot the count instead. encode.line and encode.y2 mix a line into bars or add a secondary axis: see the type's docs page.”

## `fetch_view_data`

Table: fetch_view_data at a glance

| Fact | Value |
| --- | --- |
| Setting | **Charts** or **Dashboards**, on by default |
| Who gets it | The widget in the AI client |
| Annotations | `title: Fetch view data`, `readOnlyHint: true`, `destructiveHint: false`, `openWorldHint: false` |
| Visibility | `app` |

Description as sent:

> “Loads a rendered chart or dashboard's data into the chart widget, or re-renders a dashboard for a filter pick. For the widget only: to see a chart, call render_chart or render_dashboard.”

Table: fetch_view_data parameters

| Parameter | Type | Needed | Description |
| --- | --- | --- | --- |
| `ref` | string (max 200 chars) | No | “The viewRef a render_chart or render_dashboard result carries.” |
| `dashboard_id` | string (max 64 chars) | No | “Render this dashboard live instead, for a filter pick.” |
| `params` | object | No | “With dashboard_id: the param values, as render_dashboard takes them.” |
| `item_id` | string (max 64 chars) | No | “With dashboard_id: one chart cell.” |
| `intent` | string (max 300 chars) | No | “Why this call is being made, in 15-25 words, third person. Analytics only; it never changes the result. No names, emails, IDs or row values &mdash; describe the purpose in general terms.” |

## `create_export_file`

Table: create_export_file at a glance

| Fact | Value |
| --- | --- |
| Setting | **Export data**, on by default |
| Who gets it | Anyone connected |
| Annotations | `title: Create export file`, `readOnlyHint: false`, `destructiveHint: false`, `idempotentHint: true`, `openWorldHint: false` |

Description as sent:

> “Create a CSV (`csv`, or gzip-compressed `csv.gz`) from one resultRef or a PNG from one snapshotRef without rerunning the query. The file holds exactly the rows its capture holds, including a bulk capture taken with sql_query max_rows. resources/read the returned artifactUri to get the exact bytes, then open, save, or use it in the workspace.”

Table: create_export_file parameters

| Parameter | Type | Needed | Description |
| --- | --- | --- | --- |
| `database` | string | No | “Data source key, from explore_schema” |
| `resultRef` | string | No | “Captured tabular result reference (`result://…`). Use this with format `csv` or `csv.gz`. Provide exactly one of resultRef or snapshotRef.” |
| `snapshotRef` | string | No | “Captured chart or dashboard reference (`snapshot://…`). Use this with format `png`. Provide exactly one of resultRef or snapshotRef.” |
| `deliver` | one of `reference`, `inline` | No | “`reference` (default): the reply names the file and you read its bytes with resources/read. `inline`: the bytes also ride in this reply, for a client that cannot read resources.” |
| `format` | string | Yes | “Output format: `csv` or `csv.gz` (gzip-compressed) for resultRef, `png` for snapshotRef. Prefer `csv.gz` when the export is large (rows_available exceeded rows_shown on the call that produced this resultRef) and you can gunzip it yourself. Use plain `csv` for small results, or when the file goes to a person or a UI unprocessed.” |
| `intent` | string (max 300 chars) | No | “Why this call is being made, in 15-25 words, third person. Analytics only; it never changes the result. No names, emails, IDs or row values &mdash; describe the purpose in general terms.” |

## `save_dashboard`

Table: save_dashboard at a glance

| Fact | Value |
| --- | --- |
| Setting | **Dashboards**, on by default |
| Who gets it | Anyone connected |
| Annotations | `title: Save dashboard`, `readOnlyHint: false`, `destructiveHint: false`, `idempotentHint: false`, `openWorldHint: false` |

Description as sent:

> “Save a dashboard for this user, or edit one they already saved. The id it is stored under is returned; render it with render_dashboard and that id. Omit dashboard_id to create a new one, and send one `replace_all` with `title`, a one-line `description` and `items`, the tiles in display order: {"operations":[{"op":"replace_all","title":"Sales","description":"Revenue by month.","items":[...tiles]}]}. Every change goes inside `operations`; nothing else sits at the top level beside dashboard_id and expected_version. To edit, pass that dashboard_id together with the expected_version from the last save (or from explore_dashboards), which is refused if someone else changed it first. Read the dashboard first with explore_dashboards and its dashboard_id: an edit replaces the whole tile, so pass its SQL back unless you mean to change it. A `version_conflict` means it changed: re-read it and reapply your change. Chart and KPI options: docs charts/&lt;type> (e.g. charts/line) and charts/kpi; params and optional [[ ]] filters: docs dashboards/params; open them directly, e.g. explore_docs({"topic":["charts/kpi","charts/line"]}). The SQL of the tiles you add or edit is run once as you save, and any that fail are named in the reply; the save succeeds either way, and render_dashboard shows each error in place. Dialect: PostgreSQL. Identifier quote: "double quotes". Dates: date_trunc('month', col), now() - interval '7 days'. Cast: col::type. Nulls: coalesce(a, b).”

Table: save_dashboard parameters

| Parameter | Type | Needed | Description |
| --- | --- | --- | --- |
| `database` | string | No | “Data source key, from explore_schema” |
| `dashboard_id` | string | No | “The dashboard to edit, as returned by a previous save_dashboard. Omit to create a new one.” |
| `operations` | array of objects (1 to 20 items) | Yes | “A native JSON array of operation objects. Never a JSON-encoded string. At most 20 per call. The same add / edit / move / remove / set_metadata / replace_all batch the dashboard editor uses; a replace_all must be the only operation in its batch.” |
| `operations[].op` | one of `add`, `edit`, `move`, `remove`, `set_metadata`, `replace_all` | Yes | Quoted below this table. |
| `operations[].id` | string (max 64 chars, pattern `^[a-z][a-z0-9_-]*$`) | No | “The id of the tile to edit, move or remove.” |
| `operations[].items` | array of objects (1 to 20 items) | No | Quoted below this table. |
| `operations[].items[kind=chart].kind` | `chart` | Yes |  |
| `operations[].items[kind=chart].id` | string (max 64 chars, pattern `^[a-z][a-z0-9_-]*$`) | Yes |  |
| `operations[].items[kind=chart].sql` | string (max 8000 chars) | Yes | “A read-only SELECT; {{name}} binds a declared param, and [[ AND ... ]] makes a condition optional.” |
| `operations[].items[kind=chart].title` | string (max 200 chars) | No | “The chart tile's display name. May include a declared {{param}}.” |
| `operations[].items[kind=chart].chartType` | one of `line`, `bar`, `area`, `pie`, `scatter`, `funnel`, `waterfall`, `table` | No | “line, bar, area, pie, scatter, funnel, waterfall or table; omit to infer from the columns. Every option a type takes, with an example: explore_docs topic "charts/&lt;type>", e.g. explore_docs({"topic":"charts/bar"}). Choosing a type: topic "charts".” |
| `operations[].items[kind=chart].encode` | object | No | Quoted below this table. |
| `operations[].items[kind=chart].encode.x` | string | No |  |
| `operations[].items[kind=chart].encode.y` | string or array of strings | No |  |
| `operations[].items[kind=chart].encode.series` | string | No |  |
| `operations[].items[kind=chart].fields` | array of objects | No | “Formats are never inferred from column names: declare percent, currency or compact per column, e.g. [{"name":"revenue","format":"currency","currency":"GBP"},{"name":"gm","format":"percent","label":"Gross margin %"}]. A column alias with spaces or capitals ("Gross margin") is shown exactly as written; a bare snake_case name only has its underscores spaced and its first letter capitalised. Put a title with % or other symbols in label, not in the SQL alias.” |
| `operations[].items[kind=chart].fields[].name` | string | Yes |  |
| `operations[].items[kind=chart].fields[].label` | string | No |  |
| `operations[].items[kind=chart].fields[].format` | one of `number`, `percent`, `currency`, `compact` | No |  |
| `operations[].items[kind=chart].fields[].currency` | string | No |  |
| `operations[].items[kind=chart].span` | integer (1 to 4) | No | Quoted below this table. |
| `operations[].items[kind=kpi].kind` | `kpi` | Yes |  |
| `operations[].items[kind=kpi].id` | string (max 64 chars, pattern `^[a-z][a-z0-9_-]*$`) | Yes |  |
| `operations[].items[kind=kpi].sql` | string (max 8000 chars) | Yes | “A read-only SELECT whose first row is read; {{name}} binds a declared param.” |
| `operations[].items[kind=kpi].title` | string (max 200 chars) | Yes | “The KPI's display name. May include a declared {{param}}.” |
| `operations[].items[kind=kpi].valueColumn` | string (max 256 chars) | No |  |
| `operations[].items[kind=kpi].deltaColumn` | string (max 256 chars) | No |  |
| `operations[].items[kind=kpi].format` | one of `number`, `currency`, `percent`, `compact` | No |  |
| `operations[].items[kind=kpi].currency` | string (max 64 chars) | No |  |
| `operations[].items[kind=kpi].caption` | string (max 2000 chars) | No | “A short line under the KPI value. May include a declared {{param}}.” |
| `operations[].items[kind=kpi].span` | integer (1 to 4) | No |  |
| `operations[].items[kind=text].kind` | `text` | Yes |  |
| `operations[].items[kind=text].id` | string (max 64 chars, pattern `^[a-z][a-z0-9_-]*$`) | Yes |  |
| `operations[].items[kind=text].text` | string (max 8000 chars) | Yes | “The text tile's body (markdown supported). This field is called text, not body or content. Text tiles run no SQL.” |
| `operations[].items[kind=text].heading` | string (max 200 chars) | No | “The text tile's optional heading. May include a declared {{param}}.” |
| `operations[].items[kind=text].span` | integer (1 to 4) | No |  |
| `operations[].placement` | `{"position":"start"}` or `{"position":"end"}` or object | No | “Where the tile goes: the start, the end, or after a named tile: {"position":"start"}, {"position":"end"} or {"position":"after","afterId":"by_month"}.” |
| `operations[].placement[position=after].position` | `after` | Yes |  |
| `operations[].placement[position=after].afterId` | string (max 64 chars, pattern `^[a-z][a-z0-9_-]*$`) | Yes |  |
| `operations[].title` | string (max 200 chars) | No | “The dashboard's display name. May include a declared {{param}}.” |
| `operations[].description` | string (max 500 chars) | No | “A one-line summary of the dashboard, shown by explore_dashboards.” |
| `operations[].columns` | any | No | “Ignored: the grid is fixed at 4.” |
| `operations[].params` | array of objects (max 100 items) or null | No | “A dashboard can declare params. In tile SQL, {{name}} binds a declared param, and [[ AND ... ]] around a condition makes it optional so viewers can pick All: WHERE 1=1 [[ AND region = {{region}} ]]. null clears them all. Read docs dashboards/params before declaring one; open it directly with explore_docs.” |
| `operations[].params[].name` | string (max 128 chars, pattern `^[a-z_][a-z0-9_]*$`) | Yes | “Used in SQL as {{name}}.” |
| `operations[].params[].label` | string (1 to 60 chars) | No | “Short noun viewers see; plural for a filter with All.” |
| `operations[].params[].type` | one of `string`, `number`, `date` | Yes | “string, number or date (YYYY-MM-DD): the compared column's type.” |
| `operations[].params[].required` | boolean | No | “true: nothing loads until a value is chosen.” |
| `operations[].params[].default` | string (max 1000 chars) or number or `{"mode":"first"}` | No | “A literal of the type, or {"mode":"first"}.” |
| `operations[].params[].description` | string (max 2000 chars) | No | “One sentence: how a caller picks a value.” |
| `operations[].params[].valuesFrom` | object | No | “{table, column} the filter's values come from.” |
| `operations[].params[].valuesFrom.table` | string (max 800 chars) | Yes | “The table holding the values, as explore_schema names it.” |
| `operations[].params[].valuesFrom.column` | string (max 800 chars) | Yes | “The column holding the values.” |
| `operations[].params[].sortBy` | object | No | “{column, direction} ordering those values.” |
| `operations[].params[].sortBy.column` | string (max 800 chars) | Yes | “A column of valuesFrom.table to order by.” |
| `operations[].params[].sortBy.direction` | one of `asc`, `desc` | Yes | “"desc" or "asc".” |
| `expected_version` | integer | No | “Required when editing: the version the last save returned, or the `version` field explore_dashboards reports for this dashboard. Omit when creating.” |
| `intent` | string (max 300 chars) | No | “Why this call is being made, in 15-25 words, third person. Analytics only; it never changes the result. No names, emails, IDs or row values &mdash; describe the purpose in general terms.” |

`operations[].op`, as sent:

> “Which edit to make, and what the other fields mean. `add` takes `items` (exactly one tile), `placement`; `edit` takes `id`, `items` (exactly one tile); `move` takes `id`, `placement`; `remove` takes `id`; `set_metadata` takes `title`, `description`, `columns`, `params`; `replace_all` takes `items`, `title`, `description`, `columns`, `params`. A field that belongs to another op is rejected, as is a missing one.”

`operations[].items`, as sent:

> “The tiles, always an array, even for one: {"op":"edit","id":"by_month","items":[{"kind":"chart","id":"by_month", ...the complete tile}]}. add: the one tile to add. edit: the one replacement tile, whose id must equal id. replace_all: the complete new set of tiles, in display order. Every tile has kind (chart, kpi or text), an id (lowercase letters, digits, _ and -, starting with a letter, at most 64 characters; edit, move, remove and placement refer to tiles by it) and an optional span. A dashboard holds at most 16 chart/KPI tiles, counted across the whole dashboard once your batch is applied; text tiles do not count. Rename a chart tile with an edit that changes its title, a KPI with its title; rename the dashboard with set_metadata.”

`operations[].items[kind=chart].encode`, as sent:

> “Map columns to the chart: x is the single column for the x-axis, y one measure or a list, series a column to split one measure by. Set x whenever more than one column could plausibly be it, e.g. SELECT COUNT(\*) AS n, year FROM t GROUP BY year (set x: "year"), where a wrong guess would plot the count instead. encode.line and encode.y2 mix a line into bars or add a secondary axis: see the type's docs page.”

`operations[].items[kind=chart].span`, as sent:

> “How many of the 4 grid columns this tile spans, 1 to 4; 4 is full width and 2 half. A chart or text tile added without a span gets 2, a KPI 1, and an edit that omits span keeps the tile's width. columns is ignored: the grid is fixed at 4.”

## `delete_dashboard`

Table: delete_dashboard at a glance

| Fact | Value |
| --- | --- |
| Setting | **Dashboards**, on by default |
| Who gets it | Anyone connected |
| Annotations | `title: Delete dashboard`, `readOnlyHint: false`, `destructiveHint: true`, `idempotentHint: true`, `openWorldHint: false` |

Description as sent:

> “Permanently delete one of this user's own saved dashboards. Only dashboards they saved themselves can be deleted; verified dashboards and dashboards shared with the user are not yours to remove. Pass the expected_version from explore_dashboards or the last save, which is refused if it has changed since. This cannot be undone, so confirm with the user first.”

Table: delete_dashboard parameters

| Parameter | Type | Needed | Description |
| --- | --- | --- | --- |
| `database` | string | No | “Data source key, from explore_schema” |
| `dashboard_id` | string (max 64 chars, pattern `^[a-z][a-z0-9_-]*$`) | Yes | “The dashboard to delete, one of this user's own saved ones as `explore_dashboards` lists them.” |
| `expected_version` | integer | Yes | “The version the dashboard is currently at, as explore_dashboards reports it in its `version` field.” |
| `intent` | string (max 300 chars) | No | “Why this call is being made, in 15-25 words, third person. Analytics only; it never changes the result. No names, emails, IDs or row values &mdash; describe the purpose in general terms.” |

## `save_chart`

Table: save_chart at a glance

| Fact | Value |
| --- | --- |
| Setting | **Save charts** and **Export data**, on by default |
| Who gets it | Anyone connected |
| Annotations | `title: Save chart`, `readOnlyHint: false`, `destructiveHint: false`, `idempotentHint: false`, `openWorldHint: false` |

Description as sent:

> “Save a chart for this user, or edit one they already saved. To save the chart render_chart just showed, pass its snapshotRef (or resultRef, or the viewRef of a single chart) and a title (an optional description helps you find it again later via explore_charts); the chart is stored exactly as it was drawn and its id is returned. To edit, pass that chart_id with the expected_version from the last save or from explore_charts (refused if it changed since), and the new title/description and/or a reference from a fresh render_chart with the new query. References last 30 minutes; after that, render the chart again and save the new reference. One chart per call: to keep several charts from a carousel, call save_chart once for each.”

Table: save_chart parameters

| Parameter | Type | Needed | Description |
| --- | --- | --- | --- |
| `database` | string | No | “Data source key, from explore_schema” |
| `chart_id` | string | No | “The chart to edit, as returned by a previous save_chart or listed by explore_charts. Omit to save a new one.” |
| `expected_version` | integer | No | “Required when editing: the version the last save returned, or the `version` explore_charts reports for this chart. Omit when creating.” |
| `title` | string (max 200 chars) | No | “The title to store. May be omitted when the rendered chart already has one; otherwise required.” |
| `description` | string (max 500 chars) | No | “Stored so explore_charts and search can match this chart by what it is for, not just its title. Pass an empty string to clear an existing one.” |
| `snapshotRef` | string | No | “A snapshotRef from a render_chart response, or the viewRef of a one-chart render: saves exactly the chart that produced it. Provide at most one of snapshotRef or resultRef.” |
| `resultRef` | string | No | “A resultRef from a render_chart response, accepted the same way as snapshotRef.” |
| `intent` | string (max 300 chars) | No | “Why this call is being made, in 15-25 words, third person. Analytics only; it never changes the result. No names, emails, IDs or row values &mdash; describe the purpose in general terms.” |

## `explore_charts`

Table: explore_charts at a glance

| Fact | Value |
| --- | --- |
| Setting | **Save charts**, on by default |
| Who gets it | Anyone connected |
| Annotations | `title: Explore charts`, `readOnlyHint: true`, `destructiveHint: false`, `openWorldHint: false` |

Description as sent:

> “List the charts this user saved (id, title, description, chart type, version). Show one with render_chart and its chart_id, edit or delete it with the version reported here. Pass chart_id to read one chart instead of the list; that returns its full definition, the SQL and the chart options included, in the exact shape render_chart takes as an entry. Dashboards are separate: explore_dashboards lists those.”

Table: explore_charts parameters

| Parameter | Type | Needed | Description |
| --- | --- | --- | --- |
| `database` | string | No | “Data source key, from explore_schema” |
| `chart_id` | string | No | “Read this one chart instead of listing them all. Omit to list.” |
| `intent` | string (max 300 chars) | No | “Why this call is being made, in 15-25 words, third person. Analytics only; it never changes the result. No names, emails, IDs or row values &mdash; describe the purpose in general terms.” |

## `delete_chart`

Table: delete_chart at a glance

| Fact | Value |
| --- | --- |
| Setting | **Save charts**, on by default |
| Who gets it | Anyone connected |
| Annotations | `title: Delete chart`, `readOnlyHint: false`, `destructiveHint: true`, `idempotentHint: true`, `openWorldHint: false` |

Description as sent:

> “Permanently delete one of this user's own saved charts. Pass the expected_version from explore_charts or the last save, which is refused if it has changed since. This cannot be undone, so confirm with the user first.”

Table: delete_chart parameters

| Parameter | Type | Needed | Description |
| --- | --- | --- | --- |
| `database` | string | No | “Data source key, from explore_schema” |
| `chart_id` | string (max 64 chars, pattern `^cht_[a-z0-9_-]+$`) | Yes | “The chart to delete, one of this user's own saved ones as `explore_charts` lists them.” |
| `expected_version` | integer | Yes | “The version the chart is currently at, as explore_charts reports it in its `version` field.” |
| `intent` | string (max 300 chars) | No | “Why this call is being made, in 15-25 words, third person. Analytics only; it never changes the result. No names, emails, IDs or row values &mdash; describe the purpose in general terms.” |

## `explore_docs`

Table: explore_docs at a glance

| Fact | Value |
| --- | --- |
| Setting | **Docs**, always on |
| Who gets it | Anyone connected, while Charts or Dashboards is on. Editor or higher gets it with both off. |
| Annotations | `title: Explore docs`, `readOnlyHint: true`, `destructiveHint: false`, `openWorldHint: false` |

Description as sent:

> “How-to docs for charts (render_chart) and dashboard params and KPIs (save_dashboard). The pages are fixed: charts (choosing a type), charts/&lt;type> for line, bar, area, pie, scatter, funnel, waterfall and table (the options that type takes), charts/kpi and dashboards/params. Only when asked for them: charts/styling (colours, dashed lines, markers, highlight, legend) and charts/annotations (target, average and median lines, bands, trend lines and moving averages). Open the pages you need directly with `topic`, several at once as a list; search with `query` only when you do not know which page. Each page ends with examples that validate.”

Editor or higher gets this text at the end of the description:

> “For editing this data source's context: context/runbook, context/descriptions, context/joins, context/keys-and-time, context/metrics and context/publishing. Open context/runbook first.”

Table: explore_docs parameters

| Parameter | Type | Needed | Description |
| --- | --- | --- | --- |
| `topic` | string (max 200 chars) or array of strings (1 to 4 items) | No | Quoted below this table. |
| `query` | string (max 300 chars) | No | “What you want to do, in plain words, e.g. "dual axis" or "let viewers pick all regions". Returns the best matching page and section ids.” |
| `intent` | string (max 300 chars) | No | “Why this call is being made, in 15-25 words, third person. Analytics only; it never changes the result. No names, emails, IDs or row values &mdash; describe the purpose in general terms.” |

`topic`, as sent:

> “A page id: "charts", "charts/&lt;type>" for line, bar, area, pie, scatter, funnel, waterfall or table, "charts/styling", "charts/annotations", "charts/kpi" or "dashboards/params"; add #section to open a page at a section, or pass a list of up to 4 ids. Takes precedence over `query`.”

## `explore_dashboards`

Table: explore_dashboards at a glance

| Fact | Value |
| --- | --- |
| Setting | **Dashboards**, on by default |
| Who gets it | Anyone connected |
| Annotations | `title: Explore dashboards`, `readOnlyHint: true`, `destructiveHint: false`, `openWorldHint: false` |

Description as sent:

> “List the available dashboards (id, title, description, params). Call this first to discover what you can render, then call `render_dashboard` with a chosen dashboard_id. Pass dashboard_id to read one entry instead of the list; for a dashboard the user saved themselves that returns its full definition, every tile's SQL included, which is what `save_dashboard` needs to edit it. For a dashboard that takes params, the detail read also lists each param's description, default and up to 50 sample values. A param marked clearable is used only inside [[ ]] optional clauses, so it can show All; that is derived from the SQL, not something you set. Pass item_id to re-render a single chart cell by its id (ids appear in the dashboard summary), with preview_rows for its rows.”

Table: explore_dashboards parameters

| Parameter | Type | Needed | Description |
| --- | --- | --- | --- |
| `database` | string | No | “Data source key, from explore_schema” |
| `dashboard_id` | string | No | “Read this one dashboard in detail instead of listing them all. Omit to list.” |
| `intent` | string (max 300 chars) | No | “Why this call is being made, in 15-25 words, third person. Analytics only; it never changes the result. No names, emails, IDs or row values &mdash; describe the purpose in general terms.” |

## `render_dashboard`

Table: render_dashboard at a glance

| Fact | Value |
| --- | --- |
| Setting | **Dashboards**, on by default |
| Who gets it | Anyone connected |
| Annotations | `title: Render dashboard`, `readOnlyHint: true`, `destructiveHint: false`, `openWorldHint: false` |
| Renders in | `ui://bonnard/view.html` |

Description as sent:

> “Render one dashboard by dashboard_id. This displays it to the user directly, as an interactive chart or grid of tiles. Pass item_id to re-render a single chart cell by its id (ids appear in the dashboard summary), with preview_rows for its rows. When returned, resultRef and snapshotRef identify captured item data and render specs; pass one to create_export_file instead of rerunning the dashboard. Call explore_dashboards for the dashboards this user can render, the user's own saved ones included; verified dashboards are listed first.”

Table: render_dashboard parameters

| Parameter | Type | Needed | Description |
| --- | --- | --- | --- |
| `database` | string | No | “Data source key, from explore_schema” |
| `dashboard_id` | string (max 64 chars, pattern `^[a-z][a-z0-9_-]*$`) | Yes | “The dashboard to render, from the ids `explore_dashboards` lists. Each call runs that dashboard's stored queries live.” |
| `params` | object | No | “Values for the dashboard's declared params, keyed by name (e.g. {"region": "EU"}); undeclared names are rejected. An omitted param takes its starting value. To show all values for a param that explore_dashboards marks clearable, pass "" (or omit it when it starts as All). A required param must be passed. Each value is bound as a query parameter, so pass a literal, not a SQL fragment.” |
| `item_id` | string | No | “The id of a single chart cell (from a previous render's summary): renders that cell alone and runs only its query. KPI and text tiles are not selectable; an unknown id errors and lists the ids that are.” |
| `preview_rows` | integer (0 to 150) | No | “With item_id, rows of that tile the reply previews: default 10, up to 150.” |
| `intent` | string (max 300 chars) | No | “Why this call is being made, in 15-25 words, third person. Analytics only; it never changes the result. No names, emails, IDs or row values &mdash; describe the purpose in general terms.” |

## `search_param_values`

Table: search_param_values at a glance

| Fact | Value |
| --- | --- |
| Setting | **Dashboards**, on by default |
| Who gets it | The widget in the AI client |
| Annotations | `title: Search filter values`, `readOnlyHint: true`, `destructiveHint: false`, `openWorldHint: false` |
| Visibility | `app` |

Description as sent:

> “Lists and searches one dashboard filter's values for the dashboard widget's filter menu. Not for answering questions: to see a dashboard's params and sample values, call `explore_dashboards` with its dashboard_id instead.”

Table: search_param_values parameters

| Parameter | Type | Needed | Description |
| --- | --- | --- | --- |
| `database` | string | No | “Data source key, from explore_schema” |
| `dashboard_id` | string (1 to 64 chars) | Yes | “The rendered dashboard.” |
| `param` | string (1 to 64 chars) | Yes | “A declared param with a value source.” |
| `query` | string (max 200 chars) | No | “Case-insensitive text the value must contain. Omit to list in order.” |
| `offset` | integer (0 to 500) | No | “Values to skip. Default 0.” |
| `limit` | integer (1 to 50) | No | “Values to return. Default 50.” |
| `intent` | string (max 300 chars) | No | “Why this call is being made, in 15-25 words, third person. Analytics only; it never changes the result. No names, emails, IDs or row values &mdash; describe the purpose in general terms.” |

## `get_versions`

Table: get_versions at a glance

| Fact | Value |
| --- | --- |
| Setting | None |
| Who gets it | Editor or higher |
| Annotations | `title: Get context versions`, `readOnlyHint: true`, `destructiveHint: false`, `openWorldHint: false` |

Description as sent:

> “Lists this data source's context versions, the draft on top, and the changes in any one of them. Call with no arguments first, then with a version for the tables and metrics it changed, an object for that one's changes, and a field for one change in full. Read explore_docs context/runbook before editing.”

Table: get_versions parameters

| Parameter | Type | Needed | Description |
| --- | --- | --- | --- |
| `database` | string | No | “Data source key, from explore_schema” |
| `version` | integer (greater than 0) or `draft` | No | “A version number, or "draft". Omit to list the versions.” |
| `object` | string (max 300 chars) | No | “A table or metric the version changed, by name. Lists its changes.” |
| `field` | string (max 300 chars) | No | “One of that object's changes, by its field or id. Returns it in full.” |
| `incomplete` | boolean | No | “With version "draft": only the metrics a publish would hold back as incomplete.” |
| `offset` | integer (min 0) | No | “Skip this many rows, to read the next page.” |
| `intent` | string (max 300 chars) | No | “Why this call is being made, in 15-25 words, third person. Analytics only; it never changes the result. No names, emails, IDs or row values &mdash; describe the purpose in general terms.” |

## `edit_draft`

Table: edit_draft at a glance

| Fact | Value |
| --- | --- |
| Setting | None |
| Who gets it | Editor or higher |
| Annotations | `title: Edit the context draft`, `readOnlyHint: false`, `destructiveHint: false`, `idempotentHint: false`, `openWorldHint: false` |

Description as sent:

> “Edits this data source's context draft: descriptions, keys, time columns, which tables AI clients see, joins and metrics, up to 100 operations in one call. Nothing is served to anyone until the draft is published. Each operation is checked on its own, so a refused one leaves the others applied. Read explore_docs context/runbook before editing.”

Table: edit_draft parameters

| Parameter | Type | Needed | Description |
| --- | --- | --- | --- |
| `database` | string | No | “Data source key, from explore_schema” |
| `operations` | array of objects (1 to 100 items) | Yes | Quoted below this table. |
| `operations[].op` | one of `set_description`, `set_key`, `set_time_column`, `set_visible`, `add_join`, `remove_join`, `create_metric`, `update_metric`, `remove_metric`, `remove_change`, `revert_change` | Yes |  |
| `operations[].table` | string (max 300 chars) | No | “A table, as explore_schema names it.” |
| `operations[].column` | string (max 300 chars) or null | No | “A column of table. null clears a time column.” |
| `operations[].columns` | array of strings or null | No | “set_key: the columns that identify a row. null clears the key.” |
| `operations[].description` | string or null | No | “The text. null clears it.” |
| `operations[].visible` | boolean | No | “set_visible: false hides the table from discovery (listing, search, detail, metrics), not from queries; true shows it again.” |
| `operations[].to` | string (max 300 chars) | No | “add_join, remove_join: the other table.” |
| `operations[].pairs` | array of objects | No | “add_join, remove_join: the columns to join on.” |
| `operations[].pairs[].from` | string (max 300 chars) | Yes | “A column of table.” |
| `operations[].pairs[].to` | string (max 300 chars) | Yes | “A column of to.” |
| `operations[].metric` | string (max 300 chars) | No | “A metric, by its name or key.” |
| `operations[].name` | string | No | “create_metric, update_metric: the metric's name.” |
| `operations[].calculation` | string or null | No | “The SQL expression the metric computes, naming the tables it reads.” |
| `operations[].instructions` | string or null | No | “How an AI client should use the metric.” |
| `operations[].change` | string (max 300 chars) | No | “A change id from get_versions.” |
| `operations[].object` | string (max 300 chars) | No | “remove_change: a table or metric whose draft changes all go.” |
| `operations[].version` | integer (greater than 0) | No | “revert_change: the version that made the change.” |
| `operations[].confirmed` | boolean | No | “remove_join, remove_metric, revert_change: true to remove one the live version has.” |
| `intent` | string (max 300 chars) | No | “Why this call is being made, in 15-25 words, third person. Analytics only; it never changes the result. No names, emails, IDs or row values &mdash; describe the purpose in general terms.” |

`operations`, as sent:

> “set_description { table, column?, description }, set_key { table, columns }, set_time_column { table, column }, set_visible { table, visible }, add_join { table, to, pairs }, remove_join { table, to, pairs?, confirmed? }, create_metric { name, description?, calculation?, instructions? }, update_metric { metric, name?, description?, calculation?, instructions? }, remove_metric { metric, confirmed? }, remove_change { change } or { object }, revert_change { change, version, confirmed? }.”

## `publish_draft`

Table: publish_draft at a glance

| Fact | Value |
| --- | --- |
| Setting | **Allow AI clients to publish context changes**, off by default |
| Who gets it | Editor or higher |
| Annotations | `title: Publish the context draft`, `readOnlyHint: false`, `destructiveHint: true`, `idempotentHint: false`, `openWorldHint: false` |

Description as sent:

> “Publishes this data source's context draft as a new version, so every AI client that reads the data source sees it. Pass the draftRevision from get_versions or edit_draft; a draft that changed since is refused with what changed. Works only once an admin allows publishing from AI clients in Settings › AI tools. Read explore_docs context/runbook before publishing.”

Table: publish_draft parameters

| Parameter | Type | Needed | Description |
| --- | --- | --- | --- |
| `database` | string | No | “Data source key, from explore_schema” |
| `draftRevision` | string (max 100 chars) | Yes | “The revision get_versions or edit_draft gave for the draft you reviewed.” |
| `note` | string (max 500 chars) | No | “A note the version keeps, shown in the console.” |
| `intent` | string (max 300 chars) | No | “Why this call is being made, in 15-25 words, third person. Analytics only; it never changes the result. No names, emails, IDs or row values &mdash; describe the purpose in general terms.” |

## SQL dialect by engine

The descriptions of `sql_query`, `render_chart`, and `save_dashboard` end with one dialect line, set by the data source's engine. With more than one data source, they end with one line for each, after `For <key>:`. With no data source, they have no dialect line.

Table: Dialect line for each engine

| Engine | Dialect line |
| --- | --- |
| PostgreSQL | “Dialect: PostgreSQL. Identifier quote: "double quotes". Dates: date_trunc('month', col), now() - interval '7 days'. Cast: col::type. Nulls: coalesce(a, b).” |
| Amazon Redshift | “Dialect: Amazon Redshift (PostgreSQL-like). Identifier quote: "double quotes". Dates: date_trunc('month', col), dateadd(day, -7, getdate()). Cast: col::type. Nulls: nvl(a, b).” |
| BigQuery | “Dialect: BigQuery GoogleSQL. Identifier quote: `backticks`. Dates: date_trunc(col, month), date_sub(current_date(), interval 7 day). Cast: cast(x as type) or safe_cast. Nulls: ifnull(a, b).” |
| ClickHouse | “Dialect: ClickHouse. Identifier quote: `backticks`. Dates: toStartOfMonth(col), now() - INTERVAL 7 DAY. Cast: cast(x as type). Nulls: coalesce(a, b) or ifNull(a, b).” |
| DuckDB / MotherDuck | “Dialect: DuckDB. Identifier quote: "double quotes". Dates: date_trunc('month', col), now() - INTERVAL 7 DAY. Cast: col::type. Nulls: coalesce(a, b).” |
| MySQL / MariaDB | “Dialect: MySQL/MariaDB. Identifier quote: `backticks`. Dates: date_format(col, '%Y-%m-01'), now() - interval 7 day. Cast: cast(x as signed) (there is no `as int`). Nulls: ifnull(a, b). Concat: concat(a, b); `\|\|` is OR here, not string concatenation.” |
| Microsoft SQL Server | “Dialect: Microsoft SQL Server (T-SQL). Identifier quote: [brackets]. Row limiting: select top (n) ... or order by ... offset 0 rows fetch next n rows only; there is no LIMIT. Dates: datetrunc(month, col), dateadd(day, -7, getdate()). Cast: cast(x as type) or try_cast. Nulls: isnull(a, b) or coalesce(a, b). Concat: a + b, not `\|\|`. Unicode strings: N'text'.” |

This table does not include PostHog.
