---
title: Errors
description: The publish finding envelope and its layers, every semantic check code, the refresh failure classes, and the named refusals you will meet.
updated: 2026-09-15
---

Dashies refuses in three different places, and which one you hit tells you what
to do about it.

| Where | When | What survives |
|---|---|---|
| **Publish** | Before anything is written | Nothing was published. Fix and republish. |
| **Refresh** | On a schedule, unattended | The last good body stays live. The run is recorded as failed. |
| **The published page** | While a reader is looking at it | The tile states a reason in place. See [Honest degradation](/reference/tiles#honest-degradation). |

## The publish finding envelope

A failed publish reports every finding at once, in a fixed shape:

```text
Publish failed - 2 errors (nothing was published):
- [semantic] /datasets/revenue/measures/arr: measure `arr` divides one aggregate by another ...
- [compile] /tiles/3/measure: a pie cannot display a "cents"/"points"-scaled measure ...
```

Warnings use a separate line and do not stop the publish:

```text
warning: [compile] /datasets/revenue/sql: ...
```

On a publish that succeeds, warnings arrive grouped rather than one line each:
warnings to review first, then the checks you owe, then routine notes. The same
warning on several datasets prints once, with a `Same for datasets:` line naming
the rest. A dataset whose data stays with Dashies gets one note in place of
the separate warnings about it (two on a page of managed tiles, three on a page
whose markup you wrote), and a publish whose `First data:` line says its rows
are already served drops the wait from that note, which leaves no note at all
on a page of managed tiles. A publish that passes a dry run's `spec_hash` and
`report_id` counts the warnings that dry run printed or counted, on a line that
opens `Since the dry run of`, instead of printing them again; the
`publish_dashboard` reference says when that applies. The JSON part of the
response always lists every warning.

The bracketed word is the **layer**, and the path is a JSON pointer into your
spec.

| Layer | What it means |
|---|---|
| `arg` | A tool argument is wrong: the path, the metadata, a `spec_edits` entry. |
| `identity` | The document does not name the dashboard you are publishing to: a slug mismatch, or a `look.from` that does not resolve. |
| `steer` | The dataset mode could not be resolved and you have to declare it. |
| `semantic` | The spec is well-formed but the seeded data contradicts what it declares. |
| `compile` | The spec cannot be turned into a page, or the page is over the publish cap. |
| `entitlement_gate` | A dataset declares an `entitlement` block that cannot be published as written. See [Row-level security](/reference/spec#row-level-security). |
| `seed` | A dataset's SQL did not run, or did not return the whole result. |
| `conflict` | The stored spec moved under you. Re-read and re-apply. |
| `write` | The bytes or the row could not be stored. |
| `internal` | A bug on our side. Nothing was published; please report it. |

**At most ten semantic findings are reported per publish**, errors first. When
more exist, a `findings_truncated` entry says how many of which severity were cut.
Fix what is listed and re-run to see the rest.

## Semantic checks

These run against the **seeded data**, not just the spec, which is why they can
catch a declaration the data contradicts. An `error` blocks the publish; a
`warning` does not.

| Code | Severity | What it caught |
|---|---|---|
| `col_missing` | error | A declared dimension or measure has no matching output column. |
| `col_extra` | error | An undeclared output column would ship as public island bytes. Declare it or drop it from the query. |
| `type_not_numeric` | error | A measure needs a numeric column and its output column is not one. |
| `agg_mismatch` | error | The measure declares one aggregate and the query computes another. |
| `count_not_integer` | error | A `count` measure seeded non-integer values, so it is really an average. |
| `predivided_ratio` | error | A measure divides one aggregate by another in SQL. That re-sums wrong under viewer filters. Declare a `ratio` measure instead. |
| `hybrid_rows_missing_column` | error | A `hybrid` measure's source column is absent from `rows_sql`, so the row-level path cannot recompute it. |
| `lattice_no_grand_total` | error | The lattice has no all-rolled-up cell, so the unfiltered dashboard boots blank. |
| `seed_empty` | error | The dataset seeded zero rows, so nothing it declares can be verified. |
| `seed_truncated` | error | The island holds fewer rows than the dataset really has. The missing cells would refresh silently wrong. |
| `seed_probe_unavailable` | error | The completeness probe returned no count, so there is no evidence the island is whole. |
| `rate_shaped_sum` | warning | A `sum` over values all between 0 and 1. Summing rates is usually wrong. |
| `percent_points_suspect` | warning | A measure declared `percent`/`fraction` seeded values that look like 0 to 100. |
| `date_dim_not_iso` | warning | A date dimension seeded non-ISO values. Bucket to `YYYY`, `YYYY-MM` or `YYYY-MM-DD` in SQL. |
| `domain_drift_at_publish` | warning | A seeded value falls outside its declared `domains`, so the runtime filter will drop it. |
| `series_cardinality` | warning | More distinct series values than the five-colour mark palette. |
| `slice_cardinality` | warning | More distinct slices than a pie or donut will draw. |
| `funnel_stage_absent` | warning | A declared funnel stage matches no seeded value, so it renders as absent. |
| `stack_percent_mixed_sign` | warning | A `stack: percent` column seeds both positive and negative segments, so its shares would be no true proportion. |
| `declared_grain_over_cell_bound` | warning | A grain your page asks a served dataset for can exceed the query service's cell bound, so that grain would be refused. **Each grain is priced on its own**, because the service answers each as its own question and refuses each on its own, so the other grains of the same dataset keep answering. The message names **which `dashies.data` call asks for it**, the grain, the estimate, the bound and the widest dimension, once per grain that is over; ask for a narrower grain with `by` in that call, bound the dimensions with `domains` or `buckets`, or declare fewer. Estimated from the declared domains, the seed and your page's own subscriptions, on a dry run as on a publish. A dataset no call asks for is not estimated. |
| `manual_cross_check` | advisory | A dataset is built over more than one row source. |

:::danger{title="`manual_cross_check` is the one no static check can close"}
A join that matches more than one row per key **silently multiplies every
additive measure**, and nothing in the pipeline can see it. Two things make it
worse than it sounds:

On a `lattice` or `hybrid`, each cell is still the exact aggregate of the
duplicated rows, so every subtotal reconciles with every total and the dashboard
looks internally consistent. Checking it against itself finds nothing. Measured
at roughly 150 times inflation on a real star schema.

A `rows` dataset does not escape it by deferring the aggregation: the duplicated
rows are already baked into the published island, and the query engine then
aggregates them correctly and returns the inflated number.

The only thing that finds it is a comparison against an independent direct
aggregate: [Verify your numbers](/guides/verify-your-numbers).
:::

## Named publish refusals

| Code | What happened |
|---|---|
| `over_publish_cap` | The compiled body is over the publish limit. The message names the byte count and the heaviest dataset. |
| `spec_conflict` | The stored spec changed since you read it. Re-read with `get_dashboard_spec` and re-apply. |
| `spec_base_stale` | Your `base_spec_hash` no longer names the stored spec: it was republished after you read it. |
| `spec_ref_unknown` | A `spec_hash` names neither the stored spec nor a document still held server-side. Send the document. |
| `spec_too_large` | The result of applying `spec_edits` is over the spec byte limit. |
| `slug_mismatch` | The spec's `slug` differs from the publish path. The path is the target; rename with `update_dashboard`. |
| `look_from_no_body` | `look: { from }` points at a dashboard with no published body to inherit. |
| `look_from_mismatch` | `look: { from }` must equal the publish target slug. To reuse another dashboard's body, read it and inline it. |
| `custom_html_script` | A `<script>` in a `custom` tile's `html` is inert. Move runtime code to the tile's `js`. |
| `null_leading_dimension` | A dimension's leading value is null. |
| `timezone_bucket` | The SQL buckets time without naming a zone, or with an ambiguous single `AT TIME ZONE`. Not raised when the warehouse reports every value a bucket reads as a `DATE` taken from a `DATE` column. |
| `parquet_pending` | Not a failure: a dataset whose rows are kept outside the page publishes with no data until a refresh reads them. On a page of managed tiles those tiles read "Updating"; on a page whose markup you wrote, your own `dashies.data` callback is handed `status: "pending"` for that wait, so draw a "no data yet" state for it. The human report says this once per dataset, in one note that also stands for `parquet_pending_manifest` and, on a page whose markup you wrote, `author_renderer_served_dataset`, and it drops the wait from that note when the publish's `First data:` line says the rows are already served. |
| `entitlement_requires_enterprise` | A dataset declares an `entitlement` block, and row-level security is an Enterprise capability. **One code, two sentences**: the usual one says this workspace is not on the Enterprise plan and points at the plan page; the other says Dashies could not read this space's plan, so the publish was refused rather than admitted. On that one, publish again. `check_readiness` answers whether it is available in a space before you write the block. |
| `entitlement_requires_served_mode` | A dataset declares an `entitlement` block and is not on the served path. The filter is applied where the data is queried rather than in the page, so the dataset has to declare `mode: resolved`. |
| `entitlement_not_supported_on_engine` | A dataset declares an `entitlement` block and this dashboard's data source cannot have a per-viewer filter enforced on it yet. **One code, two sentences**: the message either names the data source and the ones that do work, or says Dashies could not establish which data source the connection is. Upgrading a plan does not change the first case. |
| `entitlement_root_without_dataset` | A dashboard-level `entitlement` block with no dataset-level one. The dashboard-level block carries only the admin opt-out, so on its own it opts out of a filter that does not exist. Declare the entitlement on the dataset whose rows it should filter, or remove the dashboard-level block. |
| `entitlement_removal_undeclared` | The dashboard you are publishing over filters its rows per viewer today, and this document declares no `entitlement` block, so publishing it would serve every row to every viewer. To remove the filter on purpose, set `row_level_security_removed: true` at the top level of the document; to keep it, restore the dataset's `entitlement` block. **One code, two sentences**: the other says Dashies could not read whether the dashboard filters, so the publish was refused rather than admitted. On that one, publish again rather than declaring the removal, which would unfilter a dashboard nobody has established filters. |
| `entitlement_removal_contradicted` | The document declares an `entitlement` block AND `row_level_security_removed: true`, which cannot both be true of one dashboard. Remove the removal declaration to keep filtering, or remove the `entitlement` block to stop. |
| `expr_not_plain_expression` | A measure declares a SQL expression that is not a plain expression: it carries a subquery, a relation reference, a table function or a semicolon. The expression is evaluated over a scratch relation holding only the viewer's own rows, so a subquery, a relation reference or a table function is refused here rather than failing later. The message names the measure and the word. If the word is a column name, quote it. `extract`, `substring` and `trim` are the only functions whose `from` separator is accepted; `date_part`, `year`, `substr` and the two-argument `trim` spell the same thing without one. The check is per DASHBOARD, not per dataset: a dashboard where any dataset declares an `entitlement` block has every expression measure checked, and a dashboard that declares no `entitlement` block anywhere is not checked at all. |
| `compiler_invariant`, `internal_error` | A bug on our side. Nothing was published; please report it. |

## Refresh failure classes

A scheduled refresh that fails records one class on the run. **The last good body
stays live**, so a reader never sees a half-refreshed dashboard.

| Class | Recorded message | What to do |
|---|---|---|
| `connection_auth` | `warehouse connection authentication failed` | The credential is wrong or revoked. Rotate it in the web app. |
| `connection_unreachable` | `warehouse was unreachable` | Network, firewall, or a paused warehouse. |
| `connection_timeout` | `warehouse query timed out` | The cube is too slow for the refresh budget. Narrow it. |
| `ssl_failed` | `warehouse TLS/SSL negotiation failed` | A TLS problem at the warehouse. |
| `schema_drift` | `warehouse schema changed; cube query no longer matches` | A table or column was renamed or dropped, the query asks for a name it does not return, or the login can no longer read the object, which Snowflake reports the same way. Fix the SQL, or restore the login's access. On a PostgreSQL data source not added as only accepting known IP addresses, press **Resync schema** first, so the schema your AI tool reads from Dashies is current. |
| `cube_error` | `warehouse cube query failed` | Everything else the query raised. |
| `tier_lapsed` | `the plan that pays for this dashboard's refresh has lapsed; paused until it is upgraded` | Not a failure of the query. It resumes on upgrade. |
| `connection_removed` | `data connection is no longer active` | The connection was deleted or disabled. Dependent dashboards were paused. |
| `extractor_host` | `the Dashies extraction service failed; this is ours, not your warehouse` | The refresh failed inside Dashies, not at your warehouse. Nothing in the connection or the cube needs changing. Read the recorded reason for which part failed. |
| `audit_refused` | `the data this refresh produced did not pass its checks against the previous version, so the dashboard is still serving the last version that did` | Not a fault at either end. Nothing was published and the previous numbers stay live. Read the recorded reason for which check refused. |

Only three of those are evidence the **connection itself** is unhealthy:
`connection_auth`, `ssl_failed` and `connection_unreachable`. The rest are about
one dashboard's query, which is why a connection can read `active` while one of
its dashboards keeps failing.

:::note{title="`connection_timeout` is deliberately not a connection-health signal"}
A timeout means a slow cube on a healthy warehouse. Counting it would flip a
connection to failing because one dashboard grew too big, and take every other
dashboard on that connection with it.
:::

Never the raw driver text: the executor drops the message and the detail before
the class is recorded, because they carry the remote host, user, database and
port.

## Authentication failures

A request to the web app carrying an unusable session cookie returns
`401 {"ok":false,"error":"unauthenticated (<reason>)"}`. The reason is a stable
machine token, mainly for log analysis:

| Reason | Meaning |
|---|---|
| `no_cookie` | No session cookie was sent. |
| `malformed` | The cookie or its token could not be parsed. |
| `expired` | The token is past its expiry. |
| `invalid_signature` | The signature did not verify. |
| `wrong_issuer` | The token was issued by something else. |
| `wrong_audience` | The audience claim does not match: for example an anon key used as a session. |
| `no_sub` | The token carries no subject. |
| `fetch_failed` | The signing keys could not be fetched. Transient. |

Any of these means sign in again. **An MCP tool never surfaces these**: the client
re-runs the browser flow on its own. See
[the authorization flow](/reference/mcp-tools/conventions#authorization).

## Check it worked

Publish with `dry_run: true` first. It runs the whole pipeline, seed included,
and writes nothing, so every finding above is reachable without creating a
dashboard. A clean dry run and a failing real publish differ only in the `write`
layer.
