Dashboard spec (v1)
Every field of the Dashies spec v1 YAML document: types, bounds, defaults, and the combinations the schema refuses outright.
The spec is a YAML document you hand to
publish_dashboard. The server
compiles it into HTML, validates it, runs each dataset's SQL once to seed the
data island, and stores the manifest that later refreshes read.
The machine-readable schema is published at
https://dashies.xyz/schema/dash/v1.json. This page is that schema in prose, and
a build guard fails this site when a field or a tile type exists in the schema
with no entry here.
Everything on this page is the shape contract. The rules about whether the numbers are right live in Measure correctness and Designing the cube.
Top-level fields
| Field | Type | Required | Bounds and notes |
|---|---|---|---|
dashies | integer | yes | Always 1. The format version. |
title | string | yes | 1 to 120 characters. The dashboard heading. |
source | object | yes | Where the SQL runs and how often. See Source. |
datasets | map | yes | 1 to 8 entries. Keys match ^[a-z][a-z0-9_]{0,31}$. See Datasets. |
tiles | array | one of | 1 to 64 tiles. See Tile types. Mutually exclusive with look. |
look | object | one of | Your own HTML instead of generated tiles. Mutually exclusive with tiles. See Look. |
slug | string | no | 1 to 64 characters matching ^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$. Defaults to the slug in the publish path. |
description | string | no | Up to 4,000 characters of Markdown, rendered under the title. |
intent | string | no | Up to 2,000 characters. Notes for whoever edits this spec next. Never rendered. |
layout | object | no | columns (always 12) and max_width (640 to 1920). Refused alongside look. |
theme | object | no | See Theme. Refused alongside look. |
Exactly one of tiles or look is required. Declaring look also forbids
theme and layout, because in look mode the markup is yours and there is no
generated chrome for either to style.
Source
| Field | Type | Required | Bounds and notes |
|---|---|---|---|
connection | string | yes | self, or the UUID of a warehouse connection you may use. |
schedule | string | yes | One of manual, hourly, daily, weekly, monthly. |
timezone | string | no | 1 to 64 characters. An IANA zone name, checked against the database's zone table. |
self is the built-in no-PII Dashies metrics view and needs no setup. A warehouse
connection is created in the Dashies web app, never through MCP, and requires a
paid plan. Resolution rules and the refusals are in
Shared rules.
timezone anchors the schedule, not the SQL. Nothing propagates it into your
query. Bucket your dates in your business time zone inside the SQL, and never
rely on a session time zone: see SQL dialect notes.
Datasets
A dataset is one SQL statement plus the declaration of what its output columns mean. One to eight per dashboard.
| Field | Type | Required | Bounds and notes |
|---|---|---|---|
sql | string | yes | 8 to 100,000 characters. One read-only SELECT. |
dimensions | map | yes | 1 to 12 entries. Keys match ^[a-z][a-z0-9_]{0,63}$. |
measures | map | yes | 1 to 24 entries. Same key pattern. |
mode | string | no | cube, lattice, hybrid or rows. Resolved for you when absent. |
rows_sql | string | yes for hybrid | 8 to 100,000 characters. Refused unless mode: hybrid is declared explicitly. |
rows_window | integer | no | 1 to 8,000,000. Refused unless mode is explicitly rows or hybrid. |
data | object | no | { mode: inline } or { mode: parquet }. Refused unless mode: rows is declared explicitly. |
intent | string | no | Up to 2,000 characters. Notes for the next editor. Never rendered. |
Each key of dimensions and measures must equal an output column name of the
SQL. Letter-case differences are reconciled for you; two output columns that
differ only by case are refused, naming both.
How mode is resolved when you omit it
The resolver only ever picks cube or lattice. Both ship aggregates. rows
and hybrid ship row-level data to every viewer, so they are always an explicit
opt-in and are never chosen for you.
| Condition | Resolved mode |
|---|---|
Every measure is a sum, count, min, max or a ratio | cube |
| A non-additive measure, every dimension bounded, and no multi or range filter over this dataset | lattice |
| A non-additive measure with an unbounded dimension | refused: declare mode: rows, or bound the dimensions |
| A multi or range filter over a non-composable measure | refused: declare mode: hybrid with rows_sql, or drop that filter |
Because the three row-level fields above are refused when mode is absent, a
dataset that never declares a mode cannot become rows, hybrid or Parquet by
accident.
The trade-offs between the four modes are in Datasets and the four modes.
Dimensions
| Field | Type | Required | Bounds and notes |
|---|---|---|---|
type | string | no | category or date. Defaults to a category. |
label | string | no | Up to 80 characters. Display name. |
domains | array | see below | 1 to 200 unique strings, numbers or booleans. Refused on a date dimension. |
buckets | integer | see below | 1 to 1,000. Refused on anything but a date dimension. |
intent | string | no | Up to 1,000 characters. Never rendered. |
On a lattice or hybrid dataset every dimension must be bounded: a category
dimension needs domains, a date dimension needs buckets. That declaration is
what the 50,000-cell estimate is computed from, so an unbounded dimension there is
refused rather than guessed at.
Measures
A measure is either an aggregate or a ratio. The two shapes are exclusive.
An aggregate measure:
| Field | Type | Required | Bounds and notes |
|---|---|---|---|
agg | string | yes | See the table of allowed aggregates below. |
column | string | no | The output column to read, when it differs from the measure key. Refused on a cube dataset. |
percentile | number | no | Strictly between 0 and 1. Refused unless agg is percentile_cont or percentile_disc. |
stock | boolean | no | Declares a point-in-time level rather than a flow. |
label | string | no | Up to 80 characters. |
unit | object | no | See Units. |
intent | string | no | Up to 1,000 characters. Never rendered. |
A ratio measure:
| Field | Type | Required | Bounds and notes |
|---|---|---|---|
ratio | object | yes | { num, den }, each naming another measure key. Optional num_scope: all and den_scope: all compute that side over the unfiltered dataset. |
label | string | no | Up to 80 characters. |
unit | object | no | See Units. |
intent | string | no | Up to 1,000 characters. Never rendered. |
A ratio is the correct way to express an average, a rate or a share. Store the numerator and the denominator as additive measures and let the runtime divide under whatever filter is on screen; a stored average cannot be re-derived once the filter changes.
The schema declares twelve aggregates, and the mode decides which are legal:
agg | cube | lattice and hybrid | rows |
|---|---|---|---|
sum, count, min, max | yes | yes | yes |
avg, count_distinct, median, percentile_cont | no | yes | yes |
percentile_disc, stddev, variance, mode | no | yes | no |
stock: true marks a measure that is a level rather than a flow, so it is never
summed across periods. Omitting it on a level is the single most expensive
authoring mistake in this product: see
Measure correctness.
Units
| Field | Type | Required | Bounds and notes |
|---|---|---|---|
kind | string | yes | currency, percent, count or number. |
scale | string | see below | cents or units for currency; fraction or points for percent. Required for both, and refused for count and number. |
currency | string | no | Three upper-case letters, an ISO 4217 code. Refused unless kind is currency. |
decimals | integer | no | 0 to 6. |
compact | boolean | no | Renders 1,200,000 as 1.2M. |
scale says how the raw number is stored. A currency measure holding whole
currency units declares scale: units; a percent measure holding 0.25 declares
scale: fraction.
`cents` and `points` are refused at publish today
The schema accepts all four values, but the display divisor that would rescale
them is not live, so the compiler blocks any measure declaring scale: cents or
scale: points on every dataset mode. The refusal reads:
scale "cents" requires the display-divisor runtime (not yet live): divide in SQL (sum(amount_cents)/100.0 as revenue) and declare scale: unitsIt names cents in both cases. Divide in the SQL, and declare scale: units
for currency or scale: fraction for percent. That is one line in your query and
it is the only shape that publishes.
Look
look replaces the generated tiles with your own markup.
| Field | Type | Required | Bounds and notes |
|---|---|---|---|
html | string | one of | 1 to 4,194,304 characters. Your complete dashboard body. |
from | string | one of | 1 to 64 characters matching the slug pattern. Reuse the markup of another dashboard of yours. |
Exactly one of the two. Every data-dash binding in your markup must resolve
against the datasets you declared, or the publish is refused; the roles and
attributes are in Runtime attributes.
Declaring look forbids tiles, theme and layout.
Theme
| Field | Type | Required | Bounds and notes |
|---|---|---|---|
accent | string | no | A six-digit hex colour, #RRGGBB. |
font | string | no | sans, serif or mono. |
density | string | no | compact, comfortable or spacious. |
mode | string | no | light, dark or auto. |
css | string | no | Up to 50,000 characters, injected as a stylesheet. |
css is the escape hatch for anything the four knobs above do not cover. The
custom properties it should target are in
Theme tokens.
Check it worked
Publish with dry_run: true. The pipeline compiles, validates and seeds exactly
as a real publish would, writes nothing, and reports the resolved mode for every
dataset, the compiled byte size against the publish cap, and the rows each dataset
seeded.