---
title: Spec vs hand-authored
description: The YAML spec is how a dashboard is published. The two ways to write your own markup inside it, and why the raw HTML path is closed.
updated: 2026-09-10
---

**A dashboard reaches Dashies as a spec.** Your AI writes a small YAML document
naming a connection, a schedule, datasets, and tiles. The server compiles that
into the HTML, the data island, and the refresh manifest, and enforces that all
three agree.

**The raw body path is not available.** Uploading finished HTML with its own
island and a hand-written manifest is refused. It predates the spec, and the
costs of using it are below; dashboards already published that way are
unaffected.

Writing your own markup is still supported, INSIDE the spec, and the two ways to
do it are below.

## What the compiler does for you

When you publish a spec, the server owns three artifacts you never write:

- the **markup**, including every `data-dash` slot and its attributes
- the **data island**, seeded with real numbers from your SQL
- the **refresh manifest** that the scheduler will re-run

Because it owns all three, it can check them against each other. A tile bound to
a measure your query never returns is a publish error naming the exact field. A
measure that cannot be re-aggregated in the mode you chose is a publish error. A
funnel stage that is not one of the declared values of its dimension is a publish
error, because that stage would render as absent rather than as a drop to zero.

None of those are rendering surprises. They are refusals with a pointer at the
field, before anything is stored.

## What the spec looks like

A complete, minimal one. This publishes as written, against the built-in `self`
connection:

```yaml
dashies: 1
title: Dashboards published
source:
  connection: self
  schedule: daily
datasets:
  main:
    sql: select day, sum(dashboards_published) as dashboards from dashies_usage_metrics group by day order by day
    dimensions:
      day:
        type: date
    measures:
      dashboards:
        agg: sum
tiles:
  - type: kpi
    measure: dashboards
    title: Dashboards published
  - type: chart
    chart: line
    x: day
    measure: dashboards
```

Four required keys at the top level: `dashies`, `title`, `source`, and
`datasets`. `source` needs a `connection` and a `schedule`. Each dataset needs
`sql`, `dimensions`, and `measures`. Then either `tiles` or the whole-look escape
hatch below, never both.

Note that `connection: self` is written out. The convenience of omitting it
applies to the exploration tools, not to a spec, where leaving it out is a
missing-required-property error.

## The two ways to write your own markup

Both stay on the spec path, so you keep validation, seeding, and scheduled
refresh. You give up only the parts you explicitly opt out of.

**Neither is a fallback.** A user who wants the dashboard to look a particular
way is asking for one of these, and reaching for it is the product working as
intended rather than a workaround for something the tiles could not do.

### A `custom` tile

One tile whose HTML and JavaScript you write yourself, reading from named
datasets. Everything else on the dashboard stays a managed tile. Use it for a
visualization the tile vocabulary does not cover.

Your HTML is mounted verbatim, and your JavaScript runs after the island exists,
so it can read the data. A `<script>` tag inside the custom HTML is inert and
produces a warning rather than executing.

### The whole-look `look` field

`look` replaces the entire page body with HTML you supply, while the datasets,
the seeding, and the schedule stay managed. It is mutually exclusive with
`tiles`, and with `theme` and `layout`, because it owns the whole page.

The variant worth knowing is `look: { from: <slug> }`, which references the
**current published body** of the dashboard you are publishing to. That is how
you change a dataset or a schedule without re-sending the markup, and how an
existing hand-authored dashboard converts to the spec path without its appearance
changing by a byte.

## The raw body path is not available

**Publishing finished HTML with a hand-written manifest is refused.** The `body`,
`content_type`, `encoding` and `source_config` arguments are still declared on
`publish_dashboard`, because the path may return in a future version, and each
says it is unavailable. Dashboards published that way before are unaffected: they
keep serving and keep refreshing on their own contracts, and reading, listing,
deleting and restoring them are unchanged.

So the two ways to write your own markup above are the ways: a `custom` tile, or
the whole-look `look` field. Both stay on the spec path.

The trap the raw path used to be was treating it as the fallback when a spec
publish is **refused**. It never was. A refusal is a bug report with a pointer at
the field, and it should be fixed in the spec, the SQL, or the publish arguments.
What falling back cost, recorded because it is the argument for the path being
closed rather than merely discouraged:

- You take over hand-maintaining the island and the manifest that the scheduler
  rewrites, which is where a wrong number becomes permanent.
- You lose the publish-time structural validation entirely.
- The whole dataset moves through your AI's context. On the spec path the server
  runs the query and seeds the rows, so **the data never enters the AI's context
  at all**. On the body path the numbers are baked into the bytes the AI sends,
  so they pass through it twice.

That last point is the practical binding limit. The advertised publish ceiling is
5 MiB, but a real session hit its context wall at around 40 KB of island and spent
seven rounds shrinking, dropping a whole dataset, three dimensions, half the time
window and most of a detail table to make it fit. If you find yourself deleting
real content to make a payload fit, that is the signal to go back to the spec.

## Editing a published dashboard

Edit the spec, never the served HTML. A refresh rewrites the island, so a
hand-edit to the served bytes is either lost or left inconsistent with the
manifest.

Read the stored spec back, change one thing, and republish to the same slug
carrying the hash you read. That hash is a lost-update guard: if the stored spec
changed since you read it, the publish is rejected and the live dashboard is left
untouched, rather than your edit silently overwriting someone else's.

Renaming is a separate operation, not a spec edit. Changing the spec's `slug`
field is refused, because the publish target is the path you publish to; a rename
goes through the rename tool, which leaves the old URL redirecting to the new one.

## Next

[Datasets and the four modes](/concepts/dataset-modes) is the decision inside the
spec that matters most.
