I tried creating a dashboard using only YAML for dbt models on Snowflake with dbt Charts

I tried creating a dashboard using only YAML for dbt models on Snowflake with dbt Charts

dbt Charts has been released as OSS. I verified through the CLI using jaffle_shop the mechanism that allows you to declare a dashboard in a single YAML file and reference Snowflake dbt models with SQL.
2026.09.20

This page has been translated by machine translation. View original

kawabata here.

On September 14, 2026, dbt Labs released dbt Charts as open source software (OSS). It works by declaring the board configuration in a single YAML file, with SQL also written within that YAML.

In this article, we use jaffle_shop on Snowflake as a subject to introduce the dbt Charts CLI (dct), and verify the process from board creation to display with serve.

https://dbtcharts.com/blog/charts-built-for-chat/

https://docs.dbtcharts.com/

What is dbt Charts

Declaring Boards with YAML

dbt Charts is a set of a language for declaring dashboards in YAML and a CLI dct that renders them. In dbt Charts, dashboards are called "boards," so this article will also use the term "board" going forward.

https://github.com/dbt-labs/dbt-charts

You describe "what to display" in SQL and declare "how to display it" in YAML. The main components of a board are the following four:

  • variables: UI for filtering (select boxes and date ranges)
  • queries: SQL. Jinja can be used, and dbt models can be referenced with {{ ref('orders') }}
  • charts: Maps query columns to channels (assignment destinations for visual elements) such as x / y / color
  • Layout: rows / cols / grid / tabs

dct 0.7.1 has subcommands such as validate, render (output to SVG, PNG, HTML, etc.), and serve (local server).

Relationship with dbt Projects

According to the official documentation, dbt Charts works without dbt. When co-located with a dbt project, it reads existing configuration files rather than launching dbt commands. The targets are three files: dbt_project.yml, profiles.yml, and target/manifest.json, which are used for connection information and ref() resolution.

How to use dbt Charts and the current configuration (organized from official documentation)

Configuration dbt project manifest.json ref() Connection info
Used standalone Not required Not required Cannot be used Define connection info or data source directly in dbt_charts.yml
Co-located with dbt project Present Required Can be used profiles.yml or direct connection settings
Current configuration jaffle_shop Generated with dbt Core 1.12 Can be used trial target in profiles.yml

Scope of This Article

dbt Labs released the OSS language alongside dbt Charts Cloud (dbtCharts.com, public beta), which includes hosting, permission management, and a chat UI. This article only covers the OSS dct CLI.

Verification of creating boards via chat by loading dbt Skills into Claude Code will be covered in the next article.

Prerequisites

Verification Environment

Item Value
OS Windows 11 (PowerShell)
Python environment management uv 0.11.6
dbt (for model builds) dbt Core 1.12.0 + dbt-snowflake 1.12.0
dbt Charts 0.7.1 (Python 3.12.11)
Snowflake trial account (Enterprise Edition, AWS Tokyo region)
Snowflake connection role ACCOUNTADMIN (for verification)
dbt project jaffle_shop
  • The official documentation requires Python 3.10 or higher
  • The dbt-charts[snowflake] extra is required to connect to Snowflake

We are connecting with ACCOUNTADMIN to simplify verification, but the official documentation recommends creating a role with only SELECT permissions for dbt Charts. In production environments, please create a dedicated role and user with only SELECT permissions on the target schema.

Preparation

Installing dct in an Isolated Environment

Install using uv tool install as recommended by the official documentation.

uv tool install --python 3.12 "dbt-charts[snowflake]"

2026-09-20_20h04_44

dct places a shim in ~/.local/bin, and the main body is isolated in uv's tool directory.

dct --version

2026-09-20_20h06_14

According to the official documentation, the Python adapter used by dbt Charts depends on dbt Core 1.x. Therefore, if dbt Charts is co-located in a Python environment that includes dbt v2 (Fusion), dependency resolution may replace dbt v2 with dbt Core 1.x.

https://docs.dbtcharts.com/guides/dbt-fusion/

Running jaffle_shop seed and build

The jaffle_shop seed and build are executed with dbt Core. Replace <repo> with the path to your repository. In this case, dbt Core is installed in a venv (CoCo\.venv), so we specify its dbt.exe.

$DBT = "<repo>\CoCo\.venv\Scripts\dbt.exe"
snow sql -c trial -q "create database if not exists JAFFLE_SHOP_DB"
& $DBT deps  --profile jaffle_shop --target trial --project-dir .
& $DBT seed  --profile jaffle_shop --target trial --project-dir . --full-refresh --vars "{load_source_data: true}"
& $DBT build --profile jaffle_shop --target trial --project-dir .

We confirmed that data is loaded into Snowflake as shown below.

2026-09-20_20h16_44

Creating a Template with dct init

Run dct init in the dbt project root. Since the goal this time is CLI verification, we suppressed the installation of skills, MCP, and editor extensions with options.

dct init --yes --no-skills --no-mcp --no-vscode --no-cursor --project-dir .

2026-09-20_20h18_22

The generated dbt_charts.yml was a template with all lines commented out. You define the connection destination yourself in sources:. Since jaffle_shop's dbt_project.yml still has profile: default, we specified the profile name explicitly.

# dbt_charts.yml
sources:
  jaffle:
    type: dbt_profile
    profile: jaffle_shop
    target: trial

server:
  port: 8943

execution:
  max_query_duration_seconds: 60

Since type: dbt_profile references connection information from profiles.yml, no authentication information is stored in dbt_charts.yml itself. Please exclude profiles.yml, which holds authentication information, from Git version control. The official documentation also describes how to use environment variables (env_var()) for connection settings.

Trying It Out

Displaying the guide Board

The charts/guide.yml created by dbt Charts' dct init is a sample board that holds data within the YAML. You can verify its behavior before connecting to Snowflake.

dct validate charts/guide.yml
dct render charts/guide.yml --format png --output renders\guide.png

2026-09-20_20h23_05

Specifying --format terminal renders the same board in the terminal.

dct render charts/guide.yml --format terminal

2026-09-20_20h23_58

Use dct serve to view it in a browser. The port follows server.port in dbt_charts.yml.

dct serve --host localhost

--host localhost restricts access to the local machine. The dct serve --help in dbt Charts has no options related to authentication. Specifying --host 0.0.0.0 allows access from other devices on the network, so do not expose this in environments handling sensitive data.

Opening http://localhost:8943/guide/ displays the board.

2026-09-20_20h25_30

Referencing Snowflake dbt Models with ref()

You can verify the Snowflake connection and SQL execution with dct query. The first argument is the source name from dbt_charts.yml.

dct query jaffle "select count(*) as orders from JAFFLE_SHOP_DB.MAIN.ORDERS"

2026-09-20_20h27_06

Defining KPIs and Graphs in YAML

Using jaffle_shop's marts models, we wrote a sales board in charts/jaffle_overview.yml.

Define in yaml
title: Jaffle Shop Sales Dashboard
notes: >
  A board that references jaffle_shop's marts models (orders / order_items / products / locations / customers)
  using ref(). Filter with two variables: location and period.
  Amounts are pre-tax (orders.subtotal / order_items.product_price) for consistency.

source: jaffle

variables:
  location:
    input: select
    label: Store
    options:
      query: location_options
      column: location_name
  period:
    input: daterange
    label: Period
    default: ["2024-09-01", "2025-08-31"]

queries:
  location_options: |
    select location_name
    from {{ ref('locations') }}
    order by location_name

  kpis: |
    select
        sum(o.subtotal)                          as revenue,
        count(*)                                 as orders,
        count(distinct o.customer_id)            as customers,
        sum(o.subtotal) / nullif(count(*), 0)    as avg_order_value
    from {{ ref('orders') }} as o
    join {{ ref('locations') }} as l
        on o.location_id = l.location_id
    where {{ filter('l.location_name', location) }}
      and {{ filter_date_range('o.ordered_at', period) }}

  monthly_revenue: |
    select
        date_trunc('month', o.ordered_at)::date as month,
        sum(o.subtotal)                         as revenue,
        count(*)                                as orders
    from {{ ref('orders') }} as o
    join {{ ref('locations') }} as l
        on o.location_id = l.location_id
    where {{ filter('l.location_name', location) }}
      and {{ filter_date_range('o.ordered_at', period) }}
    group by 1
    order by 1

  revenue_by_type: |
    select
        date_trunc('month', o.ordered_at)::date  as month,
        p.product_type                           as product_type,
        sum(oi.product_price)                    as revenue
    from {{ ref('order_items') }} as oi
    join {{ ref('products') }} as p
        on oi.product_id = p.product_id
    join {{ ref('orders') }} as o
        on oi.order_id = o.order_id
    join {{ ref('locations') }} as l
        on o.location_id = l.location_id
    where {{ filter('l.location_name', location) }}
      and {{ filter_date_range('o.ordered_at', period) }}
    group by 1, 2
    order by 1, 2

  customer_mix: |
    select
        customer_type,
        count(*) as customers
    from {{ ref('customers') }}
    where customer_type is not null
    group by 1
    order by 1

  top_products: |
    select
        p.product_name                              as product_name,
        p.product_type                              as product_type,
        count(*)                                    as items_sold,
        sum(oi.product_price)                       as revenue,
        sum(oi.product_price) - sum(oi.supply_cost) as gross_profit
    from {{ ref('order_items') }} as oi
    join {{ ref('products') }} as p
        on oi.product_id = p.product_id
    where {{ filter_date_range('oi.ordered_at', period) }}
    group by 1, 2
    order by revenue desc
    limit 10

charts:
  kpi_revenue:
    type: kpi
    query: queries.kpis
    label: Revenue (pre-tax)
    value: revenue
    style:
      value:
        format: currency_whole

  kpi_orders:
    type: kpi
    query: queries.kpis
    label: Orders
    value: orders
    style:
      value:
        format: integer

  kpi_customers:
    type: kpi
    query: queries.kpis
    label: Customers
    value: customers
    style:
      value:
        format: integer

  kpi_aov:
    type: kpi
    query: queries.kpis
    label: Average Order Value
    value: avg_order_value
    style:
      value:
        format: currency_full

  revenue_trend:
    type: line
    query: queries.monthly_revenue
    title: Monthly Revenue
    x: month
    y: revenue
    style:
      number_format: currency

  customer_mix:
    type: bar
    query: queries.customer_mix
    title: New / Repeat Customers
    x: customer_type
    y: customers

  revenue_by_type:
    type: bar
    query: queries.revenue_by_type
    title: Monthly Revenue by Product Type
    x: month
    y: revenue
    color: product_type
    style:
      orientation: vertical
      stack: zero
      number_format: currency

  top_products:
    type: table
    query: queries.top_products
    title: Top 10 Products by Revenue
    style:
      columns:
        product_name:
          label: Product
        product_type:
          label: Type
        items_sold:
          label: Units Sold
          format: integer
        revenue:
          label: Revenue
          format: currency_whole
        gross_profit:
          label: Gross Profit
          format: currency_whole

rows:
  - cols: [kpi_revenue, kpi_orders, kpi_customers, kpi_aov]
  - title: Revenue Trends
    grid:
      columns: 24
      items:
        - item: revenue_trend
          width: 16
        - item: customer_mix
          width: 8
  - revenue_by_type
  - top_products

Amounts are aligned to pre-tax. KPIs and monthly trends use orders.subtotal at the order level, while revenue by product type uses order_items.product_price at the line item level.
Since a single order can have multiple line items, only the product type aggregation references order_items.
jaffle_shop's orders.yml has two tests: order_total = subtotal + tax_paid and order_items_subtotal = subtotal. This ensures both totals match and allows comparison on the same basis as pre-tax sales.

The variable name is location to match the jaffle_shop model name locations, and the display label is "Store". locations is the model representing stores.

Syntax can be checked offline with dct docs cheatsheet or dct docs charts.

# Execute a query on Snowflake, render the result to PNG and save it
dct render charts/jaffle_overview.yml --format png --output renders\jaffle_overview.png

# Open the output PNG
start renders\jaffle_overview.png

2026-09-20_20h34_58

Verifying with Snowflake's QUERY_HISTORY

According to the official documentation, queries issued by dbt Charts have attributes such as app=dbt-charts attached. In Snowflake, three items — app, the dbt Charts version, and surface — are set in JSON format in QUERY_TAG. Additional information such as board names and query names is said to be appended as comments to the queries. Also, if query_tag is already set on the connection side, dbt Charts is said not to overwrite that value.

https://docs.dbtcharts.com/sources/

We verified by running the following in Snowsight.

-- List queries issued by dbt Charts in reverse chronological order
select start_time, query_type, query_tag, left(query_text, 100) as query_text_head
from table(JAFFLE_SHOP_DB.information_schema.query_history_by_user(result_limit => 500))
where query_tag like '%dbt-charts%'
order by start_time desc;

2026-09-20_20h56_33

Closing

With dbt Charts, we were able to declare and visualize a board in a single YAML file.
While dbt Semantic Layer is not yet supported at this time, it is planned in dbt-labs/dbt-charts#1, so we look forward to that!

I hope this article is helpful to someone!


dbtの導入支援はクラスメソッドにお任せください!

クラスメソッドでは dbt の導入を支援しております。
製品の詳細や支援の内容についてお気軽にお問い合わせください。

dbtの詳細を見る

Share this article