Skip to Content
Cardinal UIDashboardsDashboard JSON spec

Dashboard JSON spec

Every Cardinal UI dashboard is stored as one JSON document, the dashboard spec. It describes the panels, the queries behind them, how they are laid out, and any variables. The same document is used in three places:

  • Export on an open dashboard downloads it.
  • Dashboards → Add Dashboard → Import from JSON creates a dashboard from it.
  • The Dashboards API takes it as the spec field.

A machine-readable JSON Schema is published at /schemas/dashboard.schema.json. Point your editor or CI at it to get completion and validation while you generate specs from code.

Reach out to support@cardinalhq.io for support or to ask questions not answered in our documentation.

Round trip: build in the UI, then template it

Hand-writing a spec from scratch is rarely the fastest route. Instead:

  1. Build one dashboard in the UI the way you want it.
  2. Click Export to download its JSON.
  3. Use that file as a template: substitute service names, thresholds or queries in your own tooling. If you exported a dashboard you added from the gallery, delete its libraryRef field before creating new dashboards from it; otherwise they show the gallery’s version instead of your spec.
  4. Load the results back with Import from JSON, or send the file’s spec field (jq .spec export.json) to the API.

File formats Import accepts

Import from JSON accepts either of these:

  • The export envelope that Export produces:

    { "schemaVersion": 2, "name": "Login health", "exportedAt": "2026-09-23T18:00:00.000Z", "spec": { "duration": "1h", "panels": { }, "sections": [ ] } }

    name pre-fills the dashboard name in the import dialog.

  • A bare spec, i.e. just the spec object. The dialog then uses the file name as the dashboard name.

The dialog also asks for a category, which decides where the dashboard is listed.

Import is forgiving. A panel with an unknown kind, or one missing id, title or queries, is dropped with a warning instead of failing the whole file. The same goes for an invalid variable or grid cell. The schema is stricter: it rejects unknown fields. So a spec that validates against the schema imports without warnings.

A few problems still reject the whole file: a missing duration, panels that isn’t an object, or a section without a cells list. If variables’ scope settings depend on each other in a loop, Import removes those scopes and warns you.

Top-level fields

FieldRequiredDescription
durationyesDefault time window when the dashboard opens: "15m", "1h", "24h", "7d", … Viewers can change it in the toolbar.
panelsyesObject of panels keyed by panel id. Grid cells refer to panels by this key, so keep it the same as the panel’s id.
sectionsyesOrdered list of sections. Each section holds the grid positions of its panels.
variablesnoDashboard variables (drop-down pickers) whose values are substituted into queries. See Variables.
categorynoWhere the dashboard is listed on the Dashboards page: applications, infrastructure, kubernetes, eks, ecs, rds, sqs, kafka, aws-networking, proxmox, ceph, bind, headscale, outcomes, custom. Omitted or unknown → Custom. Import from JSON ignores this field and uses the category you pick in the dialog; the API honors it.
smoothnotrue applies a display-only moving average to time-series panels. Default false.
sharedCrosshairnoHovering a time on one chart draws a line at the same time on every other time-axis chart. Default on; set false to turn it off.
schemaVersionnoSpec format version, currently 2. Export stamps it for you.

Sections and layout

Panels are placed on a 24-column grid inside sections. Sections stack vertically, and each can be collapsed.

"sections": [ { "title": "Logins", "cells": [ { "i": "logins_rate", "x": 0, "y": 0, "w": 16, "h": 8 }, { "i": "logins_now", "x": 16, "y": 0, "w": 8, "h": 8 } ] } ]
FieldDescription
titleSection header text.
cells[].iId of the panel in this cell. It must be a key in panels.
cells[].x, cells[].yColumn (0–23) and row of the top-left corner.
cells[].w, cells[].hWidth in columns (1–24) and height in row units.
rowHeightOptional fixed pixel height per row unit for this section. Without it, row height scales to fit the viewport.
defaultCollapsedOptional. true opens the section collapsed.

A panel that isn’t referenced by any cell is kept in the spec but not shown.

Panels

Every panel has these fields:

FieldRequiredDescription
idyesUnique within the dashboard. Use the same value as its key in panels.
kindyesOne of the kinds below.
titleyesPanel header text.
queriesyesList of queries. Use [] for kinds that don’t query (service-graph, log-events).
descriptionnoMarkdown help text, shown behind a ? icon next to the title.
interlinksnoClick-through links to other dashboards. See the schema’s PanelInterlink.

Queries

{ "query": "sum by (app) (rate(login_success_total[5m]))", "name": "{{app}}" }
FieldDescription
queryPromQL (default) or LogQL expression. $variable and ${variable} tokens are substituted from the dashboard’s variables.
queryKind"prometheus" (default) or "loki" for LogQL.
nameLegend name for the returned series. {{label}} inserts a label value.
colorFixed hex color for this query’s series, e.g. "#3b82f6".
hiddentrue keeps the query in the spec but doesn’t plot it.

timeseries

A line, area or bar chart over time. This is the kind to use for most metrics.

FieldDescription
variantline (default), area, stacked-area, bar, stacked-bar, normalized-bar.
unitUnit shown on the Y axis, e.g. "ms", "%", "req/s".
yAxisLabelY-axis title. Drawn as Title (unit) when unit is also set.
xAxisLabelX-axis title, drawn under the time ticks.
yMin, yMaxPin the Y axis to fixed bounds instead of auto-fitting.
integerYMaxRound the top of the Y axis up to a whole number (counts, replicas).
percentScaleHold the Y axis at 0–100.
smoothPer-panel override of the dashboard-level smooth.
highlightWhenShade the background wherever a second query is above a threshold: { "query": "…", "threshold": 0.5, "colors": { "<label>": "#hex" } }.

bar

A stacked bar chart over time, one stack segment per series.

FieldDescription
reducerHow samples in the same bar are combined: last (default), mean, max, min, sum. Use sum for counters and last/max for gauges.
unit, yAxisLabel, xAxisLabel, percentScaleAs for timeseries.
colorSingle color for every series.

stat and gauge

A single number, or an arc for gauge.

FieldDescription
calculationstat only. How the series is reduced to one number: last (default), mean, max, min, sum.
maxgauge only. Upper bound of the arc. Omit it to auto-scale.
format{ "unit": "sessions", "decimalPlaces": 0 }.
sparkline, sparklineColorDraw the underlying series as a small trend line under the number.

label

One card per series the query returns, e.g. one per service_name when the query groups by it.

FieldDescription
reducerAs for bar. Default last.
sortlabel (default), value-asc, value-desc.
formatAs for stat.
colorStatic card color.
thresholdsConditional colors: [{ "value": 0, "color": "green" }, { "value": 5, "color": "red" }]. The highest stop at or below the value wins.
timestampAgeTreat values as Unix timestamps in seconds and show how long ago the newest one was.

pie

One slice per series, reduced with reducer (default last).

log-events

Log lines with a volume chart above them. Set queries to [], and give either rawLogql (a LogQL log selector and pipeline) or logqlState (the Logs Explore builder’s state, easiest to get from an export). Add limit (default 100) to cap the number of lines, or set distributionOnly: true to show just the per-service severity chart.

service-graph

A service dependency graph built from traces. Set queries to []. Options:

  • metric: requests (default), errors or latency
  • serviceName: centre the graph on this service; a $variable works here
  • maxHops: how far out from serviceName to go (default 3)

service-gauges and service-memory-bars

Purpose-built panels for Cardinal’s system dashboards. Each expects a fixed set of queries in a fixed order. Copy one from an exported gallery dashboard rather than writing it by hand.

Variables

Variables add drop-down pickers to the dashboard. A variable named app is substituted wherever a query contains $app or ${app}.

"variables": [ { "name": "app", "label": "Application", "kind": "query", "source": { "signal": "metrics", "metric": "login_success_total", "label": "app" }, "multi": true, "includeAll": true } ]
FieldDescription
nameLetters, digits and _, not starting with a digit. This is the token used in queries.
labelText above the picker. Defaults to name.
kindAlways "query".
sourceWhere the options come from. Metrics: { "signal": "metrics", "metric": "<metric>", "label": "<label>" }, which offers the values of <label> on <metric>. Add "scope": "cluster=~\"$cluster\"" to limit the options by another variable’s selection. Logs: { "signal": "logs", "label": "<label>" }, with an optional expr selector.
multiAllow several values. They are substituted as a regex alternation, so match them with =~.
includeAllWith multi, offer an All option, substituted as .+.
defaultValueInitial selection, as a list of strings.
regexHide options that don’t match this regex.
sortalphabetical (default), alphabetical-desc, numerical, numerical-desc.

Complete example

This dashboard charts login successes and failures for one or more applications. It shows the current failure count as a single number that turns red at 5 or more.

{ "schemaVersion": 2, "category": "applications", "duration": "6h", "variables": [ { "name": "app", "label": "Application", "kind": "query", "source": { "signal": "metrics", "metric": "login_success_total", "label": "app" }, "multi": true, "includeAll": true } ], "panels": { "logins": { "id": "logins", "kind": "timeseries", "title": "Logins per minute", "yAxisLabel": "Logins", "unit": "/min", "queries": [ { "query": "sum by (app) (rate(login_success_total{app=~\"$app\"}[5m])) * 60", "name": "{{app}} success" }, { "query": "sum by (app) (rate(login_failure_total{app=~\"$app\"}[5m])) * 60", "name": "{{app}} failure", "color": "#ef4444" } ] }, "failures_now": { "id": "failures_now", "kind": "label", "title": "Failed logins (last 5m)", "reducer": "last", "thresholds": [ { "value": 0, "color": "#22c55e" }, { "value": 5, "color": "#ef4444" } ], "queries": [ { "query": "sum by (app) (increase(login_failure_total{app=~\"$app\"}[5m]))", "name": "{{app}}" } ] } }, "sections": [ { "title": "Logins", "cells": [ { "i": "logins", "x": 0, "y": 0, "w": 16, "h": 8 }, { "i": "failures_now", "x": 16, "y": 0, "w": 8, "h": 8 } ] } ] }

Dashboards show metrics. To be notified when a condition holds for a while (for example, more than 5 failed logins per minute for 10 minutes), create an alert rule with a For duration.

Versions

Axis titles (yAxisLabel, xAxisLabel) and sharedCrosshair need Cardinal UI v1.96.0 or later. Earlier versions accept a spec that contains them but don’t draw them.

Last updated on