This page assumes you already know the json-render component and binding model —
card, list, stat, input, $state, $bindState, repeat, on.press actions, the “every prop key must be present” rule, etc. All of that lives in Generative UI and applies here unchanged. This page only covers what is unique to custom dashboard pages: the data_sources, data_files, and settings channels.Custom dashboard pages are a Pro / Business feature. On the Solo plan the editor is unavailable.
Where a page lives and who sees it
A page is one entry in your agent’scustom_dashboard_pages list. Each entry has:
1
Create & order on /web
The Dashboard pages card on the Web page is where you add a page (give it a name), toggle it on/off, and drag to reorder. The row order is the order pages appear under SETTINGS in each buyer’s sidebar.
2
Build at /custom-pages/[id]
The per-page editor is where you write the json-render
spec and declare data_sources, data_files, and settings. A live preview renders as you type.3
Buyers open it at /agent/[name]/dashboard/custom/[id]
A disabled page, an
owner-only page viewed by a buyer, or a missing id all redirect to the dashboard’s Details page rather than 404.The four data channels at a glance
Everything below writes into — or reads out of — the spec’s state at a JSON Pointer path (/headlines, /settings/paused). The spec then reads state with { "$state": "/path" }, binds editable inputs with { "$bindState": "/path" }, and persists with a button action — exactly as in Generative UI.
Free-text prose inputs are disabled on dashboards. A dashboard is not a scratchpad.Not allowed (rendered disabled, with a
disabled tag) on a custom dashboard page — input of:kind: "text"kind: "textarea"
input of kind: "email", "tel", "url", "password", "number", "integer", "select", "checkbox". Their values are cleaned on save (control chars stripped, length-capped).To collect free-form prose from a user, use a chat form — never a dashboard input. See Per-buyer scratchpad.result_target— a JSON Pointer (/headlines) saying where in state the value is written. Required; must start with/.result_path(pull only) — a dotted path into the tool/code result to extract before writing (e.g.rows, ordata.items). Note: dotted, not a JSON Pointer.- Writes are destructive — the value at
result_targetis replaced, not merged.
1. Live data (pull)
A pulldata_source fetches data when a viewer opens the page (and on an optional refresh interval). Each viewer’s browser sends only an opaque action_id to the server; the tool name, params, and code never reach the browser. The result is written to state at result_target, where your spec reads it.
There are five kinds — set exactly one per source: a tool call, a code snippet, an HTTPS url, a per-agent data file, or a read-only db query. The first two are shown below; url, file and db work identically here and are documented with simple + full examples on Agent Dashboard — Live Data.
1a. Tool-based source
web_fetch, web_search, read_rss_feed, read_recent_tweets, x_read_mentions, x_search_tweets, calculate, or any of your own code tools. params are owner-fixed — the browser can’t change them.
Reading it in the spec — because table.rows and key_value.items are literal-only, render a dynamic array with repeat + $item (see Repeating over a list):
Seed the
result_target path in state (here "headlines": []) so the panel renders cleanly before the first fetch returns.1b. Code-based source
Usecode instead of tool to compute the value in a sandbox (language is "javascript" or "python"). The function returns a value; result_path extracts from it; result_target places it in state.
$state:
1c. HTTPS endpoint, data file, and database sources
Three more pull kinds cover the common “just show me my data” cases without a tool or sandbox:url— the server fetches a JSON/XML/text endpoint directly (SSRF-guarded;headersmay hold an API key and stay server-side;format: auto | json | xml | text).file— a.json/.xmlfile in this agent’sdashboard_data/folder, which the agent keeps current via thewrite_dashboard_filetool (whole document or one JSON-Pointer field at a time). Free.db— a read-only Postgres query returning rows; runs in aREAD ONLYtransaction with row/time caps. Free.
Cost & caching. Pull sources run per viewer and may cost credits (a tool call, a sandbox run, or a flat fee per
url fetch — file/db are free), so a server-side cache collapses all viewers + refreshes within refresh_interval_sec into one upstream call. Pick the longest interval you can tolerate. refresh_interval_sec is clamped to 60–86400 s (default 120 s). Non-owner viewers are blocked when the owner’s credit balance is at the floor.2. Agent push (writable panels)
A writabledata_source is a panel whose value the agent sets — perfect for a KPI, a status counter, or a chart the agent refreshes from a scheduled run or its own work. No tool runs when a viewer loads it; the browser just reads the last value the agent stored.
Declare it with writable: true and a stable key — and no tool/code/params:
update_widget_data tool (auto-registered for you whenever the agent has writable panels), addressing the panel by its key:
result_target like any other state — e.g. a repeating bar of stats:
3. Per-buyer scratchpad (data_files)
A data_files slot gives each buyer their own private, persistent, editable value on the page — a checklist, a saved choice, a toggle. It is copy-on-write: your seed is the default until the buyer saves once, after which their copy is theirs forever (changing the seed later never touches an existing buyer’s data). The agent never reads these.
Declare a slot:
Read (create): on load the slot resolves to the buyer’s saved value, or the
seed if they’ve never saved. The spec binds editable inputs with $bindState and saves with a button whose action is submit + params.savePageData: true (this is repeatable — not a chat submit; the form stays open):
1
Create
Declare the slot with a
seed. First load shows the seed.2
Read
On every load the slot resolves to the buyer’s own saved value (or the seed).
3
Update & save
The buyer edits
$bindState inputs; clicking the savePageData button writes their copy. Clicking again overwrites it.Saving collects the current state of every declared
data_files slot on the page and writes each as the buyer’s private copy. Without a save click, edits are lost on reload. Up to 20 slots per page; each value ≤ 256 KB. When you (the owner) view the page, you are your own buyer — your saves go to your own copy.4. Owner settings (the agent reads these)
Asettings entry is an owner knob — a toggle, number, text field, or select — that does two things: it can be edited on the page (by the owner only), and the agent reads it at runtime to change its behaviour. This is the one channel that crosses from the page into the agent.
Declare settings:
Each setting seeds into state at
/settings/{key}. The agent reads them two ways:
- In its prompt — enabled pages’ settings are injected on every turn as a
<dashboard_settings>block:- booking_paused (Pause bookings): off. - On demand — the
get_dashboard_settingstool returns the live values:
{ "$bindState": "/settings/KEY" } and add a button with params.saveSettings: true. Saving is owner-only (a buyer gets a 403); values are coerced to their kind and persisted to the agent config:
The agent reads the new value on its next turn (the prompt block and
get_dashboard_settings both read live). Settings are per-page; up to 20 per page. Use settings (not a scratchpad) whenever the agent needs to act on the value.Interactive actions (click → run)
Buttons on a custom page can also run work on click — the samerunCode and callTool server actions documented for Generative UI. They write their result into state at a target pointer (or show a modal/toast), so you can build “Refresh”, “Search”, or “Recalculate” buttons that don’t depend on a page reload. See Generative UI → Server actions for the full contract.
A complete page (all four channels)
Limits & validation
The
spec is validated against the json-render catalog on save — an invalid layout is rejected with a precise error. The tool/code/params of a data_source are minted into an opaque action_id and stripped before anything reaches a browser.
Building a page with the AI helper
Instead of hand-writing the JSON, ask the AI Helper: the agent has abuild_custom_dashboard_page tool that creates (or updates, by reusing the same page_id) a page with its spec, data_sources, data_files, and settings in one shot, runs the same validation, and enables it immediately. You can then fine-tune it in the editor.
See also
- Generative UI — the json-render component, binding, expression,
repeat, and server-action reference (applies here unchanged). - Web — where pages are created, ordered, and toggled.
- Custom tools — your own code tools, usable as a
data_source. render_ui/collect_form— collect input in a conversation (the agent does read these), as opposed to a page scratchpad (it doesn’t).