Dashboard component reference
Reference for dashboard widgets: the JSON shape of each component, its settings and how it reads data from a table into a chart or grid.
A dashboard widget is one JSON spec that lists components and binds each figure to a query on a Loopfour table. Chat writes these specs for most users; this page is for developers who write or edit them through the API. For the plain-language guide, see Charts and tables you can use.
The same component library renders dashboards in Studio, chart previews in chat and public share pages.
Widget envelope
Create a widget with POST /api/v1/dashboards/{id}/widgets (scope dashboards:write). The body is strict: unknown keys are rejected.
| Field | Type | Required | Meaning |
|---|---|---|---|
kind | "json-render" | No | The only widget kind. Defaults to "json-render". |
config.version | 1 | No | Config format version. Defaults to 1. |
config.title | string, up to 120 characters | No | Section title shown on the dashboard. |
config.spec | object | Yes | The spec described below. |
position.w, position.h | integer 1 to 12 | No | Size hints. The server assigns the coordinates. |
PATCH /api/v1/dashboards/{id}/widgets/{widgetId} replaces the whole config; it is not a partial merge of the spec. See the Dashboards API for the other endpoints.
Spec shape
| Key | Meaning |
|---|---|
root | The id of the top element. It must exist in elements. |
elements | A map of element id to { type, props, children, on?, watch? }. type is a component name from the catalog below. |
state | Optional. May only seed filters, for example a default period. Any other key is rejected. |
How figures get their data
Figures do not hold literal numbers. A loadData action queries one table and writes the result into state at /data/<key>. Components read it with a {"$state": "/data/<key>/rows"} binding, or /data/<key>/value for a single aggregate.
- Put the initial loads on the root element under
on.mount. - To re-run loads when a filter changes, add a
watchentry per filter path on the root element, for example/filters/fromand/filters/to.
loadData parameters
| Param | Type | Meaning |
|---|---|---|
target | string | /data/<key>, letters, digits, _ and - only. |
tableId | UUID | A literal table id. A state reference is rejected. |
aggregate | { fn, columnId? } | fn is count, count_distinct, sum, avg, min or max. With an aggregate the result is [{label, value}], and a single-row result also fills /data/<key>/value. |
groupBy | string | Column id to group by. |
timeGrain | string | day, week, month, quarter or year, for a date groupBy. |
order | string | label_asc, label_desc, value_asc or value_desc. |
limit | integer 1 to 500 | Rows or groups returned. |
dateColumn | string | Column id the period filter applies to. |
from, to | "YYYY-MM" or {"$state": ...} | The period, usually bound to /filters/from and /filters/to. |
searchColumn | string | Column id a search box or dropdown filters. |
search | string or {"$state": ...} | The filter value. |
searchOperator | string | contains for a search box, equals for a dropdown. |
Column params (columnId, groupBy, dateColumn, searchColumn) take column ids, not display names. Without aggregate, the result is raw rows, and each row's fields are keyed by column name; a component reads them with keys such as labelKey or a table column's key.
Other actions
| Action | Where | Params |
|---|---|---|
exportCsv | on | { statePath: "/data/<key>/rows", filename? } |
navigate | on | { path }, an in-app path |
setState | on | Must write a path under /filters/ |
watch accepts loadData only, and a watch path cannot be under /data.
Components
Top-level optional props accept null. Each titled component also takes the header props.
Layout and text
| Component | Props |
|---|---|
Stack | direction (horizontal, vertical), gap (sm, md, lg), align (start, center, between). Has children. |
Grid | columns (a number or "auto"), gap. Has children. "auto" fits a row of KPI tiles. |
Card | title, description. Has children. |
Heading | text, level (h1, h2, h3). |
Text | text, muted. |
Badge | text, tone (neutral, success, warning, danger). |
Callout | text, title, tone (info, success, warning). |
Button | label, variant (primary, secondary, ghost). Bind an action under on.press. |
Figures and charts
| Component | Use it for | Key props |
|---|---|---|
Metric | One KPI tile | label, value, format (required), compareValue, compareLabel, delta (mode, invert, neutralWithin), sparkline, target, targetLabel, thresholds |
BarChart | Comparisons by period or category | Cartesian props, plus layout (vertical, horizontal), stacked, signed, netLine, showValues |
LineChart | Trends over time | Cartesian props, plus forecastFrom ("YYYY-MM"), band (lower, upper, label) |
AreaChart | Cumulative or volume trends | Same as LineChart |
ComboChart | Bars and lines on one chart | Cartesian props, plus rightFormat. Each series sets kind and optionally axis: "right" |
PieChart | Share of a total | title, data, format, height. Shows at most 5 slices; the rest group into "Other" |
BarList | Rankings | title, data, labelKey, valueKey, compareKey, format, limit (1 to 50), otherLabel, showShare, color, sort |
WaterfallChart | Bridges | title, data, labelKey, valueKey, mode (delta, absolute), startLabel, endLabel, subtotalsAfter, format, colors (increase, decrease, total), sort, height |
Cartesian props: title (required), description, data (one-series shortcut), series (up to 8), format, color, height (sm, md, lg), yDomain (min, max, zero), legend.
A series is { key, label, data, kind?, color?, role?, stack?, axis? }. role is actual, compare, budget, forecast or variance.
Tables
| Component | Use it for | Key props |
|---|---|---|
DataTable | Row lists and statements | title, data, columns (up to 40, required), searchable, searchStatePath, pageSize, rowRoleKey, indentKey, footer (totals, none), stickyFirstColumn, defaultSort |
PivotTable | Cross-tabs | title, data, rowField, columnField, valueField (all required), rowFieldLabel, rowGroupField, aggregate, format, totals, rowSort, columnSort, thresholds, collapsible |
VarianceTable | Budget against actual | title, data, labelKey, actualKey, budgetKey, favorable (all required), rowRoleKey, indentKey, format, showPercent, tolerance |
CohortHeatmap | Retention or revenue by cohort | title, data, cohortKey, periodKey, valueKey (all required), measure, normalizeTo, maxPeriods (1 to 36), format, scale, color, thresholds |
A DataTable column is { key, label, format?, width?, align?, sticky?, thresholds?, total?, negativeColor? }. total is sum, avg or none. A column format also accepts month and text.
rowRoleKey names the row field that holds header, item, subtotal or total; indentKey names the field that holds an indent level from 0 to 3. Together they lay out a statement.
VarianceTable.favorable is higher, lower or the name of a row field that holds one of those per row.
Filters
| Component | Props |
|---|---|
DateRangeFilter | fromStatePath, toStatePath (point them at /filters/from and /filters/to), presets, compare |
SearchInput | statePath, placeholder, label |
Select | statePath, options ([{ value, label }]), label, kind (entity, currency, generic) |
Presets: mtd, qtd, ytd, last_3_months, last_6_months, last_12_months, fiscal_ytd, last_fiscal_year, closed_period, all_time.
With compare: true the filter also writes /filters/compare, /filters/compareFrom and /filters/compareTo. Bind a second load to the compare range to feed Metric.compareValue.
When a spec does not seed them, the filters default to the last 12 months ending this month, with compare set to the previous period.
Shared settings
Number format
A format is a legacy string (currency, compact_currency, percent, number) or an object:
| Field | Values |
|---|---|
kind | currency, percent, pp, number, integer, months, days, ratio (required) |
currency | ISO-4217 code, for example EUR |
scale | auto, none, thousands, millions, billions |
decimals | 0 to 4 |
negative | minus or parentheses |
signDisplay | auto or always |
Missing values show —. A percent change on a zero base shows N/M. Fields left out fall back to the dashboard settings.
Color tokens
Specs name tokens, not hex values:
- Series:
series-1toseries-8(chart-1tochart-5are kept as aliases). - Status:
positive,negative,neutral,warning. - Roles:
compare,forecast.
Thresholds
Up to 8 rules, { op, value, value2?, color, label? }. op is lt, lte, gt, gte or between (which needs value2). The first matching rule wins.
Sort and totals
A sort is { by, direction }, where by is label, value or a column key and direction is asc or desc. PivotTable.totals is both, row, column, none, or { row?, column?, label? }.
Header props
| Prop | Meaning |
|---|---|
subtitle | One muted line under the title, up to 160 characters. |
definition | Tooltip text on the title, up to 500 characters. |
basis | A short basis label, such as Accrual, up to 40 characters. |
showAsOf | Show when the figure last loaded. |
actions | Up to two of export (download the rows as CSV) and open (link to the source table). open is hidden on public share pages. |
Dashboard settings
Each dashboard has one set of display defaults. A component format overrides them field by field. Set them with PATCH /api/v1/dashboards/{id} and a settings object; only the keys you send change.
| Setting | Values | Default |
|---|---|---|
locale | BCP-47 tag, for example en-GB | en-US |
currency | ISO-4217 code | USD |
negative | minus or parentheses | minus |
fiscalYearStartMonth | 1 to 12 | 1 |
density | compact or comfortable | compact |
The currency setting changes display only. It does not convert amounts.
Example
A receivables widget on a table of open invoices: a period filter, a total, the top customers and the invoice list. The table id and column ids are placeholders; take the real ones from the Tables API.
{
"kind": "json-render",
"config": {
"version": 1,
"title": "Receivables overview",
"spec": {
"root": "page",
"elements": {
"page": {
"type": "Stack",
"props": { "direction": "vertical", "gap": "md" },
"on": {
"mount": [
{ "action": "loadData", "params": { "target": "/data/open_ar", "tableId": "3f2b9c1e-7a4d-4e8b-9c2f-1d5e6a7b8c9d", "aggregate": { "fn": "sum", "columnId": "col_amount" }, "dateColumn": "col_due_date", "from": { "$state": "/filters/from" }, "to": { "$state": "/filters/to" } } },
{ "action": "loadData", "params": { "target": "/data/by_customer", "tableId": "3f2b9c1e-7a4d-4e8b-9c2f-1d5e6a7b8c9d", "aggregate": { "fn": "sum", "columnId": "col_amount" }, "groupBy": "col_customer", "order": "value_desc", "limit": 10, "dateColumn": "col_due_date", "from": { "$state": "/filters/from" }, "to": { "$state": "/filters/to" } } },
{ "action": "loadData", "params": { "target": "/data/invoices", "tableId": "3f2b9c1e-7a4d-4e8b-9c2f-1d5e6a7b8c9d", "limit": 100, "dateColumn": "col_due_date", "from": { "$state": "/filters/from" }, "to": { "$state": "/filters/to" } } }
]
},
"watch": {
"/filters/from": [
{ "action": "loadData", "params": { "target": "/data/open_ar", "tableId": "3f2b9c1e-7a4d-4e8b-9c2f-1d5e6a7b8c9d", "aggregate": { "fn": "sum", "columnId": "col_amount" }, "dateColumn": "col_due_date", "from": { "$state": "/filters/from" }, "to": { "$state": "/filters/to" } } },
{ "action": "loadData", "params": { "target": "/data/by_customer", "tableId": "3f2b9c1e-7a4d-4e8b-9c2f-1d5e6a7b8c9d", "aggregate": { "fn": "sum", "columnId": "col_amount" }, "groupBy": "col_customer", "order": "value_desc", "limit": 10, "dateColumn": "col_due_date", "from": { "$state": "/filters/from" }, "to": { "$state": "/filters/to" } } },
{ "action": "loadData", "params": { "target": "/data/invoices", "tableId": "3f2b9c1e-7a4d-4e8b-9c2f-1d5e6a7b8c9d", "limit": 100, "dateColumn": "col_due_date", "from": { "$state": "/filters/from" }, "to": { "$state": "/filters/to" } } }
],
"/filters/to": [
{ "action": "loadData", "params": { "target": "/data/open_ar", "tableId": "3f2b9c1e-7a4d-4e8b-9c2f-1d5e6a7b8c9d", "aggregate": { "fn": "sum", "columnId": "col_amount" }, "dateColumn": "col_due_date", "from": { "$state": "/filters/from" }, "to": { "$state": "/filters/to" } } },
{ "action": "loadData", "params": { "target": "/data/by_customer", "tableId": "3f2b9c1e-7a4d-4e8b-9c2f-1d5e6a7b8c9d", "aggregate": { "fn": "sum", "columnId": "col_amount" }, "groupBy": "col_customer", "order": "value_desc", "limit": 10, "dateColumn": "col_due_date", "from": { "$state": "/filters/from" }, "to": { "$state": "/filters/to" } } },
{ "action": "loadData", "params": { "target": "/data/invoices", "tableId": "3f2b9c1e-7a4d-4e8b-9c2f-1d5e6a7b8c9d", "limit": 100, "dateColumn": "col_due_date", "from": { "$state": "/filters/from" }, "to": { "$state": "/filters/to" } } }
]
},
"children": ["filter", "open_ar", "top_customers", "invoices"]
},
"filter": {
"type": "DateRangeFilter",
"props": { "fromStatePath": "/filters/from", "toStatePath": "/filters/to", "presets": ["mtd", "qtd", "ytd", "last_12_months"], "compare": false },
"children": []
},
"open_ar": {
"type": "Metric",
"props": { "label": "Open receivables", "value": { "$state": "/data/open_ar/value" }, "format": { "kind": "currency", "scale": "auto" }, "definition": "Sum of open invoice amounts due in the selected period." },
"children": []
},
"top_customers": {
"type": "BarList",
"props": { "title": "Top customers by open balance", "data": { "$state": "/data/by_customer/rows" }, "format": { "kind": "currency", "scale": "auto" }, "limit": 10, "showShare": true },
"children": []
},
"invoices": {
"type": "DataTable",
"props": {
"title": "Open invoices",
"data": { "$state": "/data/invoices/rows" },
"columns": [
{ "key": "customer", "label": "Customer" },
{ "key": "invoice_number", "label": "Invoice" },
{ "key": "due_date", "label": "Due", "format": "text" },
{ "key": "amount", "label": "Amount", "format": { "kind": "currency", "negative": "parentheses" }, "align": "right", "total": "sum" }
],
"footer": "totals",
"actions": ["export", "open"]
},
"children": []
}
}
}
}
}To check a spec without saving it, send { "spec": ... } to POST /api/v1/dashboards/specs/check (scope dashboards:read). It runs the same checks as a widget write, including that the tables and columns exist in your company, and returns { "ok": true } or { "ok": false, "issues": [...] }.
Validation and limits
A widget write is rejected when the spec breaks one of these rules:
- A figure prop (
Metric.value,compareValue,sparkline, chartdata,series[].data,band.lower,band.upper, tabledata,DataTable.rowCount) holds a literal instead of a/data/...binding. - A component reads
/data/<key>that noloadDatatargets, or an action reads a state path that is not defined. - An action outside the lists above, or a
watchwith anything other thanloadData. - A
tableIdthat is not a literal UUID, or a table or column that does not exist in your company. - The spec is larger than 256 KB, or has more than 50 load bindings.
- One refresh of the widget needs more than 20 table reads. An aggregate load counts as one read; a rows load counts one per 100 rows of
limit. - A chart has more than 8 series, or a
betweenthreshold has novalue2.
Dashboard-wide limits: 40 widgets, 200 load bindings across all widgets, and 2 MB of spec JSON in total.
Public share pages
When a dashboard has a public link, GET /api/v1/dashboards/share/{token} returns a redacted copy of each spec: table ids, column ids and literal filter values are replaced with opaque targets. The page then asks POST /api/v1/dashboards/share/{token}/data for each figure by widget and target, and the server runs only the query stored in that widget. Responses carry Cache-Control: public, s-maxage=60, and an open share page reloads its figures every 5 minutes. After the link is revoked, both endpoints return 410 with "code": "GONE".
Zuora actions
Reference for the Zuora actions: the OAuth client connection, each action id, Object Query filters and paging, write rules, outputs and an example.
API reference
Reference for the Loopfour API: the base URL, authentication with API keys, pagination, error responses and rate limits for each endpoint.
Loopfour