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

# Agent Dashboard

> Feed the Agent Dashboard live values from built-in tools, sandboxed code, HTTPS APIs, per-agent JSON/XML data files your agent maintains, and read-only SQL databases.

The **Agent Dashboard** (dashboard → *Agent Dashboard*, under Activity) is a single [json-render](/generative-ui) page you design once and that renders **in your owner console and in each buyer's own dashboard sidebar** — never on the public chat page. Out of the box its spec is static; this page shows how to bind every number, table and chart on it to **live data**.

<Note>
  This page assumes the json-render basics — `{ root, elements, state }`, `{ "$state": "/path" }` bindings, `repeat`, `{ "$item": "field" }` — documented in **[Generative UI](/generative-ui)**. Everything here also applies unchanged to the `data_sources` of [custom dashboard pages](/custom-pages) and of live front-page panels, which share the same pipeline.
</Note>

## How live data works

The Agent Dashboard editor has two boxes:

1. **Layout (json-render spec)** — the visual template. It reads values from its `state`, e.g. a `stat` whose `value` is `{ "$state": "/kpis/open_tickets" }`. Seed `state` with empty defaults so the page renders before the first fetch.
2. **Data sources (live values)** — a JSON array. Each entry fetches something at view time and writes the result into the spec's state at `result_target` (a [JSON Pointer](https://datatracker.ietf.org/doc/html/rfc6901)).

On save, the platform mints an opaque, content-addressed `action_id` for every source and stores the executable definition (tool name, params, code, URL, headers, connection string, query…) **server-side only**. A viewer's browser receives nothing but the id, POSTs it to the agent's `widget-action` endpoint on page load (and on the optional refresh interval), and gets back the result. Secrets and queries never ship to any browser.

**Caching & cost control.** Results are cached server-side per action for `refresh_interval_sec` seconds (default 120 when no interval is set), so a hundred open dashboards cause **one** upstream run per window, not a hundred. Rate limits (owner 120/min, signed-in buyer 30/min, anonymous 10/min per IP), a wallet circuit-breaker and the platform kill switches sit in front of every run.

### Fields every source shares

| Field                  | Required | Meaning                                                                                                             |
| ---------------------- | -------- | ------------------------------------------------------------------------------------------------------------------- |
| `result_target`        | ✅        | JSON Pointer into the spec's state where the result is written, e.g. `"/news"`.                                     |
| `result_path`          | —        | Dotted path plucked from the raw result *before* writing, e.g. `"rows"` or `"0.count"` (array indices allowed).     |
| `refresh_interval_sec` | —        | Auto-refetch cadence, clamped to **60 – 86400**. Omit for a single fetch per page view (served from a 120 s cache). |
| `action_id`            | (minted) | Stamped by the save — the only field a browser ever sees. Don't write it yourself.                                  |

A page may declare **up to 12 data sources**; a 13th is rejected at save.

### Choosing a source type

Each data source sets **exactly one** of the following:

| You set                               | The panel shows                                                                                        | Typical use                                                 |
| ------------------------------------- | ------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------- |
| [`tool`](#1-built-in-or-custom-tool)  | The result of a curated read-only built-in or one of your custom code tools, with owner-fixed `params` | RSS feeds, web search/fetch, X posts                        |
| [`code`](#2-inline-sandboxed-code)    | Whatever a sandboxed Python/JavaScript body returns                                                    | Computed / derived values, bespoke fetch logic              |
| [`url`](#3-https-endpoint-url)        | A JSON/XML/text document fetched **server-side** from an HTTPS endpoint                                | REST APIs, status endpoints, exported feeds                 |
| [`file`](#4-per-agent-data-file-file) | A `.json` / `.xml` file in this agent's own `dashboard_data/` folder                                   | Values **your agent maintains** with `write_dashboard_file` |
| [`db`](#5-read-only-database-db)      | Rows from a read-only Postgres query                                                                   | Your own database — KPIs, orders, inventory                 |

A sixth kind — the **agent-push panel** (`writable: true` + `key`) — exists on [custom dashboard pages](/custom-pages#agent-push) and front-page panels, where the agent pushes a single value with `update_widget_data`. It is *not* available on the Agent Dashboard section itself; use a [`file` source](#4-per-agent-data-file-file) there instead (it is strictly more capable — a whole document the agent can patch field-by-field).

***

## 1. Built-in (or custom) tool

Runs one of the curated **read-only** built-ins with parameters you fix at design time: `web_fetch`, `web_search`, `read_rss_feed`, `read_recent_tweets`, `x_read_mentions`, `x_search_tweets`, `calculate` — or any of this agent's own `type: "code"` custom tools by name. Viewers can never change the params. Billing is the tool's own per-call fee (e.g. an RSS poll), once per cache window.

### Simple — latest headline as a stat

Data sources:

```json theme={null}
[
  {
    "tool": "read_rss_feed",
    "params": { "feed_url": "https://hnrss.org/frontpage" },
    "result_target": "/news",
    "result_path": "rows"
  }
]
```

Layout — bind the first row's title:

```json theme={null}
{
  "root": "root",
  "elements": {
    "root": { "type": "card", "props": { "title": "Newsroom", "subtitle": null }, "children": ["headline"] },
    "headline": { "type": "stat", "props": { "label": "Top story", "value": { "$state": "/news/0/title" } } }
  },
  "state": { "news": [] }
}
```

### Full example — auto-refreshing feed list

`read_rss_feed` returns `{ ok, count, rows: [{ title, url, published_at, … }] }`, so `result_path: "rows"` extracts the array; the spec repeats a `link` per row. Refreshes every 15 minutes; between refreshes every viewer is served the cached result.

```json theme={null}
[
  {
    "tool": "read_rss_feed",
    "params": { "feed_url": "https://hnrss.org/frontpage", "limit": 8 },
    "result_target": "/news",
    "result_path": "rows",
    "refresh_interval_sec": 900
  }
]
```

```json theme={null}
{
  "root": "root",
  "elements": {
    "root": { "type": "card", "props": { "title": "Newsroom", "subtitle": "Fresh from the feed" }, "children": ["count", "rows"] },
    "count": { "type": "badge", "props": { "label": { "$template": "showing latest stories" }, "variant": "info" } },
    "rows": { "type": "list", "props": { "direction": "vertical", "gap": "sm" }, "children": ["row"], "repeat": { "statePath": "/news", "key": "url" } },
    "row": { "type": "link", "props": { "href": { "$item": "url" }, "label": { "$item": "title" }, "variant": null } }
  },
  "state": { "news": [] }
}
```

To use one of your **own tools**, set `"tool": "my_tool_name"` (it must be an enabled `type: "code"` tool on this agent) — its `handle(input, ctx)` receives your fixed `params` as `input`. This is the escape hatch when a source needs logic the other kinds can't express.

***

## 2. Inline sandboxed code

A Python or JavaScript body executed in the code sandbox per (cached) request — no tool definition needed. The body follows the same convention as custom code tools: define `handle(input)` and return a JSON-serializable value. There is **no `ctx.tools` bridge** in widget code (unlike custom tools run by the agent), and each run bills sandbox execution seconds — prefer [`url`](#3-https-endpoint-url) for a plain HTTP fetch, which is cheaper and faster.

### Simple — computed value

```json theme={null}
[
  {
    "code": "def handle(input):\n    return {\"answer\": 6 * 7}",
    "language": "python",
    "result_target": "/calc",
    "result_path": "answer"
  }
]
```

```json theme={null}
{
  "root": "root",
  "elements": {
    "root": { "type": "card", "props": { "title": "Computed", "subtitle": null }, "children": ["a"] },
    "a": { "type": "stat", "props": { "label": "The answer", "value": { "$state": "/calc" } } }
  },
  "state": { "calc": null }
}
```

### Full example — fetch + reshape for a chart

The sandbox has network access, so code can call an API and reshape the payload into the `[{ label, value }]` array a `chart` expects. Refreshes hourly.

```json theme={null}
[
  {
    "code": "async function handle(input) {\n  const res = await fetch('https://api.example.com/signups/daily');\n  const days = await res.json();\n  return days.slice(-7).map(d => ({ label: d.date.slice(5), value: d.count }));\n}",
    "language": "javascript",
    "result_target": "/signups",
    "refresh_interval_sec": 3600
  }
]
```

```json theme={null}
{
  "root": "root",
  "elements": {
    "root": { "type": "card", "props": { "title": "Growth", "subtitle": "Signups, last 7 days" }, "children": ["chart"] },
    "chart": { "type": "chart", "props": { "type": "bar", "title": null, "data": { "$state": "/signups" } } }
  },
  "state": { "signups": [] }
}
```

***

## 3. HTTPS endpoint (`url`)

The most direct way to read a **REST API or any HTTPS document**: the *server* fetches your fixed URL, parses the body, and serves the parsed value. No sandbox, no provider — one flat platform fee per upstream fetch (cached). Use this instead of `web_fetch` whenever the endpoint returns data rather than a web page.

* **SSRF-guarded**: only `http(s)`, every redirect hop re-validated, private/loopback/metadata addresses refused.
* **Parsing** (`format`): `json` (default when the content-type says JSON), `xml` (also RSS/Atom content-types — converted to JSON, attributes prefixed `@`, mixed text under `#text`), `text` (raw string), or `auto` (default — follows content-type, then sniffs JSON, then falls back to text).
* **`headers`** may carry an API key. They are stored server-side only and are stripped from everything a browser receives. On a cross-origin redirect, `Authorization`/`Cookie` are dropped (standard fetch behaviour).
* **Caps**: 1 MB response body, 10 s timeout (operators can tune `live_sources` in `config/widget-security.json`).

### Simple — public JSON API

```json theme={null}
[
  {
    "url": "https://api.coindesk.com/v1/bpi/currentprice.json",
    "result_target": "/btc",
    "result_path": "bpi.USD.rate",
    "refresh_interval_sec": 300
  }
]
```

```json theme={null}
{
  "root": "root",
  "elements": {
    "root": { "type": "card", "props": { "title": "Markets", "subtitle": null }, "children": ["price"] },
    "price": { "type": "stat", "props": { "label": "BTC / USD", "value": { "$state": "/btc" }, "prefix": "$" } }
  },
  "state": { "btc": "—" }
}
```

### Full example — authenticated API + an XML source side by side

Two sources: a JSON status API behind a bearer token, and a legacy XML export. The XML document

```xml theme={null}
<report generated="2026-07-12">
  <system name="api" status="ok"/>
  <system name="worker" status="degraded"/>
</report>
```

parses to `{ "report": { "@generated": "2026-07-12", "system": [ { "@name": "api", "@status": "ok" }, … ] } }`.

```json theme={null}
[
  {
    "url": "https://status.example.com/api/v2/summary.json",
    "headers": { "Authorization": "Bearer sk_live_…" },
    "format": "json",
    "result_target": "/status",
    "result_path": "summary",
    "refresh_interval_sec": 120
  },
  {
    "url": "https://legacy.example.com/exports/report.xml",
    "format": "xml",
    "result_target": "/report",
    "result_path": "report.system",
    "refresh_interval_sec": 600
  }
]
```

```json theme={null}
{
  "root": "root",
  "elements": {
    "root": { "type": "card", "props": { "title": "Operations", "subtitle": null }, "children": ["uptime", "systems"] },
    "uptime": { "type": "stat", "props": { "label": "Uptime (30d)", "value": { "$state": "/status/uptime_pct" }, "suffix": "%" } },
    "systems": { "type": "list", "props": { "direction": "vertical", "gap": "sm" }, "children": ["sys"], "repeat": { "statePath": "/report", "key": "@name" } },
    "sys": { "type": "badge", "props": { "label": { "$template": "${@name}: ${@status}" }, "variant": null } }
  },
  "state": { "status": {}, "report": [] }
}
```

<Warning>
  The **result** of a source is visible to everyone who can see the dashboard (and to anonymous visitors if you use the same source on a front-page panel). The API key itself never leaves the server — but don't point a source at an endpoint whose *response* contains secrets.
</Warning>

***

## 4. Per-agent data file (`file`)

Every agent has its own data directory on the server; a `file` source reads a **`.json` or `.xml` file from its `dashboard_data/` folder** on every (cached) view. This is the recommended way to show **values your agent maintains itself**: the agent updates the file with the built-in `write_dashboard_file` tool, and the dashboard re-reads it live. Reading and writing are local disk operations — **free**.

The write side, available to the agent in owner-context sessions (owner chat/preview, schedules, the owner API — buyers and anonymous visitors can never trigger it):

* `write_dashboard_file({ file, value })` — replace the whole document.
* `write_dashboard_file({ file, value, path: "/kpis/open_tickets" })` — patch one sub-value (RFC 6901 JSON Pointer); the rest of the file is preserved. `"/items/-"` appends to an array.
* Custom code tools can call it too: `await ctx.tools.write_dashboard_file({ file, value, path })`.
* Only files that a saved data source **declares** are writable (save the dashboard first, then let the agent write), only `.json` (never `.xml`), values are sanitized to inert display data and capped at 1 MB.
* A file that hasn't been written yet reads as `null` — seed your spec's `state` so the panel renders sensibly empty.
* Self-hosting? Any external process (cron, ETL, another app) may write `AGENT_DATA_DIR/{user}/{agent_id}/dashboard_data/` directly; `.xml` files are parsed like the [`url` XML format](#3-https-endpoint-url).

### Simple — one KPI the agent keeps current

```json theme={null}
[
  { "file": "metrics.json", "result_target": "/metrics" }
]
```

```json theme={null}
{
  "root": "root",
  "elements": {
    "root": { "type": "card", "props": { "title": "Support", "subtitle": null }, "children": ["open"] },
    "open": { "type": "stat", "props": { "label": "Open tickets", "value": { "$state": "/metrics/open_tickets" } } }
  },
  "state": { "metrics": { "open_tickets": "—" } }
}
```

Then tell the agent (Instructions): *"After every support conversation, update the dashboard: `write_dashboard_file` file `metrics.json`, path `/open_tickets`, value = the current count."* — or have a scheduled run push the whole document:

```json theme={null}
write_dashboard_file({
  "file": "metrics.json",
  "value": { "open_tickets": 12, "updated_at": "2026-07-12T09:30:00Z" }
})
```

### Full example — a status board the agent patches field-by-field

One file backs several panels; the agent (or a code tool via `ctx.tools`) patches individual pointers as it works, so unrelated values never clobber each other.

```json theme={null}
[
  { "file": "board.json", "result_target": "/board", "refresh_interval_sec": 60 }
]
```

```json theme={null}
{
  "root": "root",
  "elements": {
    "root": { "type": "card", "props": { "title": "Agent status board", "subtitle": { "$template": "Updated ${/board/updated_at}" } }, "children": ["row", "chart", "note"] },
    "row": { "type": "list", "props": { "direction": "horizontal", "gap": "lg" }, "children": ["leads", "booked"] },
    "leads": { "type": "stat", "props": { "label": "Leads today", "value": { "$state": "/board/kpis/leads" }, "change": { "$state": "/board/kpis/leads_change" }, "changeType": "up" } },
    "booked": { "type": "stat", "props": { "label": "Meetings booked", "value": { "$state": "/board/kpis/booked" } } },
    "chart": { "type": "chart", "props": { "type": "line", "title": "Bookings this week", "data": { "$state": "/board/weekly" } } },
    "note": { "type": "alert", "props": { "title": null, "message": { "$state": "/board/note" }, "variant": "info" } }
  },
  "state": { "board": { "updated_at": "…", "kpis": { "leads": 0, "leads_change": "", "booked": 0 }, "weekly": [], "note": "No updates yet." } }
}
```

Example agent calls over a day:

```json theme={null}
write_dashboard_file({ "file": "board.json", "path": "/kpis/leads", "value": 14 })
write_dashboard_file({ "file": "board.json", "path": "/weekly/-", "value": { "label": "Sat", "value": 3 } })
write_dashboard_file({ "file": "board.json", "path": "/note", "value": "Campaign B is outperforming A 2:1." })
write_dashboard_file({ "file": "board.json", "path": "/updated_at", "value": "2026-07-12T17:00:00Z" })
```

***

## 5. Read-only database (`db`)

Runs an owner-fixed, **read-only** SQL query against your own **Postgres** database (standard connection string) and writes the rows (an array of objects) into state. The query executes inside a `READ ONLY` transaction with a statement timeout, so it cannot write, and a runaway query is cut off.

Guarantees & limits: single statement only; writes rejected; results truncated to 500 rows and capped at 1 MB JSON; connection string and SQL live server-side only. On the hosted platform, connection strings resolving to **private/loopback hosts are refused** — self-hosted operators can allow them with `live_sources.db_allow_private_hosts` in `config/widget-security.json` (e.g. to reach a Postgres on `localhost`). Alias your columns to match what the spec needs — e.g. `AS label` / `AS value` feeds a `chart` directly.

### Simple — one number

```json theme={null}
[
  {
    "db": { "engine": "postgres", "url": "postgresql://readonly_user:s3cret@db.example.com:5432/shop?sslmode=require", "query": "SELECT COUNT(*) AS n FROM orders WHERE status = 'open'" },
    "result_target": "/open_orders",
    "result_path": "0.n"
  }
]
```

```json theme={null}
{
  "root": "root",
  "elements": {
    "root": { "type": "card", "props": { "title": "Shop", "subtitle": null }, "children": ["orders"] },
    "orders": { "type": "stat", "props": { "label": "Open orders", "value": { "$state": "/open_orders" } } }
  },
  "state": { "open_orders": "—" }
}
```

### Full example — KPIs + chart + latest rows

Three sources against one database: a single-row KPI query, a chart-shaped aggregate, and a recent-items list. Bind values with `$1`-style placeholders via `params`.

```json theme={null}
[
  {
    "db": {
      "engine": "postgres",
      "url": "postgresql://readonly_user:s3cret@db.example.com:5432/shop?sslmode=require",
      "query": "SELECT COUNT(*) AS open, COALESCE(SUM(total),0)::int AS revenue FROM orders WHERE created_at > now() - interval '1 day'"
    },
    "result_target": "/kpis",
    "result_path": "0",
    "refresh_interval_sec": 300
  },
  {
    "db": {
      "engine": "postgres",
      "url": "postgresql://readonly_user:s3cret@db.example.com:5432/shop?sslmode=require",
      "query": "SELECT to_char(day, 'Dy') AS label, orders::int AS value FROM daily_order_counts WHERE day > now() - interval '7 days' ORDER BY day"
    },
    "result_target": "/weekly",
    "refresh_interval_sec": 3600
  },
  {
    "db": {
      "engine": "postgres",
      "url": "postgresql://readonly_user:s3cret@db.example.com:5432/shop?sslmode=require",
      "query": "SELECT id, customer_name, status FROM orders WHERE status = $1 ORDER BY created_at DESC LIMIT 10",
      "params": ["open"]
    },
    "result_target": "/recent",
    "refresh_interval_sec": 120
  }
]
```

```json theme={null}
{
  "root": "root",
  "elements": {
    "root": { "type": "card", "props": { "title": "Store dashboard", "subtitle": "Live from Postgres" }, "children": ["kpis", "chart", "recent"] },
    "kpis": { "type": "list", "props": { "direction": "horizontal", "gap": "lg" }, "children": ["open", "revenue"] },
    "open": { "type": "stat", "props": { "label": "Orders (24h)", "value": { "$state": "/kpis/open" } } },
    "revenue": { "type": "stat", "props": { "label": "Revenue (24h)", "value": { "$state": "/kpis/revenue" }, "prefix": "$" } },
    "chart": { "type": "chart", "props": { "type": "bar", "title": "Orders per day", "data": { "$state": "/weekly" } } },
    "recent": { "type": "list", "props": { "direction": "vertical", "gap": "sm" }, "children": ["order"], "repeat": { "statePath": "/recent", "key": "id" } },
    "order": { "type": "badge", "props": { "label": { "$template": "#${id} — ${customer_name} (${status})" }, "variant": null } }
  },
  "state": { "kpis": { "open": 0, "revenue": 0 }, "weekly": [], "recent": [] }
}
```

<Tip>
  Create a dedicated **read-only database role** for dashboard sources anyway (`GRANT SELECT` only). The platform enforces a read-only transaction, but least privilege at the database beats trusting any single layer.
</Tip>

***

## 6. Agent-push panels (`writable`) — custom pages & front page

On [custom dashboard pages](/custom-pages) and live front-page panels, a source may instead be `{ "writable": true, "key": "sales", "result_target": "/sales" }` — no pull at all; the panel shows whatever the agent last pushed with `update_widget_data({ key: "sales", value: … })`. The Agent Dashboard section deliberately doesn't support this kind; a [`file` source](#4-per-agent-data-file-file) covers the same need with per-field patching on top.

***

## What each kind costs & where it runs

| Kind   | Executed by                         | Billed                                                | Bounded by                               |
| ------ | ----------------------------------- | ----------------------------------------------------- | ---------------------------------------- |
| `tool` | The tool's own runtime, server-side | The tool's per-call fee (RSS poll, web fetch/search…) | Cache window, kill switches              |
| `code` | Code sandbox                        | Sandbox execution seconds                             | Cache window, kill switches              |
| `url`  | Direct server fetch                 | Flat per-fetch platform fee                           | 1 MB / 10 s caps, cache window           |
| `file` | Local disk read                     | **Free**                                              | 1 MB cap, cache window                   |
| `db`   | Postgres `READ ONLY` transaction    | **Free**                                              | 500 rows / 1 MB / 8 s caps, cache window |

All kinds share the endpoint's rate limits and the wallet circuit-breaker (non-owner viewers are refused once your balance reaches the floor, so traffic can't drag you deep into deficit).

## Troubleshooting

* **Panel stays empty** — check, in order: the spec's `state` seeds the path; `result_target` starts with `/`; `result_path` matches the real result shape (open the browser dev tools — the dashboard POSTs to `/agent/<name>/widget-action` and the JSON response shows exactly what came back); you saved after editing (sources only run once minted).
* **`file` panel shows nothing** — the file doesn't exist yet (reads as `null` until the first `write_dashboard_file`), or the agent session wasn't owner-context, or the dashboard wasn't saved *before* the agent tried to write (the declared files are the allowlist).
* **`db` refuses to connect** — private/loopback host on the hosted platform (see above), wrong credentials, or the query isn't a single read statement.
* **Stale values** — that's the cache: one upstream run per `refresh_interval_sec` (or 120 s). Lower the interval (min 60 s) if you need it fresher.

<Info>
  Field reference: every `data_sources` property, with types, is listed in the generated [Configuration Schema Reference → agent\_dashboard](/reference/agent_dashboard).
</Info>
