---
title: Dashboard spec (v1)
description: 'Every field of the Dashies spec v1 YAML document: types, bounds, defaults, and the combinations the schema refuses outright.'
updated: 2026-08-05
---

The spec is a YAML document you hand to
[`publish_dashboard`](/reference/mcp-tools/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](/concepts/measure-correctness)
and [Designing the cube](/concepts/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](#source). |
| `datasets` | map | yes | 1 to 8 entries. Keys match `^[a-z][a-z0-9_]{0,31}$`. See [Datasets](#datasets). |
| `tiles` | array | one of | 1 to 64 tiles. See [Tile types](/reference/tiles). Mutually exclusive with `look`. |
| `look` | object | one of | Your own HTML instead of generated tiles. Mutually exclusive with `tiles`. See [Look](#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](#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](/reference/mcp-tools/conventions#connections).

`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](/reference/sql-dialects).

## 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](/concepts/dataset-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](#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](#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](/concepts/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`.

:::warning{title="`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:

```text
scale "cents" requires the display-divisor runtime (not yet live): divide in SQL (sum(amount_cents)/100.0 as revenue) and declare scale: units
```

It 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](/reference/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](/reference/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.
