> ## Documentation Index
> Fetch the complete documentation index at: https://sidenet.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Dashboards

> Let end users build their own dashboards in the chat — grids of stats, charts and tables fed by your tools, refreshed without a model — and render them in your product.

A user asks the chat for "a dashboard of this month's bookings by property,
with revenue and occupancy", and gets one: a grid of stats tiles, charts and
tables, fed by the data tools your agents already have, with a date-range
and a property filter on top. They refresh it with a button, change a filter,
ask for a change in plain words ("add a chart of cancellations"), and go back
to an earlier version if the change was not what they wanted. No model runs
when a dashboard refreshes: the agent builds it once; from then on it is a
compiled workflow your API runs on request.

The chat widget's **dashboard agent** builds and edits dashboards and answers
questions about their data. The dashboards themselves are reachable over
HTTP (`/v1/dashboards`), with the same session token the widget holds, so
your product renders them wherever it wants.

## How the pieces fit

| Piece | What it is |
| - | - |
| **Dashboard agent** | A platform agent attached to every network, like the handoff and follow-up agents — not part of the roster. The chat runs it instead of the roster when the request says `mode: "dashboard"`. It may read from any **read-only** tool of any agent in the network. |
| **Spec** | What the agent writes: the filters, the sources (tool calls) and the panels (a transform each). Stored with every version. |
| **Dashboard** | The owner row (`GET /v1/dashboards/{id}`): name, the state of the last refresh, the filter values the page opens on. Owned by one end user, like their automations. |
| **Version** | Every save from the chat publishes a version; the user can activate an earlier one. |
| **Refresh** | One run of the compiled workflow with the filter values as input. The result is one rendered payload per panel, in the shape the chat components take. |

## What a dashboard is made of

**Filters** are the controls on the page: a `date_range` (with a preset
default such as "last 30 days", so a refresh stays relative), a `select`
(a property, a store, a team), a `text`, `number` or `date`. Source arguments
read them as `{{filters.<key>}}` — `{{filters.<key>.from}}` and `.to` for a
range.

**Sources** are the data: one entry per tool call, with the tool's arguments
and, when it paginates, a `loop` (`all_pages`, `fields` to keep, a row
`filter`). Several panels can read one source.

**Panels** are what is shown, in order, on a 12-column grid. Each has a
`span` (3, 4, 6, 8 or 12 columns) and a `height` (`sm`, `md`, `lg`).
`stats`, `chart` and `table` panels name their sources and carry a
**transform**: a few lines of JavaScript that turn the sources' raw output
into the panel's payload — exactly the payload of the `showStats`,
`showChart` or `showTable` [component](/docs/components), so the SDK renders a
panel with the same code it renders a chat answer with. `text` panels carry
markdown.

```json theme={null}
{
  "name": "Bookings this month",
  "filters": [
    { "key": "range", "label": "Period", "kind": "date_range", "default": { "preset": "this_month" } },
    { "key": "property", "label": "Property", "kind": "select",
      "options": [{ "value": "BER", "label": "Berlin" }, { "value": "PAR", "label": "Paris" }], "default": "BER" }
  ],
  "sources": [
    { "id": "bookings", "tool": "pms_list_reservations",
      "args": { "arrival_from": "{{filters.range.from}}", "arrival_to": "{{filters.range.to}}", "property": "{{filters.property}}" },
      "loop": { "all_pages": true, "fields": ["id", "status", "total", "arrival"] } }
  ],
  "panels": [
    { "id": "kpis", "title": "This month", "kind": "stats", "span": 12, "height": "sm", "sources": ["bookings"],
      "transform": "const rows = input.bookings.items ?? []; const revenue = rows.reduce((s, r) => s + Number(r.total || 0), 0); return { id: 'kpis', stats: [{ key: 'n', label: 'Bookings', value: rows.length }, { key: 'rev', label: 'Revenue', value: revenue, format: { kind: 'currency', currency: 'EUR' } }] };" },
    { "id": "by_day", "title": "Bookings per day", "kind": "chart", "span": 8, "sources": ["bookings"],
      "transform": "const by = {}; for (const r of input.bookings.items ?? []) by[r.arrival] = (by[r.arrival] || 0) + 1; return { id: 'by_day', type: 'bar', data: Object.entries(by).map(([d, n]) => ({ d, n })).sort((a, b) => a.d < b.d ? -1 : 1), xKey: 'd', series: [{ key: 'n', label: 'Bookings' }] };" },
    { "id": "note", "title": "About", "kind": "text", "span": 4, "markdown": "Arrivals in the selected period." }
  ]
}
```

The platform compiles this to a [workflow](/docs/workflows): a step that resolves
the filters (date presets in the dashboard's timezone), the sources in
parallel, then each panel's transform (in the code sandbox) feeding the
rendering tool, whose own schema validates the payload. A refresh is a run of
that workflow with the filter values as input; nothing in it calls a model.

## How the agent builds one

The dashboard agent never guesses a field name. It lists the read-only tools
of the network (`list_source_tools`), reads a tool's arguments
(`describe_source_tool`), **runs the sources for real** with the filters
resolved (`preview_dashboard_sources`) and writes each transform against the
output it saw. `save_dashboard` then compiles the spec, runs a full test
refresh, and publishes a version only when every panel rendered; a failed
test hands the agent the failing step with the data it received, so it fixes
the transform and saves again. The user sees a dashboard only once a refresh
has succeeded.

Only read-only tools qualify as sources: a custom API tool with a `GET`
method, an MCP tool whose server marks it `readOnlyHint` (or whose name reads
as one: `get_`, `list_`, `search_`…), a toolkit action with a read verb. A
dashboard can never send a write.

## Running the dashboard agent

Send the chat a turn with `mode: "dashboard"` and the network's `copilotId`;
routing is skipped and the dashboard agent answers. A thread that started
this way stays in dashboard mode, so follow-ups need no flag. With
`dashboardId`, the turn is about that dashboard: an edit ("make the chart
wider"), or a question about its data ("what stands out this week?") — the
agent sees the spec and the current data of every panel and answers from it,
and only saves when asked to change something.

```json theme={null}
{ "copilotId": "…", "mode": "dashboard", "dashboardId": "…",
  "messages": [{ "role": "user", "parts": [{ "type": "text", "text": "Add occupancy next to revenue." }] }] }
```

The agent's model, memory and extra instructions are set per network under
`system_agents.dashboard` — see [the platform agents](/docs/designing-a-network#the-platform-agents).

A thread the dashboard agent held carries `mode: "dashboard"` and the
`dashboardId` in its metadata, so `GET /v1/threads?dashboardId=…` lists the
conversations about one dashboard (the widget's "previous conversations"),
and a client that lists the roster's threads can leave them out.

## Rendering and refreshing

`GET /v1/dashboards/{id}` returns everything a page needs: the spec (filters,
panels, their `span`, `height` and order), the filter values to open on, and
`data` — the latest refresh with each panel's rendered payload
(`panels[].component.args` is the `showStats` / `showChart` / `showTable`
input) or its markdown.

```
POST /v1/dashboards/{id}/refresh   { "filters": { "range": { "preset": "last_7_days" }, "property": "PAR" } }
→ 202 { "run_id": "…", "status": "running" }
GET  /v1/dashboards/{id}/runs/{run_id}      poll until status is succeeded / failed
GET  /v1/dashboards/{id}                    read the data again
```

A filter change is a refresh with other values; `wait: true` answers once the
run is over instead of 202. A refresh with the same filters started moments
ago is joined rather than started again. The `workflow.run.finished` event on
`GET /v1/events` fires when a refresh ends.

`GET /v1/dashboards/{id}/versions` lists what the chat published;
`POST /v1/dashboards/{id}/activate` goes back to one of them and refreshes
it. `DELETE /v1/dashboards/{id}` removes the dashboard.

<Note>
  A refresh runs with the user's own tool credentials, as a chat turn does —
  the ones the session carries, or the ones captured when the dashboard was
  saved. An API key refreshing on a user's behalf names the user with
  `X-User-Id` and may send `runtimeAuth`.
</Note>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.