Skip to content

Author a dashboard with your AI

The real publishing loop, from introspecting your schema to publishing by spec hash, and what to check at each step so you catch problems early.

You do not write the dashboard. You describe the numbers you want, and your AI writes a spec: one small YAML document of datasets and tiles. The Dashies server compiles it into HTML, validates it, runs the SQL once to bake real numbers in, and gives you back a URL.

This page is what that loop looks like from your side, and what to check at each step. Your AI runs the tool calls; you read the reports.

The loop

1. Your AI reads the schema

It calls introspect_schema on the data source you are building against, which returns table and column names. No data rows are read.

Check: your AI should tell you which connection it is reading from, by name and engine, before this call. What a connection is bound to, and why that cannot change, is in Connections and scope. If it does not say, ask. The SQL runs server-side inside Dashies against your warehouse, using the credentials you stored in the web app, and that is worth knowing before it starts.

2. Your AI writes and validates the SQL

It writes one read-only SELECT per dataset and checks each with validate_cube_sql. The design rules it is working to are in Designing the cube. That proves the statement runs inside the same confined executor the scheduled refresh will use, under the same caps and timeout.

Check: validate_cube_sql proves the SQL runs. It does not prove the numbers are right. Do not treat a clean validation as a correctness result. See Verify your numbers.

3. Your AI dry-runs the spec

It calls publish_dashboard with dry_run: true. The server does the full compile, validation, and a read-only seed of every dataset, and writes nothing.

The report comes back with:

  • spec_hash, which names the exact document the server just checked,
  • mode_choices, saying what each dataset resolved to and why,
  • warnings, which are advisory,
  • obligations, which are not,
  • bytes, and errors if there are any.

Check: read mode_choices and obligations yourself. Both are covered below.

4. Your AI publishes the hash

With a clean dry run, it calls publish_dashboard again passing spec_hash rather than the document. The server compiles, validates, and seeds exactly the same document. Nothing is skipped; the only thing saved is re-sending the YAML.

The response carries the URL.

Check: open the URL. The page should load with real numbers already in it, not a loading state.

Why publishing by hash matters

A dry run stores the document server-side under its hash for about an hour, so the real publish names the hash instead of carrying a second copy. A large spec re-sent on every correction is the single most common waste on this path.

If the hour lapses, the publish is refused naming that, and your AI re-sends the document. That is a miss, never a wrong publish: the server re-checks the digest before using the stored entry.

What a spec looks like

You do not have to write this, but reading one makes the reports below make sense. This is a complete, valid spec:

dashies: 1
title: Dashboards published
slug: dashboards-published
source:
  connection: self
  schedule: daily
datasets:
  main:
    mode: cube
    sql: >
      select day, dashboards_published
      from dashies_usage_metrics
      order by day
    dimensions:
      day: { type: date }
    measures:
      dashboards_published: { agg: sum }
tiles:
  - type: kpi
    measure: dashboards_published
    title: Total published
  - type: chart
    chart: line
    x: day
    measure: dashboards_published
    title: Published per day

source.connection is required and has no default. self is Dashies' own built-in metrics view, which holds no personal data; anything else is a data source id. source.schedule is the coarse cadence, and you refine the timing afterwards.

That example runs as written, because self exposes exactly one relation, dashies_usage_metrics, at day grain: day plus eight additive counts (dashboards_published, public_count, private_count, active_count, paused_count, failed_count, archived_count, scheduled_count). It is the only thing self can read, so a query against any other table fails there.

Everything else, the data-dash markup, the data island, and the refresh manifest, is emitted by the compiler. Your AI never hand-writes any of it.

Reading the publish report

mode_choices

Each dataset resolves to one of four materializations, and the report says which and why, for example main -> lattice: a distinct count over low-cardinality dimensions.

If a mode surprises you, that usually means a measure is not what you thought it was. See Choose a dataset mode for the decision, and the four modes for what each one is.

obligations

An obligation is the format asking you to run a check no static analysis can run. It appears whenever a cube is built over more than one row source: a join, a CTE, a comma join, or a derived table.

Do the check. It is described in Verify your numbers.

An empty obligations list is not a clean bill of health

Empty obligations means only that the cube reads one row source, so it cannot fan out. It says nothing about whether your numbers are correct. A non-additive aggregate or a mis-declared measure on a single-source cube is still yours to catch.

warnings

Warnings do not block the publish. They are the server reporting what it saw in the real seeded values, which is the cheapest signal you will get. Several describe a tile that publishes cleanly and then refuses to draw.

One is worth reading closely every time: a rolled-up value that disagrees with its siblings. If two datasets compute the same measure the same way over the same column and their fully rolled-up totals differ, and a tile actually displays the differing one, the report says so and gives you the aggregate, column, scope, and value for each.

That is not automatically an error. Month-to-date beside year-to-date legitimately differ, and the scope field is how you tell. But it is exactly the shape of a real incident: a cohort lattice summing a point-in-time snapshot across 24 tenure months put $596,348,393 on a KPI card against a true $36,384,217.

Editing a published dashboard

Editing means editing the spec, never the served HTML. The next refresh rewrites the data island, so a hand edit to the served page would be lost or left inconsistent.

The loop is:

  1. get_dashboard_spec returns the stored spec verbatim, with its comments, formatting, and spec_hash intact.
  2. Your AI republishes to the same path with spec_edits, which are exact-string replacements, plus base_spec_hash set to the hash it just read.

base_spec_hash does two jobs: it names the document the edits apply to, and it is the lost-update guard. If the stored spec changed since it was read, the publish is rejected with spec_conflict and your live dashboard is left untouched.

Every edit re-seeds and re-validates, so an edit that would break a binding is a pointered error rather than a broken live dashboard.

Renaming is not a spec edit

To rename a dashboard, your AI uses update_dashboard with a new slug, which keeps the old URL working via a redirect. Changing slug in the spec is not a rename. See Share a dashboard.

If your dashboard predates specs, or was published as raw HTML, derive_dashboard_spec reconstructs a draft spec from it first. It is read-only and stores nothing.

If a publish is refused

Every refusal names the exact field or the exact gate, for example:

[semantic] /datasets/main/measures/revenue: measure `revenue` has no matching output column; the query outputs day, orders

Fix the thing it names.

A refused spec is a bug report, not a reason to hand-author

The most expensive wrong turn on this path is falling back to a hand-written HTML file after a refusal, and it is silent. You lose the compiler emitting and validating the richer tiles, you take over hand-maintaining the data island and the refresh manifest, and the whole dataset moves into your AI's context, where it will start deleting real content to make the payload fit.

If your AI starts dropping datasets, dimensions, or rows "to fit", stop it. That is the signal it left the spec path.

The most common refusal by far is a data source scope mismatch, and on a spec publish it does not stop the publish: the dashboard goes live and only the refresh manifest is refused, leaving a permanently static page. See Work as a team.

Check it worked

  1. Open the URL. Real numbers, no loading state.
  2. Ask for the refresh status. Your AI can call get_refresh_status for the slug, which reports whether it is refreshing, its schedule, and its next run. A dashboard that published fine but shows no schedule and no next run is not refreshable, which almost always means the manifest was refused.
  3. Cross-check one number. Not optional. See Verify your numbers.