Quick Guide¶
dbt Charts boards are YAML files that render as dashboards. The fastest way to your first board is to hand the tool to a coding agent. The rest of this page is the setup by hand, and the syntax you will read in whatever the agent writes.
Start with your agent¶
In the project you want charts for, paste this into Claude Code, Codex, or Cursor:
The agent installs dct, reads the intro skill (which teaches it the language
and which skill to read next), looks at your data, writes boards into charts/,
and renders them for you to look at. Ask for changes in plain language; every
edit lands in the same YAML file, which you can read, diff, and review.
This works in agents that have a shell and your files: Claude Code, Codex, and Cursor today. In claude.ai or ChatGPT there is nothing to install, so start on dbtCharts.com instead (see dbt Charts Cloud).
Already ran it? Skip to Syntax to read what your agent wrote.
Or by hand¶
Install dbt Charts, then scaffold a project with dct init:
dct init writes a dbt_charts.yml project anchor and a charts/ directory with a
starter dashboard (charts/guide.yml) that renders without a database. Preview it
right away with dct serve, then edit the starter or add your own board alongside
it. dct init can also set up editor and AI integrations.
If you are starting in an existing dbt project, run dct init in the project
root: dct serve discovers the root from dbt_project.yml, and query sources
come from your dbt profiles.yml.
See Getting Started for the full setup path.
Syntax¶
The board is the base element: a layout tree that can hold text, variables, queries, charts, and other boards. Start with board layout, then add the pieces that make a board interactive.
Boards¶
A board can be as small as a title and Markdown text:
title: "Simple board"
text: |
This is Markdown text.
- **bold** for important points
- *italics* for nuance
- `code` for field names
Boards become powerful because they nest. When you need structure, rows
stacks items vertically, cols places them side by side, and each item is
itself a board:
title: "This is the parent board"
width: 600
rows:
- text: |
With just **some text** on it. It's Markdown, so we can put things like
[links](https://example.com/), *emphasis*, and lists:
- keep prose close to the data
- call out important details
- add structure without adding a chart
style:
border:
width: 1
color: "#cbd5e1"
- title: "Nested board"
text: |
This is a nested board with its own border.
style:
border:
width: 1
color: "#cbd5e1"
- title: "Two nested boards"
cols:
- text: |
And here's another with just text, no title.
- **Bold text** works inside nested boards.
- *Italic text* does too.
- Bullets make short notes easy to scan.
style:
border:
width: 1
color: "#cbd5e1"
- title: "And another with just a title"
style:
border:
width: 1
color: "#cbd5e1"
When a board defines charts but no explicit layout, dbt Charts renders those charts
as implicit rows. Use rows, cols, tabs, or grid when the arrangement
matters.
See Boards for layout patterns.
Variables¶
Variables are the controls readers use to change a board. This one creates a segment selector with three static options:
variables:
segment:
input: select
options:
static:
- Enterprise
- SMB
- Mid-Market
text: |
# Segment: {{ segment or 'All segments' }}
Change the selector above. The text reads **{{ segment or 'All segments' }}**
from the current variable value.
The {{ segment }} expression can appear in text, titles, links, SQL, and
query filters. A variable does not filter data by itself; you wire it into a
query explicitly.
Variables are usually not static like this first example. In production, you often populate them from a query, a source column, or a dbt model column; they also support many UI element and styling options. Read more in Variables.
Queries¶
Queries define the data a board can use. A board can mix database-backed SQL queries with static values for targets, labels, or small reference tables:
queries: revenue: # sql to a database sql: | SELECT month, segment, revenue FROM mart_revenue ORDER BY month source: analytics targets: # static values type: values columns: [segment, target] values: - [Enterprise, 438000] - [SMB, 228000] - [Mid-Market, 285000]
Queries can use the same {{ segment }} variable from above. The filter
helper writes the SQL predicate for the selected value, or a no-op predicate
when no segment is selected:
queries: revenue: sql: | SELECT month, segment, revenue FROM mart_revenue WHERE {{ filter('segment', segment) }} ORDER BY month source: analytics
Each query has a name (revenue, targets) and returns named columns.
Queries can reference other queries, read previous query results, tune cache
behavior, and combine SQL with CSV, dbt model data, HTTP, and static values.
Read more in Queries.
Charts¶
Charts reference queries and map result columns to visual channels. Start with a query, then bind its columns to a chart:
queries:
monthly_revenue:
sql: |
SELECT date, SUM(revenue) AS revenue
FROM ecommerce_orders
GROUP BY date
ORDER BY date
source: examples_db
charts:
revenue_trend:
query: monthly_revenue
type: line
title: "Revenue trend"
x: date
y: revenue
The chart does not aggregate or infer meaning. The query owns the rows and
columns; the chart owns the visual encoding. Here, the SQL groups orders by
date, and the line chart maps date to the x-axis and revenue to the y-axis.
Variables can then feed the same query layer. This example populates a category selector from SQL and uses the selected value in the revenue query:
variables:
category:
input: select
options:
query: category_options
queries:
category_options:
sql: |
SELECT DISTINCT category AS value
FROM ecommerce_orders
ORDER BY category
source: examples_db
revenue:
sql: |
SELECT category, SUM(revenue) AS revenue
FROM ecommerce_orders
WHERE {{ filter('category', category) }}
GROUP BY category
ORDER BY revenue DESC
source: examples_db
charts:
revenue_by_category:
query: revenue
type: bar
title: "Revenue by category"
x: category
y: revenue
Here, the variable changes the SQL predicate, while the chart still only maps the category and revenue columns to the axes.
Charts come in many families, and extensible charts can render custom SVG or HTML when the built-in families are not enough. Dive into Charts.
Styling¶
dbt Charts keeps styling declarative too. Choose a theme for the whole board,
then use typed chart options for labels, axes, formats, colors, marks, and
scale behavior. Built-in themes are clarity (the default), paper,
vivid, neon, and stark (see Themes); chart-local style only
needs to override what should differ from the theme:
title: "Styled revenue trend"
theme: paper
width: 600
queries:
daily_revenue:
sql: |
SELECT date, SUM(revenue) AS revenue
FROM ecommerce_orders
GROUP BY date
ORDER BY date
source: examples_db
charts:
revenue_trend:
query: daily_revenue
type: line
x: date
y: revenue
x_label: "Order date"
y_label: "Daily revenue"
style:
number_format: currency_whole
marks:
line:
curve: monotone
Use styles when the default rendering needs more polish, but keep data meaning in the query and visual mapping in the chart definition. See Styling and the chart family pages for the full option set.
Bringing It All Together Cheat Sheet¶
Start with the same pattern in a small board: one variable, one query, two KPIs above one single-value chart.
title: "Category snapshot"
width: 600
variables:
category:
label: "Category"
input: select
options:
query: category_options
queries:
category_options:
sql: |
SELECT DISTINCT category AS value
FROM ecommerce_orders
ORDER BY category
source: examples_db
summary:
sql: |
SELECT
SUM(revenue) AS revenue,
COUNT(*) AS orders,
AVG(revenue) AS avg_order
FROM ecommerce_orders
WHERE {{ filter('category', category) }}
source: examples_db
charts:
revenue:
query: summary
type: kpi
label: "Revenue"
value: revenue
style:
value:
format: currency_whole
orders:
query: summary
type: kpi
label: "Orders"
value: orders
avg_order:
query: summary
type: kpi
label: "Avg order"
value: avg_order
style:
value:
format: currency_whole
rows:
- cols:
- revenue
- orders
- avg_order
dbt Charts is flexible enough that you could use it to write the quick guide
itself. The fuller cheat sheet below uses the same pieces, then nests the
examples so the board reads like a compact version of this page. The parent
board sets the default clarity theme explicitly, and most of the embedded
example boards add their own border, so they read as cards nested inside the
guide. Notice the chart definitions live at the top, then nested boards list
chart IDs like revenue_by_month and target_by_month where those charts
should appear.
title: "dbt charts quick start cheat sheet"
theme: clarity
variables:
segment:
input: select
options:
static: [Enterprise, SMB, Mid-Market]
queries:
revenue:
type: values
columns: [month, segment, revenue, target]
values:
- [Jan, Enterprise, 142000, 130000]
- [Feb, Enterprise, 151000, 138000]
- [Mar, Enterprise, 164000, 146000]
- [Jan, SMB, 68000, 72000]
- [Feb, SMB, 74000, 76000]
- [Mar, SMB, 81000, 80000]
- [Jan, Mid-Market, 97000, 91000]
- [Feb, Mid-Market, 101000, 95000]
- [Mar, Mid-Market, 109000, 99000]
charts:
revenue_by_month:
query: revenue
type: bar
title: "Revenue by month"
x: month
y: revenue
color: segment
style:
number_format: currency_whole
target_by_month:
query: revenue
type: line
title: "Target by month"
x: month
y: target
color: segment
style:
number_format: currency_whole
rows:
- title: "1. Boards"
cols:
- text: |
A board starts with a `title`, then stacks `rows` or places `cols`
beside each other. Text cells support **markdown**, including links,
lists, emphasis, and inline `code`.
```yaml
title: "Basic board"
rows:
- text: |
This is markdown text.
- **bold** for important points
- *italics* for nuance
- `code` for field names
- cols:
- title: "Left panel"
text: "Boards can sit side by side."
- title: "Right panel"
text: "Each item is itself a board."
```
- title: "Basic board"
rows:
- text: |
This is markdown text.
- **bold** for important points
- *italics* for nuance
- `code` for field names
- cols:
- title: "Left panel"
text: "Boards can sit side by side."
- title: "Right panel"
text: "Each item is itself a board."
- title: "2. Variables"
cols:
- text: |
Variables define controls. This board has a `segment` selector, and
both the text and charts read the current value. The board on the
right is an actual nested board with its own variable definition.
```yaml
variables:
example_segment:
label: "Segment"
input: select
options:
static: [Enterprise, SMB, Mid-Market]
rows:
- text: |
Current segment:
**{{ example_segment or 'All segments' }}**
```
- style:
border:
width: 1
radius: 8
title: "Rendered variable"
variables:
example_segment:
label: "Segment"
input: select
options:
static: [Enterprise, SMB, Mid-Market]
text: |
Current segment: **{{ example_segment or 'All segments' }}**.
Change this nested board's selector and the text updates with it.
- title: "3. Queries"
cols:
- text: |
Queries own the rows and columns. They can come from SQL, CSV, dbt
models, HTTP data, or inline values.
```yaml
queries:
revenue:
type: values
columns: [month, segment, revenue, target]
values:
- [Jan, Enterprise, 142000, 130000]
- [Feb, Enterprise, 151000, 138000]
- [Mar, Enterprise, 164000, 146000]
```
- style:
border:
width: 1
radius: 8
title: "Query result"
text: |
The `revenue` query in this cheat sheet returns monthly revenue,
segment, and target columns. The chart definitions below decide how to
encode those columns visually.
- title: "4. Charts"
rows:
- text: |
Charts reference a query and bind result columns to visual channels.
The chart definitions are declared once above, then this nested board
lists the chart IDs in `cols` to place them side by side.
```yaml
charts:
revenue_by_month:
query: revenue
type: bar
title: "Revenue by month"
x: month
y: revenue
color: segment
```
- style:
border:
width: 1
radius: 8
title: "Rendered charts"
cols:
- revenue_by_month
- target_by_month
- title: "5. Nested boards"
cols:
- text: |
Nested boards are how larger dashboards stay organized. This nested
board has its own variable, query, charts, and layout. Inside it,
chart IDs are placed where they belong.
```yaml
title: "Segment review"
variables:
review_segment:
label: "Segment"
input: select
options:
static: [Enterprise, SMB, Mid-Market]
queries:
review_revenue:
type: values
columns: [month, segment, revenue, target]
values:
- [Jan, Enterprise, 142000, 130000]
- [Feb, Enterprise, 151000, 138000]
- [Mar, Enterprise, 164000, 146000]
charts:
review_revenue_by_month:
query: review_revenue
type: bar
x: month
y: revenue
cols:
- text: |
Current segment:
**{{ review_segment or 'All segments' }}**
- review_revenue_by_month
```
- style:
border:
width: 1
radius: 8
title: "Segment review"
variables:
review_segment:
label: "Segment"
input: select
options:
static: [Enterprise, SMB, Mid-Market]
queries:
review_revenue:
type: values
columns: [month, segment, revenue, target]
values:
- [Jan, Enterprise, 142000, 130000]
- [Feb, Enterprise, 151000, 138000]
- [Mar, Enterprise, 164000, 146000]
- [Jan, SMB, 68000, 72000]
- [Feb, SMB, 74000, 76000]
- [Mar, SMB, 81000, 80000]
- [Jan, Mid-Market, 97000, 91000]
- [Feb, Mid-Market, 101000, 95000]
- [Mar, Mid-Market, 109000, 99000]
charts:
review_revenue_by_month:
query: review_revenue
type: bar
title: "Revenue"
x: month
y: revenue
color: segment
style:
number_format: currency_whole
review_target_by_month:
query: review_revenue
type: line
title: "Target"
x: month
y: target
color: segment
style:
number_format: currency_whole
cols:
- text: |
Current segment: **{{ review_segment or 'All segments' }}**
- compare revenue against target
- keep the explanation beside the visuals
- rows:
- review_revenue_by_month
- review_target_by_month
Small boards compose into larger reports, and those reports can compose again. When a nested board gets large, move it to its own board YAML file and import it from the layout.
Quick Reference¶
title: "My Board" variables: segment: input: select options: static: [Enterprise, SMB, Mid-Market] queries: revenue: type: values columns: [month, segment, revenue] values: - [Jan, Enterprise, 142000] charts: revenue_by_month: query: revenue type: bar x: month y: revenue rows: - title: "Revenue" text: "Monthly revenue by selected segment." - revenue_by_month
Start with the data and interaction you need, then add the board shape that makes it readable.
Tools¶
Use dct render when you want a static artifact from one board file:
dct render charts/revenue.yml
dct render charts/revenue.yml --format html
dct render charts/revenue.yml --format png --output revenue.png
Use --var to render a specific variable state:
See dct render for every output format.
Use dct serve when you want live browser previews while editing YAML:
Board paths map to URLs. For example, charts/revenue.yml renders at
/revenue/, and query parameters set variables:
See dct serve for routing, ports, and database options.