Skip to content

Dashboards

Dashboards are configurable, widget-based views over your data. Each dashboard belongs to an application and is rendered from a stored layout of widgets — charts, KPIs, embedded tables, and text. A single widget can read from a different application, so one dashboard can combine data from CRM, ITSM, and HR without weakening per-application security.

A dashboard is a record in the system.dashboards table, identified within its application by a slug. Its layout is a JSON document describing the grid and its widgets. Every widget’s query runs through the same authorized read path as the rest of the platform, so row-level security, column masks, and application scope always apply — a viewer only ever sees the rows they are allowed to see.

GET /api/apps/{app}/dashboards # list dashboards in an app
GET /api/apps/{app}/dashboards/{slug} # get one dashboard's layout
PATCH /api/apps/{app}/dashboards/{slug} # update the layout
GET /api/apps/{app}/dashboards/{slug}/data # run every widget query, get results
POST /api/apps/{app}/dashboards/{slug}/data # same, with filter parameters in the body

A widget declares what to render and where to render it. Common widget types include chart, page_embed, table_embed, text, and divider. Each data-bound widget carries an inline query (a table, filters, aggregations, group-by, sort, and limit) or references a named data source.

{
"id": "open-deals",
"type": "chart",
"title": "Open deals by stage",
"inline_query": {
"table": "deals",
"filters": "eq(status, open)",
"aggregations": [{ "function": "count", "alias": "count" }],
"group_by": ["stage"]
},
"position": { "x": 0, "y": 0, "w": 6, "h": 4 }
}

Widgets can also declare KPI targets with red/amber/green thresholds, drilldown behavior on click, and a visible_to list to scope a widget to specific roles.

A dashboard can define parameters — a filter bar shared across widgets (enum, date-range, text, or number-range). Widgets opt in by listing parameter names in their query. When you POST to the /data endpoint with parameter values, every participating widget re-runs with those filters applied.

By default a widget reads from the dashboard’s own application. Set a widget’s data_app to read from a different application instead:

{
"id": "incidents-this-week",
"type": "chart",
"title": "Open incidents",
"data_app": "itsm",
"inline_query": { "table": "incidents", "filters": "eq(status, open)" },
"position": { "x": 6, "y": 0, "w": 6, "h": 4 }
}

Cross-app dashboards are built by combining several independently-authorized single-app reads — never by a cross-application join. Each widget resolves its own application’s permissions and RLS, and the results are combined for display. This keeps every source’s security enforced by construction:

  • A viewer who can read the widget’s data app sees its data, filtered by that app’s row-level security.
  • A viewer who cannot read that app gets a per-widget “forbidden” indicator — the rest of the dashboard still renders. There is no all-or-nothing failure.
  • Single-app dashboards are unaffected: a widget with no data_app behaves exactly as before.

Instead of embedding a query in each widget, you can define a named data source and point widgets at it by name. Data sources are stored in the system.data_sources table and executed on demand:

POST /api/apps/{app}/data-sources/{name}/execute

A data source returns results in the same shape as an inline widget query, so the same definition can back multiple widgets across multiple dashboards.