# Dashboard operations

> The 6 operations save_dashboard takes, with the fields of each, and the rules for tiles, placement, versions, and verified dashboards.

Updated: 2026-11-09

An agent, the model in an AI client that acts for a person, changes a dashboard by sending operations to `save_dashboard`. One call holds up to 20 operations. Bonnard refuses a field that belongs to another operation, and a missing needed field.

Verified 9 Nov 2026.

Table: Operations and their fields

| Operation | Needed | Optional |
| --- | --- | --- |
| [`replace_all`](#replace_all) | `items`, and `title` and `description` to create | `title`, `description`, `columns`, `params` |
| [`set_metadata`](#set_metadata) | One of the optional fields | `title`, `description`, `columns`, `params` |
| [`add`](#add) | `items` (one tile), `placement` | None |
| [`edit`](#edit) | `id`, `items` (one tile) | None |
| [`move`](#move) | `id`, `placement` | None |
| [`remove`](#remove) | `id` | None |

A call has `operations`, and `dashboard_id` and `expected_version` when it changes a saved dashboard. Bonnard runs the SQL of each added or changed tile one time. A tile whose query fails is named in the reply, and the save succeeds. Bonnard refuses the whole save for SQL that is not read-only, a `{{name}}` with no declared param, or malformed or nested `[[ ]]`. `columns` is accepted and ignored, because the grid is fixed at 4 columns.

## `replace_all`

Creates a dashboard, or replaces all of its tiles. Omit `dashboard_id` to create one, with `title` and `description`. Without them, Bonnard answers `title is required (<=200 chars)` or `description is required (<=500 chars)`. It must be the only operation in its call. `items` holds up to 20 tiles in display order.

`title` takes up to 200 characters, and `description` up to 500. `params` takes up to 100 entries, and `null` clears them. To declare one, go to [Add filters to a dashboard](https://bonnard.ai/docs/dashboards/params.md).

```json
{"operations":[{"op":"replace_all","title":"Sales","description":"Revenue by month.","items":[{"kind":"chart","id":"by_month","sql":"SELECT date_trunc('month', order_date) AS month, SUM(amount) AS revenue FROM orders GROUP BY 1 ORDER BY 1","chartType":"line","title":"Revenue by month"}]}]}
```

## `set_metadata`

Changes the dashboard's title, description, or params, and leaves its tiles as they are.

```json
{"dashboard_id":"<dashboard id>","expected_version":3,"operations":[{"op":"set_metadata","title":"Sales overview"}]}
```

## `add`

Adds one tile at a [placement](#placement).

```json
{"dashboard_id":"<dashboard id>","expected_version":3,"operations":[{"op":"add","items":[{"kind":"text","id":"note","text":"Figures exclude refunds."}],"placement":{"position":"end"}}]}
```

## `edit`

Replaces one whole tile. `items[0].id` must equal `id`. Read the dashboard with `explore_dashboards` first, and pass the tile's SQL back unless you change it. Without a `span`, the tile keeps its width.

```json
{"dashboard_id":"<dashboard id>","expected_version":3,"operations":[{"op":"edit","id":"note","items":[{"kind":"text","id":"note","text":"Figures include refunds."}]}]}
```

## `move`

Moves one tile to a [placement](#placement).

```json
{"dashboard_id":"<dashboard id>","expected_version":3,"operations":[{"op":"move","id":"note","placement":{"position":"start"}}]}
```

## `remove`

Removes one tile. A dashboard keeps at least one tile. Removing the last one gets `items must be a non-empty array`, and in the console at `app.bonnard.ai`, **Save** is blocked by `Add at least one item.`

```json
{"dashboard_id":"<dashboard id>","expected_version":3,"operations":[{"op":"remove","id":"note"}]}
```

## Tiles

A dashboard holds at most 16 chart and KPI tiles, counted after the call is applied. Text tiles do not count. Each tile has an `id` of up to 64 characters: lowercase letters, digits, `_`, and `-`, starting with a letter.

Table: Tile kinds

| Kind | Needed | Optional | Default `span` |
| --- | --- | --- | --- |
| `chart` | `kind`, `id`, `sql` (up to 8,000 characters) | `title` (up to 200 characters), `span`, and the options in [Chart spec](https://bonnard.ai/docs/for-ai-agents/chart-spec.md) | 2 |
| `kpi` | `kind`, `id`, `sql`, `title` | The fields in [KPI tile](https://bonnard.ai/docs/for-ai-agents/chart-spec.md#kpi-tile) | 1 |
| `text` | `kind`, `id`, `text` (up to 8,000 characters) | `heading` (up to 200 characters), `span` | 2 |

`span` is 1 to 4 of the grid's 4 columns. A value of 4 is full width.

A saved dashboard or chart holds at most 65,536 bytes as JSON, so 16 tiles of 8,000-character SQL do not fit. A saved chart's SQL holds at most 8,000 characters. Bonnard refuses a save over either limit:

```text
def exceeds the 65536-byte cap (<bytes> bytes)
```

```text
sql is required (<=8000 chars)
```

## Placement

`placement` says where a tile goes.

Table: placement values

| Value | Effect |
| --- | --- |
| `{"position":"start"}` | First on the dashboard |
| `{"position":"end"}` | Last on the dashboard |
| `{"position":"after","afterId":"<tile id>"}` | After the named tile |

## Versions

`expected_version` is needed on each change to a saved dashboard. It comes from the last save or from `explore_dashboards`. Without it, Bonnard answers:

```text
expected_version is required when editing; read it from explore_dashboards or the last save
```

If someone else changed the dashboard first, Bonnard refuses the call:

```text
version_conflict: "<id>" is at version <version>, not <expected version>; re-read it and reapply your change
```

To fix it, read the dashboard with `explore_dashboards`, then send the change again with the version it returns.

## Verified dashboards

An Editor or higher can change a verified dashboard from an AI client. The save writes unpublished changes, and `render_dashboard` keeps the published version until someone publishes the changes in the console at `app.bonnard.ai`. Bonnard refuses anyone else:

```text
"<id>" is verified, so only an admin or editor can change it, in the console or from their own AI client
```

## Dashboards customers have

A Viewer who owns a dashboard that customers have cannot change it or delete it from an AI client. An Editor or higher can change it. An AI client gets the first message, and the console the second:

```text
"<id>" is shared with customers, so only an admin or editor can change it
```

```text
Only an editor or admin can change a dashboard shared with customers.
```

## `delete_dashboard`

`delete_dashboard` removes a dashboard the caller saved, and the delete cannot be undone. It needs `dashboard_id` and `expected_version`. Bonnard refuses to delete a verified dashboard, and an Editor or higher gets this reply:

```text
"<id>" is verified and cannot be deleted from chat; an admin can delete it in the console
```

A version that differs from the saved one is refused:

```text
version_conflict: "<id>" is not at version <version>; re-read it before deleting
```
