Skip to content

Reference / Publishing

publish_dashboard

Compile a Dashies spec into a dashboard and get a stable URL back. Parameters, return shape, errors and scope rules.

Publishes a dashboard and returns its URL.

A dashboard is published as a spec. You send a Dashies spec and the server compiles it to HTML, a data island and a refresh manifest, then seeds every dataset from the connection and validates the result. A structurally broken or silently wrong dashboard cannot be published this way.

Body mode is not available. Uploading raw bytes with body (and content_type, encoding, or a hand-written source_config manifest) is refused. The arguments below are still declared, because the path may return in a future version, and they are documented so a refusal is not a surprise.

Dashboards published before this are unaffected: they keep serving and keep refreshing, and reading, listing, deleting and restoring them are unchanged.

Behaviour hints for clients: not read-only, not idempotent, not open-world.

Parameters

ParameterTypeRequiredNotes
pathstringyes<slug> or <slug>/<filename>. A bare slug means <slug>/index.html. The workspace is added server-side, as the host.
bodystringnoNot available. A publish carrying raw bytes is refused. Still declared, because the path may return.
content_typestringnoNot available, with body. Forbidden in spec mode, where a compiled spec is always text/html.
encodingstringnoNot available, with body. Beside a spec it is accepted and ignored, a compiled spec being text.
specstringnoSpec mode, document sent inline. Capped at 5242880 bytes.
spec_hashstringnoSpec mode, republish a document the server already holds. Cached for about an hour after any send. Pass that dry run's report_id beside it to have the report count what that dry run printed or counted.
report_idstringnoWith spec_hash only: the report id that document's dry run returned. The report then counts the warnings that dry run printed in full, or itself counted from an earlier one, instead of printing them again. Every warning prints when it is omitted, comes from another document or an earlier dry run, is more than about an hour old, or the dashboard's stored spec is now a different document. A dry run of the stored spec returns none.
spec_editsarraynoSpec mode, exact-string edits against the stored spec. Requires base_spec_hash.
dry_runbooleannoSpec-only. Runs compile, validation and a read-only seed and writes nothing to the dashboard. Returns a spec_hash and, usually, a report_id to pass back beside it. Default false.
base_spec_hashstringnoSpec-only lost-update guard. Required with spec_edits, where it also names the document.
namestringnoDisplay name. Defaults to a title-cased slug on insert; preserved on republish.
tagsarraynoPreserved when omitted. [] clears.
chartstringnoPreserved when omitted.
workspacestringnoWorkspace slug. Works in both modes.
source_configobjectnoNot available. The hand-written refresh manifest was only ever accepted beside a body, so it cannot be sent on its own. A spec's manifest is compiled from its datasets. Dashboards that already carry one keep refreshing, and get_source_config still reads it back.

Exactly one of spec, spec_hash and spec_edits may name the document. All three compile, validate and seed identically; the saving is transmission, never checking.

Returns

Spec mode is the one you will see. The body-mode receipt below is kept, marked, because the arguments are still declared and the path may return; a publish carrying raw bytes is refused before it can produce one.

Body mode returned the URL, the byte count and the resolved metadata:

Published to workspace.
Slug: revenue-overview
Bytes: 48213
Name: Revenue Overview

Refresh: daily (manifest stored; the cube SQL re-runs on this schedule).
URL: https://acme.dashies.ai/revenue-overview

There is no Visibility: line any more: a dashboard opens for the members of the workspace it lives in, so there was nothing left to report. A manual cadence reads Refresh: manual (manifest stored; no automatic schedule - refresh on demand). Auto-promote notes, seed notes and Warning: blocks are appended, separated by blank lines.

Body mode also rode structuredContent: { slug, filename, url, fileUrl, name, wasInsert }, plus optional refresh: { frequency } and warnings: string[]. Unreachable for the same reason, and kept for the same one.

Spec mode returns human lines then a JSON block. This one is a publish by the spec_hash and report_id a dry run returned, which is the taught flow, so its warnings are counted rather than printed again:

Published revenue-overview as mickey.
Dashboard URL: https://acme.dashies.ai/revenue-overview
Spec hash: 6f1c...
Datasets:
- revenue: its data stays with Dashies and is queried when someone opens the page, so the page does not have to carry it all, ...
Rows seeded: revenue 1000 (total unconfirmed)
Bytes: page 51204, data 18422 (body is 1% of the 5242880-byte publish limit)
First data: extracting now. The dashboard shows "updating" until it lands.
Since the dry run of this same document (spec_hash 6f1c...): 3 warnings and 1 check you owe are unchanged and not printed again, the oldest of them last printed in full at 2026-09-15T19:01:05.000Z. Nothing new was found. The JSON below still lists every warning and check, under `warnings` and `obligations`.

BEGIN_JSON
{"ok":true,"published":true,"url":"https://acme.dashies.ai/revenue-overview","slug":"revenue-overview","spec_hash":"6f1c...","datasets":[{"name":"revenue","plan":"..."}],"warnings":[...],"obligations":[...],"bytes":{"body":51204,"data":18422,"per_dataset":{"revenue":18310}},"seed_rows":[{"name":"revenue","seeded":1000,"available":null}],"assets":[],"first_data":"extracting_now","report_baseline":{"spec_hash":"6f1c...","dry_run_at":"2026-09-15T19:01:05.000Z","unchanged_warnings":[0,1,2],"unchanged_obligations":[0],"no_longer_present":0}}
END_JSON

A dry run opens with Dry run: <slug> compiled cleanly - nothing was published. and reports published:false, url:null. Under its spec hash it prints a Report id: line when it could store its report, and the JSON carries the same value as report_id, or null.

The warning area is presentation over the lists the JSON carries in full.

  • Warnings to review, the checks you owe, and routine notes each get a block, in that order, headed by how many warnings the block stands for.
  • 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 at most ONE routine note in place of the separate warnings about it, saying it publishes with NO data until a refresh reads the rows and how a page you wrote reads them. The JSON keeps the separate warnings that note stands for.
  • A publish whose First data: line says the rows are already served (unchanged, or loaded N rows) prints no note about waiting for data, which on a page of managed tiles leaves no note at all, and a closing Not printed: line counts the notes it left out. first_data in the JSON names which First data: line was printed: extracting_now, loaded, unchanged, no_new_extract or not_started, or null on a dry run and on a publish that printed none.
  • A publish that passes a dry run's spec_hash and that dry run's report_id is compared with that dry run's report. Identical warnings and checks are counted on the Since the dry run of line, which names when the oldest of them was last printed in full; what is new, or was last printed in full more than about an hour ago, prints; and report_baseline lists which items were unchanged. Everything prints when the report_id is missing, belongs to another document (a dry run of spec_edits returns its own) or to an earlier dry run of the same one, or is more than about an hour old, and when the dashboard's stored spec is now a different document than when that dry run ran. The comparison is kept as a digest of each warning with when its text was last printed, never the text, and a real publish discards it.

Read the rows-seeded line in full

The receipt reports rows in and rows shipped per dataset. Equal counts render as mix 9 (total unconfirmed), not a bare mix 9, and the suffix is load-bearing: where an engine reports no server-side total the server falls back to the length of what it received, which always manufactures agreement. Only a genuine shortfall gets the bare 3/6 form. Quote the line whole or not at all.

Errors

Argument matrix, returned before any work happens:

provide exactly one of `spec`, `spec_hash`, or `spec_edits` (got `spec` + `spec_hash`): `spec` sends the document inline, `spec_hash` republishes a document the server already validated, and `spec_edits` patches the stored spec.
provide either `body` or `spec`, not both: `spec` publishes the compiled file format, `body` publishes raw bytes.

The remaining matrix refusals, each verbatim:

dry_run is only valid with `spec` (the file-format publish); a body publish always writes.
base_spec_hash is only valid with `spec` (the file-format publish's lost-update guard).
dry_run must be a boolean.
base_spec_hash must be a non-empty string (a spec_hash returned by get_dashboard_spec).
spec must be a non-empty string (the Dashies file-format YAML).
spec_hash must be a non-empty string (a spec_hash returned by a dry run, a previous publish, or get_dashboard_spec).
content_type is not allowed with `spec`: a compiled spec is always text/html.
source_config is not allowed with `spec`: the refresh manifest is compiled from the spec's datasets and source.
spec_edits requires base_spec_hash: the edits are applied to the spec with that hash, and it is also the lost-update guard. Read the current spec with get_dashboard_spec and pass the spec_hash it returns.

In the first two, <noun> is whichever of the three spec arguments you sent, so spec_edits produces the same sentence naming spec_edits.

An edit whose old_string is not found, or is found more than once without replace_all, refuses the whole publish and writes nothing. Include more surrounding lines rather than retrying blind.

A publish carrying raw bytes is refused before anything is written, with the argument named and the fix named:

Publishing a raw HTML `body` is not available: a Dashies dashboard is published as a spec. Pass `spec` with the YAML document instead of `body`, and the server compiles the page, its numbers and its refresh settings from it. See the dashies authoring skill for the format, and validate each dataset's SELECT with validate_cube_sql first. Dashboards already published are unaffected: reading, listing, deleting and restoring them are unchanged.

Nothing is stored: no object, no row, and no tombstone.

The body-mode conditions below are no longer reachable, because that refusal lands before any of them is evaluated. They are kept, marked, because the arguments are still declared and the path may return.

ConditionText
Missing bodybody is required
Missing content typecontent_type is required
Bad base64400 invalid base64 body: <err>
Bad encoding400 invalid encoding "<enc>"
Over the cap413 payload too large: encoded body is <n> bytes, maximum is 5242880
Storage write failedr2 put failed: <err>
Database write failed after the bytes landeddb write failed after R2 put: <err>

Manifest shape errors come from the same validator the refresh cron mirrors, and include source_config must be a JSON object, source_config.manifest_version must be 1, 2, 3, or 4, source_config.connection must be a non-empty string, source_config.cube_sql must be a non-empty string, source_config.schedule must be one of manual/hourly/daily/weekly/monthly, and source_config.timezone "<tz>" is not a valid IANA time zone (e.g. "America/Los_Angeles", "Europe/London", "UTC"). Omit it to default to UTC.

A manifest that stores but fails to attach is a partial success:

Published, but failed to attach the refresh manifest: <err>. The dashboard is live as a static page; retry the same publish to attach the manifest and enable refresh.

Spec mode failures print a count, then one line per finding in the shape - [<layer>] <path or (spec)>: <message> (<hint>):

Publish failed - 1 error (nothing was published):
- [seed] datasets[0]: <the engine's own error> (the seed runs under the same time budget a scheduled refresh gets, so this cube did not finish in the time the cron would give it - publishing it would produce a dashboard that fails every refresh. If the warehouse was cold or paused, retry (it should be warm now); otherwise aggregate to a coarser grain or narrow the window. Nothing was published.)

The angle-bracketed part is the engine's message, which varies. The hint after it is ours and is fixed per failure class; the one above is the connection_timeout hint, and it is worth reading closely because a spec publish is a dry run of the refresh's clock. The seed gets the cron's budget, not the more generous authoring budget validate_cube_sql runs under, so a cube that validates in 90 seconds can still be refused here. That is the point: it would otherwise publish cleanly and then time out on every scheduled refresh, forever.

Being fixed per failure class means it carries no connection of its own, and aggregate to a coarser grain there is the in-file remedy. On a dataset whose rows Dashies holds, reach for the narrower window instead: grouping those records to what the dashboard reports at hands Dashies a summary to summarize, and a count then comes back as the number of groups you made rather than the number of records. See Datasets.

Layers are arg, compile, seed, conflict, internal and write.

A slug the app would shadow is refused on the arg layer, before anything compiles. pricing, auth, app, assets and api are routes of the web app, so a dashboard slugged with one would be hidden by the app on its own subdomain:

slug "pricing" is reserved - it would be shadowed by the app at https://acme.dashies.ai/pricing; choose a different slug.

See Reserved first path segments. THERE ARE THREE GUARD SITES AND THEY DO NOT ALL WORD IT ALIKE. Two are in the workspace writers, publishWorkspaceDashboardImpl and updateWorkspaceDashboardImpl, which share reservedSlugRefusal and end the sentence with no full stop. The third is inline in the SPEC publish path under isReservedSubdomainSlug, and it ends with one.

The receipt above is the third one, because it is the only site this tool can now reach: a raw body publish is refused before the workspace writer runs, so publish_dashboard always takes the spec path. That is also why the paragraph above says the refusal comes on the arg layer, which is a field only the spec path produces.

Renaming a dashboard away from a reserved slug is allowed, and that rename is one of the two sites that word it without the stop.

The workspace writers also have a second form, without the URL, for a request naming a workspace on a host that resolves none. The SPA never sends one.

Going over the byte cap reports the heaviest dataset by name, so you know which one to shrink:

the compiled dashboard is <n> bytes, over the 5242880-byte publish limit. The heaviest dataset is `<name>` (<n> bytes) - coarsen its grain or reduce its rows.

coarsen its grain is the server's own wording and it assumes the in-file shape. Only what ships inside the file counts toward that cap, so a dashboard whose datasets Dashies holds does not reach it through its data, and coarsening a held dataset's grain would hand Dashies a summary to summarize rather than a smaller file: it works each number out when a reader opens the page, so a statement that has already aggregated is aggregated a second time. See Datasets.

A base_spec_hash mismatch arrives under code: "spec_conflict".

One spec failure keeps published: true, because the content did land:

dashboard content was saved, but refresh installation failed - call get_dashboard_spec and republish with the returned spec_hash to complete.

Rate limited at 60 calls per user per minute, in both modes. See Rate limits.

Scope

Both modes publish into a workspace. Pass workspace, or use a workspace-authorized connection. See Scope.

Check it worked

Open the Dashboard URL the receipt printed. If you attached a manifest, confirm the Refresh: line names the cadence you asked for rather than manual, then read the state back with get_refresh_status.