Skip to main content
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

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, so the SDK renders a panel with the same code it renders a chat answer with. text panels carry markdown.
The platform compiles this to a workflow: 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.
The agent’s model, memory and extra instructions are set per network under system_agents.dashboard — see 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.
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.
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.