Build

A dashboard is one YAML file that holds the connection, the filters, the queries, and the tiles. Here is a trimmed version of the demo that sqldash init --demo writes.

YAML
title: Order Analytics
description: Demo dashboard over a local CSV. Swap the source for your warehouse.

source: {type: duckdb, attach_files: true}

filters:
  - {name: dates, type: daterange, label: Date range, default: last_60_days}
  - name: region
    type: select
    label: Region
    options_sql: "SELECT DISTINCT region FROM orders ORDER BY region"

tiles:
  - title: Total revenue
    metric: revenue
    size: 6x2
    compare: previous_period

  - title: Revenue by category
    chart: bar
    format: currency
    size: 6x4
    sql: |
      SELECT category, ROUND(SUM(amount), 2) AS revenue
      FROM orders
      WHERE order_date BETWEEN {{ dates_start }} AND {{ dates_end }}
        {% if region %}AND region = {{ region }}{% endif %}
      GROUP BY 1
      ORDER BY 2 DESC

  - size: 12x2
    markdown: |
      **This whole dashboard is one YAML file.**

Dashboard files live in .sqldash/ at the root of a repo, which is what init creates. You can also serve a single file directly with sqldash serve report.yaml. Every dashboard file in .sqldash/ shows up in the dashboard picker.

The top of the file

key what it does
title Required. The name shown in the picker and at the top of the page.
description A line of context under the title.
source The connection, or several named connections. See Sources.
filters Controls at the top of the page that feed values into queries.
tiles Everything on the grid, in order.
queries Named SQL that several tiles can share.
metrics, relations Metrics defined inside this dashboard, so a single file stays portable.
layout Grid settings, such as row_height in pixels.
refresh Re-runs every tile on an interval, such as 30s, 5m, or 1h.
currency, locale Defaults for number formatting.
css A theme for this dashboard. See Themes.

Unknown keys are errors with a did-you-mean hint, so a typo never fails silently.

Tiles

A tile shows either data or text. A data tile gets its data from exactly one place, and the three ways look like this.

YAML
tiles:
  # 1. a governed metric from metrics.yaml
  - title: Revenue by month
    metric: {name: revenue, grain: month, dimensions: [region]}
    chart: {type: line, group_by: region}

  # 2. inline SQL
  - title: Largest orders
    chart: table
    sql: |
      SELECT order_date, region, amount
      FROM orders
      ORDER BY amount DESC
      LIMIT 20

  # 3. a named query shared with other tiles
  - title: Orders by region
    query: orders_by_region
    chart: bar

queries:
  orders_by_region: |
    SELECT region, COUNT(*) AS orders FROM orders GROUP BY 1

Use a metric whenever one exists. The tile then inherits the metric’s definition and format, respects its allowed dimensions, and stays in step with the CLI and agents. Use inline SQL for one-off views, and a named query when several tiles need the same result.

A text tile is just a markdown: key, useful for headings, notes, and links.

YAML
  - size: 12x1
    markdown: |
      ### Revenue
      Figures are in USD and exclude refunds. [Metric definitions](https://example.com)

Every tile key

key what it does
title The tile’s heading. Also used to derive its id.
id A stable name for the tile. Set it when a theme rule or an agent refers to the tile.
metric, sql, query Where the data comes from. Exactly one.
grain The time grain for a metric tile, such as day or month.
compare previous_period or yoy on a metric tile. See below.
chart How to draw the result. See below.
format How to format numbers. See below.
source Which named connection to run against, when the dashboard has several.
size, position Where the tile sits on the grid. See below.
markdown Text for a text tile.

Charts

A data tile draws its result with chart:. There are seven chart types. A tile without chart: shows its result as a table.

How sqldash picks the columns

You rarely need to say which column goes on which axis. For line, area, bar, and scatter charts, sqldash reads the result’s columns and fills in what you leave out.

If you name a column the result does not have, sqldash ignores that setting and falls back to these rules instead of drawing an empty chart.

Line

A line chart of daily revenue with one line per region

Use a line for a value over time. One number column gives one line. Add group_by to draw a line for each value of another column.

YAML
- title: Revenue by day
  chart: {type: line, group_by: region}
  sql: |
    SELECT order_date, region, ROUND(SUM(amount), 2) AS revenue
    FROM orders GROUP BY 1, 2 ORDER BY 1

With group_by, the query returns one row per date and group, and sqldash turns each group into its own series. The legend appears whenever there are two or more series.

Area

A stacked area chart of daily revenue by region

An area chart is a line chart with the space underneath filled in. Area series always stack, so the top edge shows the total and each band shows its share of it.

YAML
- title: Revenue by day, stacked by region
  chart: {type: area, group_by: region}
  sql: |
    SELECT order_date, region, ROUND(SUM(amount), 2) AS revenue
    FROM orders GROUP BY 1, 2 ORDER BY 1

Bar

A vertical bar chart of revenue by category

Use bars to compare categories. The order of the bars follows the order of the rows, so sort in SQL.

YAML
- title: Revenue by category
  chart: bar
  format: currency
  sql: SELECT category, ROUND(SUM(amount), 2) AS revenue FROM orders GROUP BY 1 ORDER BY 2 DESC
A horizontal bar chart of revenue by category, shaded by value

orientation: horizontal turns the bars sideways, which helps with long labels. color_by: value shades a single series from light to dark by size.

YAML
- title: Revenue by category
  chart: {type: bar, orientation: horizontal, color_by: value}
  format: currency
  sql: SELECT category, ROUND(SUM(amount), 2) AS revenue FROM orders GROUP BY 1 ORDER BY 2
A stacked bar chart of revenue by category split by region

group_by splits each bar into a series per group. Bars sit side by side unless you add stacked: true.

YAML
- title: Revenue by category and region
  chart: {type: bar, group_by: region, stacked: true}
  sql: SELECT category, region, ROUND(SUM(amount), 2) AS revenue FROM orders GROUP BY 1, 2

A bar chart with a date on the x axis draws one bar per date.

Scatter

A scatter plot of daily orders against daily revenue

A scatter plots one number against another, one dot per row. Name both axes, because two number columns are otherwise both treated as values. The x values are drawn in row order, so sort by the x column.

YAML
- title: Orders vs revenue per day
  chart: {type: scatter, x: orders, y: revenue}
  sql: |
    SELECT COUNT(*) AS orders, ROUND(SUM(amount), 2) AS revenue
    FROM orders GROUP BY order_date ORDER BY orders

Pie

A donut chart of revenue share by region

A pie shows each row’s share of the total, drawn as a donut. It needs one label column and one number column. Keep it to a handful of slices.

YAML
- title: Revenue share by region
  chart: pie
  format: currency
  sql: SELECT region, ROUND(SUM(amount), 2) AS revenue FROM orders GROUP BY 1

Set label and value to pick the columns yourself.

Big number

A big number tile showing total revenue

A big number shows a single value from the first row, with the column name underneath. It suits totals and headline metrics. On a metric tile, add compare: to show the change from the previous period.

YAML
- title: Total revenue
  chart: big_number
  format: currency
  sql: SELECT ROUND(SUM(amount), 2) AS revenue FROM orders

Set value to choose which column to show.

Table

A table of recent orders

A table shows every column and row. Click a column heading to sort. It is the default when a tile has no chart:.

YAML
- title: Recent orders
  sql: SELECT order_date, region, category, amount FROM orders ORDER BY order_date DESC LIMIT 50

Chart options

option charts what it does
type all line, area, bar, scatter, pie, big_number, or table.
x line, area, bar, scatter The column on the x axis.
y line, area, bar, scatter One column or a list of columns to plot as values.
group_by line, area, bar, scatter Splits one value column into a series per distinct value of this column.
stacked bar Stacks the series instead of placing them side by side. Areas always stack.
orientation bar horizontal draws the bars sideways.
color_by bar value shades a single series by size.
label, value pie The slice label column and the size column.
value big number The column to show.
legend line, area, bar, scatter, pie false hides the legend.
format all How to format values. See Formatting below.

chart: bar is shorthand for chart: {type: bar}. Use the long form when you want to set any other option.

Layout

Tiles flow left to right in file order on a 12-column grid, wrapping to the next row when a row fills up. size: sets the footprint as width by height, so 6x4 is half the width and four rows tall. A tile without a size is 6x4.

YAML
tiles:
  - {title: Revenue, metric: revenue, size: 3x2}
  - {title: Orders, metric: order_count, size: 3x2}
  - {title: Revenue by day, metric: {name: revenue, grain: day}, chart: area, size: 6x2}

When you drag or resize a tile in the browser, sqldash writes an exact position: {x, y, w, h} for it and leaves every other line alone, so the diff stays small. Tiles with a position stay exactly where they are, and tiles without one flow in file order below them. layout: {row_height: 84} changes the height of one grid row.

Edit in the browser

Every change you make in the browser is written back to the YAML file, so it shows up as an ordinary diff you can review.

+ Explore in the top bar is where new tiles come from. It opens the query workspace, where you write SQL or pick a metric, look at the result, chart it, and click Add to dashboard. See Explore.

Edit changes the dashboard in place. You can drag and resize tiles, add or remove filters, and delete tiles. The pencil on a tile opens it in the tile editor, where you change its SQL, text, or metric, its dimensions and grain, and its chart type, run it to preview, and click Save tile. For changes you would rather describe than click, use AI Studio.

Filters

Filters are the controls at the top of a dashboard. Each one has a name, a type, and an optional label and default. Queries read a filter’s value with {{ name }}.

YAML
filters:
  - {name: dates, type: daterange, label: Period, default: last_30_days}
  - {name: region, type: select, label: Region, options: [us, eu, apac]}
  - {name: channel, type: select, options_sql: "SELECT DISTINCT channel FROM orders"}
  - {name: min_amount, type: number, label: Minimum order, default: 0}
  - {name: customer, type: text, label: Customer email}
  - {name: as_of, type: date, label: As of, default: today}
type what it is
daterange A start and end date. A filter named dates gives queries {{ dates_start }} and {{ dates_end }}.
date A single date.
select A dropdown. Takes a fixed options list, or options_sql to load choices from a query, which also adds an all choice.
number A number input.
text A free text input. This is the type when you leave type out.

Filters also apply to metric tiles. A filter whose name matches one of the metric’s dimensions filters that dimension, and a daterange becomes the metric’s time range.

Using filter values in SQL

{{ name }} is always sent to the database as a query parameter, never pasted into the SQL text, so a filter value can never change the query itself.

Write the placeholder unquoted, as in region = {{ region }}. A quoted '{{ region }}' is refused by name, because a bound value cannot reach inside a string literal and sqldash will not paste text into one. For a LIKE pattern, build the pattern in SQL, as in name LIKE '%' || {{ search }} || '%'.

When a filter has no value, wrap the condition in {% if name %}…{% endif %} and it drops out of the query. A select sitting on all counts as no value.

SQL
SELECT category, SUM(amount) AS revenue
FROM orders
WHERE order_date BETWEEN {{ dates_start }} AND {{ dates_end }}
  {% if region %}AND region = {{ region }}{% endif %}
  {% if min_amount %}AND amount >= {{ min_amount }}{% endif %}
GROUP BY 1

A block can also have {% elif other %} branches and an {% else %} fallback. The first branch whose filter has a value wins, {% else %} runs when none do, and every branch binds its own values the same way.

SQL
{% if region %}SELECT * FROM orders WHERE region = {{ region }}
{% elif country %}SELECT * FROM orders WHERE country = {{ country }}
{% else %}SELECT * FROM orders{% endif %}

The condition is always a bare filter name. Comparisons, not, a block inside another block, loops, and every other Jinja tag are refused by name before the warehouse sees the query, on purpose, so the SQL stays readable and safe.

Dates

Anywhere a date goes, you can write an ISO date like 2026-08-01 or a relative token.

token means
today Today.
-30d, -4w, -6m, -1y That many days, weeks, months, or years back.
last_30_days The same as -30d.
mtd, ytd The start of this month or this year.

A token names a window, and it resolves to the edge of that window that fits where you use it. As a start, -30d means 30 days ago. As an end, it means today. To end a range in the past, use an ISO date.

Tokens resolve against the date on the machine running sqldash, never the viewer’s laptop, so a tile, the CLI, and MCP all run the same window for the same token.

Period comparison

Add compare: to a metric tile to show how the current window compares with an earlier one.

YAML
- title: Revenue
  metric: revenue
  compare: previous_period
- title: Revenue by day
  metric: {name: revenue, grain: day}
  chart: line
  compare: yoy

previous_period compares with the window of the same length just before, and yoy compares with the same window a year earlier. Big numbers show the change with an arrow, and time series draw the earlier window as a dashed line. The earlier window is the dashboard’s date range shifted back, so the dashboard needs a daterange filter. Without one, sqldash lint reports an error and the tile shows it instead of a number with no change. The same comparison works in sqldash metric query --compare and in the MCP query_metric tool, with no SQL to write.

Formatting

format: controls how numbers display. It takes number, currency, percent, compact, date, or any ISO 4217 currency code such as EUR.

YAML
currency: EUR
locale: de-DE

tiles:
  - {title: Revenue, metric: revenue, format: currency}
  - title: Orders by region
    sql: SELECT region, SUM(amount) AS revenue, COUNT(*) AS orders FROM orders GROUP BY 1
    format: {revenue: currency, orders: compact}

format: currency uses the dashboard’s currency:, which defaults to USD. A mapping formats each column separately. locale: sets separators and date style for everyone, overriding the viewer’s browser. A metric’s format: in metrics.yaml carries over to its tiles automatically.

Validate before you commit

Terminal
sqldash lint            # checks every dashboard, metric, and agent
sqldash lint --strict   # also probes the warehouse and fails on warnings, for CI

Lint catches unknown keys, missing metrics and dimensions, broken filter references, plaintext secrets, and missing drivers, so a broken dashboard fails the pull request instead of the page. Plain lint never opens a warehouse. --strict does, and runs every metrics.yaml metric and every agent SQL tool wrapped in WHERE 1 = 0, so an expression or column the warehouse cannot resolve fails CI with the warehouse’s own error and no rows come back.