# Building an AI-First Dashboard with Snowflake Dashboards 2.0

> Source: <https://dev.to/sagara/building-an-ai-first-dashboard-with-snowflake-dashboards-20-1pii>
> Published: 2026-11-13 08:11:10+00:00

**Note:** This is the English translation of the following article.

[https://dev.classmethod.jp/articles/snowflake-try-dashboards-2/](https://dev.classmethod.jp/articles/snowflake-try-dashboards-2/)

Hi, I'm Sagara.

Snowflake has a new feature called **Dashboards 2.0**. It's an AI-first BI artifact. You build a dashboard as a `.dash` file in Snowsight Workspaces, share it with roles, and viewers open it in CoWork or Snowsight.

[https://docs.snowflake.com/en/user-guide/snowflake-dashboards](https://docs.snowflake.com/en/user-guide/snowflake-dashboards)

The official docs below cover the sharing details.

[https://docs.snowflake.com/user-guide/snowflake-artifacts-create-share](https://docs.snowflake.com/user-guide/snowflake-artifacts-create-share)

In this article, I'll define a Semantic View on the TPC-H sample data and then visualize it with Dashboards 2.0. I'll also briefly explain what's inside the generated `.dash` file and try granting privileges through roles.

**Note:** As of November 13, 2026, the official docs list this feature as "Preview Feature — Open." The specs may change before GA, so please keep that in mind.

Dashboards 2.0 is a SQL-based, multi-tile dashboard that you create in Snowsight Workspaces. Analysts build it by chatting with Cortex Code (CoCo) and then share it with roles. Viewers open it in CoWork or Snowsight and can explore by clicking on charts. In CoWork, viewers can also ask follow-up questions about a tile.

A dashboard is mainly made up of these components:

| Component | Description | 
|---|---|
| Dashboard | The main body, containing the layout, filters, and tiles (the `.dash` file) | 
| Tile | A single display unit with SQL and a visualization type (chart / table / markdown). Placed on a 12-column grid | 
| Tab | A group of tiles. Up to 20 per dashboard. Filters apply across all tabs | 
| Filter | Defined per dashboard. Connected to tile SQL with `{{ filter('name') }}` | 
| Hierarchy | A hierarchy definition used for drill-down | 
| Chart action | Clicking or dragging on a chart filters or highlights other tiles | 
| Artifact | A read-only copy of the `.dash` file. This is the unit you share with roles | 

Tile queries run with **the viewer's own privileges (caller's rights)**, and no data is extracted.

Even though queries run with the viewer's privileges, a tile that references a Semantic View only needs the viewer to have SELECT on that Semantic View. **You don't need to grant privileges on the underlying tables.** The privileges required to query a Semantic View are listed on this page of the official docs:

[https://docs.snowflake.com/en/user-guide/views-semantic/querying](https://docs.snowflake.com/en/user-guide/views-semantic/querying)

The official docs state clearly that Dashboards 2.0 is not a drop-in replacement for the legacy version. The main differences are:

| Item | Legacy | Dashboards 2.0 | 
|---|---|---|
| Follow-up questions | Not supported | Conversational in CoWork | 
| How to create | Worksheet UI | CoCo + manual | 
| Access control | UBAC | RBAC | 
| Filter scope | Account-wide | Per dashboard | 
| Query cache | Shared within a role | Per user (results reused per viewer) | 

Version 2.0 doesn't recognize the legacy `:filter_name` syntax. You have to replace it with `{{ filter('name') }}`. An upgrade tool can also do this conversion automatically.

Pei-san, a Snowflake Data Superhero, wrote a great article (in Japanese) titled "Why Snowflake Dashboards 2.0 Is the Best, and What BI Will Need Going Forward." It covers the feature's characteristics and where BI is heading. It was a big help when I wrote the overview section of this article.

Please check it out as well.

[https://zenn.dev/dataheroes/articles/snowflake-dashboards-2-and-future-of-bi](https://zenn.dev/dataheroes/articles/snowflake-dashboards-2-and-future-of-bi)

Here are the items from the "Limitations (public preview)" section of the official docs that seem relevant to this walkthrough:

`.dash` file in Workspaces. Users granted `Can edit` (the `WRITE` privilege) can publish changes to the artifact. Note that having `Open source file` will point to a path that no longer exists. Renaming or moving the file may make its link to the artifact outdated or break it. If you then create the artifact again, it may become a separate artifact with a different link.`{{ hierarchy_key(...) }}` must also use `{{ hierarchy_rows(...) }}`.` SHOW ARTIFACTS`, `DESCRIBE ARTIFACT`, etc.) is listed as "Coming soon" as of November 13, 2026.
For this walkthrough, I'll set up an author role and a viewer role.

```
USE ROLE ACCOUNTADMIN;

-- Author role and viewer role
CREATE ROLE IF NOT EXISTS DASH_AUTHOR;
CREATE ROLE IF NOT EXISTS DASH_VIEWER;

-- Database, schema, and warehouse for testing
CREATE DATABASE IF NOT EXISTS DASH_TEST_DB;
CREATE SCHEMA IF NOT EXISTS DASH_TEST_DB.SV;
CREATE WAREHOUSE IF NOT EXISTS DASH_TEST_WH WAREHOUSE_SIZE = XSMALL AUTO_SUSPEND = 60;

-- Create the sample database (run only if SNOWFLAKE_SAMPLE_DATA does not exist in your account)
CREATE DATABASE SNOWFLAKE_SAMPLE_DATA FROM SHARE SFC_SAMPLES.SAMPLE_DATA;
```

Next, grant the required privileges to the author role `DASH_AUTHOR`.

```
-- Privileges on the sample data (shared DB) - author only; not granted to the viewer
GRANT IMPORTED PRIVILEGES ON DATABASE SNOWFLAKE_SAMPLE_DATA TO ROLE DASH_AUTHOR;

-- Author role privileges (limited to what's needed)
GRANT USAGE ON DATABASE DASH_TEST_DB TO ROLE DASH_AUTHOR;
GRANT USAGE ON SCHEMA DASH_TEST_DB.SV TO ROLE DASH_AUTHOR;
GRANT CREATE SEMANTIC VIEW ON SCHEMA DASH_TEST_DB.SV TO ROLE DASH_AUTHOR;
GRANT USAGE ON WAREHOUSE DASH_TEST_WH TO ROLE DASH_AUTHOR;

-- Privilege to share artifacts (don't rely on the default grant to PUBLIC)
GRANT MANAGE ACCESS ON DASHBOARD ARTIFACTS
  ON ACCOUNT TO ROLE DASH_AUTHOR;

-- Grant the author role to your own user
GRANT ROLE DASH_AUTHOR TO USER <your_user>;
```

I'll define a Semantic View using the TPC-H `ORDERS`, `CUSTOMER`, and `LINEITEM` tables.

```
USE ROLE DASH_AUTHOR;
USE WAREHOUSE DASH_TEST_WH;
USE SCHEMA DASH_TEST_DB.SV;

CREATE OR REPLACE SEMANTIC VIEW TPCH_SALES_SV
  TABLES (
    customers AS SNOWFLAKE_SAMPLE_DATA.TPCH_SF1.CUSTOMER
      PRIMARY KEY (c_custkey)
      COMMENT = 'Customers',
    orders AS SNOWFLAKE_SAMPLE_DATA.TPCH_SF1.ORDERS
      PRIMARY KEY (o_orderkey)
      COMMENT = 'Orders',
    line_items AS SNOWFLAKE_SAMPLE_DATA.TPCH_SF1.LINEITEM
      PRIMARY KEY (l_orderkey, l_linenumber)
      COMMENT = 'Order line items'
  )
  RELATIONSHIPS (
    orders_to_customers AS orders (o_custkey) REFERENCES customers,
    line_items_to_orders AS line_items (l_orderkey) REFERENCES orders
  )
  FACTS (
    line_items.net_price AS l_extendedprice * (1 - l_discount)
  )
  DIMENSIONS (
    customers.customer_name AS c_name,
    customers.market_segment AS c_mktsegment,
    orders.order_date AS o_orderdate,
    orders.order_year AS YEAR(o_orderdate),
    orders.order_priority AS o_orderpriority
  )
  METRICS (
    orders.order_count AS COUNT(o_orderkey),
    line_items.total_net_sales AS SUM(line_items.net_price)
  )
  COMMENT = 'Semantic View for TPC-H sales analysis';
```

I based the DDL syntax on this page of the official docs:

[https://docs.snowflake.com/en/sql-reference/sql/create-semantic-view](https://docs.snowflake.com/en/sql-reference/sql/create-semantic-view)

To confirm the Semantic View was created, query it directly (run these as the `DASH_AUTHOR` role).

```
-- Total sales by year
SELECT *
FROM SEMANTIC_VIEW(
  DASH_TEST_DB.SV.TPCH_SALES_SV
  DIMENSIONS orders.order_year
  METRICS line_items.total_net_sales
)
ORDER BY order_year;
-- Order count by order priority
SELECT *
FROM SEMANTIC_VIEW(
  DASH_TEST_DB.SV.TPCH_SALES_SV
  DIMENSIONS orders.order_priority
  METRICS orders.order_count
)
ORDER BY order_priority;
```

If you get one row per year and one row per priority, you're good. I based the `SEMANTIC_VIEW` clause syntax on this page of the official docs:

Note that the sales metric `total_net_sales` is the sum of each line item's extended price (`l_extendedprice`) minus its discount. It won't match amounts that include tax or the sum of `ORDERS.O_TOTALPRICE`.

Open `Workspaces` in Snowsight and switch the role to `DASH_AUTHOR` and the warehouse to `DASH_TEST_WH` in the Workspace.

Then select `Add new` → `Dashboard`.

The CoCo chat panel opens on the right side of the Workspace. In the prompt, I specify which Semantic View to use. If you don't specify one, CoCo searches for a relevant Semantic View, but this time I named it explicitly. I also told CoCo not to query the underlying tables directly and not to redefine the metrics on its own. This keeps the dashboard consistent with the setup, where the viewer has no privileges on the underlying tables.

```
/dashboard Using DASH_TEST_DB.SV.TPCH_SALES_SV, please create a TPC-H sales dashboard.
- Total sales trend by year (line chart)
- Total sales by market segment (bar chart)
- Order count by order priority (table)
- Filter: order year

All tile SQL, and any SQL used to fetch the options for the order year filter, must reference DASH_TEST_DB.SV.TPCH_SALES_SV.
Do not reference the underlying tables in SNOWFLAKE_SAMPLE_DATA directly.
Use line_items.total_net_sales for sales and orders.order_count for order count, and do not redefine the metrics on your own.
```

(The prompt in the screenshot is the original Japanese version.)

CoCo shows this as in the screenshot below and starts by drafting an implementation plan. For some reason it wrote the plan in English, so I asked it to reply in Japanese.

The implementation plan then appeared in Japanese. It looked fine, so I asked CoCo to build it.

After a short wait, a `.dash` file was created in Workspaces and opened, with charts showing in the tiles.

Next, I asked CoCo to add a chart action. Chart actions can't be added from the tile editor; they can only be added through CoCo.

I sent CoCo the following request:

```
When a bar in the sales by market segment chart is clicked,
add a chart action that filters the sales by year and
the order count by order priority to the selected market segment.
```

As a result, a chart action was added: clicking a bar in "Total Sales by Market Segment" now updates the other tiles, as shown below.

By the way, here's what the definition of the added filter looked like.

(You can also add filters manually through the GUI. However, the GUI didn't let me pick a Semantic View dimension, so the filter uses a SELECT statement that extracts a single dimension from the Semantic View.)

Let's open the generated `.dash` file and look at its contents.

You can view it by clicking `View JSON` in the top-right corner of the dashboard.

The file is in JSON format. The file generated this time is made up of the following main blocks. (The full file is included in a collapsible section at the end of this step.)

| Key | Contents | 
|---|---|
| `version` /`spec_version` /`spec_revision` | Format version information ( `2` /`2` /`17` in this case) | 
| `title` | Dashboard title | 
| `filters` | Filter definitions ( `order_year` ,`market_segment` ) | 
| `datasources` | SQL definitions used by tiles and filter options | 
| `tabs` | Tabs and the tiles (charts and tables) inside them | 
| `layout` | Position and size of each tile on the 12-column grid | 
| `scopes` | Definitions of how far chart actions reach | 

`filters`)
Order year is defined as `single_select`, and market segment as `multi_select`. The options use `"source": "query"` and reference a datasource that fetches them (such as `ds-order_year-options`).

The official docs write filter modes with hyphens, like `multi-select`, but the actual file uses underscores, like `multi_select`.

`datasources`)
Each datasource is `"type": "sql"`, with the SQL embedded as a string. For example, the SQL for sales by year looked like this:

```
SELECT
    order_year,
    AGG(total_net_sales) AS TOTAL_NET_SALES
FROM DASH_TEST_DB.SV.TPCH_SALES_SV
WHERE
    {{ filter('order_year') }}
    AND {{ filter('market_segment') }}
GROUP BY
    order_year
ORDER BY
    order_year
```

Key points:

`FROM` and aggregates metrics with `AGG()`.` TPCH_SALES_SV` and none touched the underlying tables in `SNOWFLAKE_SAMPLE_DATA` directly. Sales used `total_net_sales`, order count used `order_count`, and neither metric was redefined. So the instructions from Step 1 were followed.`{{ filter('order_year') }}` in the SQL and the `filter_id` in `filter_bindings`.` ds-sales-by-segment`) is bound only to `order_year`; the `market_segment` filter isn't connected. My guess is that this is because this tile is the source of the chart action.`ds-order_year-options`) use SQL that extracts a single dimension from the Semantic View with `GROUP BY`, as mentioned in Step 2.` tabs`)
There are two tile types, `chart` and `table`. A `chart` tile's `spec` uses `mark` (`line` / `bar`) and `encoding` (`field`, `type`, and `title` for `x` / `y`). The official docs say chart tiles are rendered with Vega-Lite, and this format is indeed close to Vega-Lite syntax.

The chart action added in Step 2 is defined as `interactions` on the sales by market segment tile.

```
"interactions": [
  {
    "trigger": "select",
    "operator": "set_filter",
    "fields": ["MARKET_SEGMENT"],
    "targets": [
      {
        "scope": "segment_action",
        "fields": ["MARKET_SEGMENT"],
        "filter": "market_segment"
      }
    ]
  }
]
```

When a bar is selected (`select`), this sets the `market_segment` filter (`set_filter`). The target tiles (sales by year and order count by order priority) have `"scope": "segment_action"`, and `segment_action` is defined in `scopes` at the end of the file.

`layout`)
`layout` specifies `x` / `y` / `w` / `h` for each tile, per tab. Sales by year has a width of 12 (full width). Sales by market segment and order count by order priority have a width of 6 each and sit side by side in the bottom row. This matches the "12-column grid" described in the official docs.

```
{
  "version": 2,
  "title": "TPC-H 売上ダッシュボード",
  "filters": {
    "order_year": {
      "label": "注文年",
      "mode": "single_select",
      "valueType": "STRING",
      "options": {
        "source": "query",
        "datasources": [
          "ds-order_year-options"
        ]
      },
      "default_values": []
    },
    "market_segment": {
      "label": "市場セグメント",
      "mode": "multi_select",
      "valueType": "STRING",
      "options": {
        "source": "query",
        "datasources": [
          "ds-market_segment-options"
        ]
      },
      "default_values": []
    }
  },
  "datasources": {
    "ds-order_year-options": {
      "type": "sql",
      "sql": "SELECT\n    order_year\nFROM DASH_TEST_DB.SV.TPCH_SALES_SV\nGROUP BY\n    order_year\nORDER BY\n    order_year"
    },
    "ds-sales-by-year": {
      "type": "sql",
      "columns": [
        {
          "name": "ORDER_YEAR",
          "type": "NUMBER"
        },
        {
          "name": "TOTAL_NET_SALES",
          "type": "NUMBER"
        }
      ],
      "sql": "SELECT\n    order_year,\n    AGG(total_net_sales) AS TOTAL_NET_SALES\nFROM DASH_TEST_DB.SV.TPCH_SALES_SV\nWHERE\n    {{ filter('order_year') }}\n    AND {{ filter('market_segment') }}\nGROUP BY\n    order_year\nORDER BY\n    order_year",
      "filter_bindings": [
        {
          "filter_id": "order_year"
        },
        {
          "filter_id": "market_segment"
        }
      ]
    },
    "ds-sales-by-segment": {
      "type": "sql",
      "columns": [
        {
          "name": "MARKET_SEGMENT",
          "type": "VARCHAR"
        },
        {
          "name": "TOTAL_NET_SALES",
          "type": "NUMBER"
        }
      ],
      "sql": "SELECT\n    market_segment,\n    AGG(total_net_sales) AS TOTAL_NET_SALES\nFROM DASH_TEST_DB.SV.TPCH_SALES_SV\nWHERE\n    {{ filter('order_year') }}\nGROUP BY\n    market_segment\nORDER BY\n    TOTAL_NET_SALES DESC",
      "filter_bindings": [
        {
          "filter_id": "order_year"
        }
      ]
    },
    "ds-orders-by-priority": {
      "type": "sql",
      "columns": [
        {
          "name": "ORDER_PRIORITY",
          "type": "VARCHAR"
        },
        {
          "name": "ORDER_COUNT",
          "type": "NUMBER"
        }
      ],
      "sql": "SELECT\n    order_priority,\n    AGG(order_count) AS ORDER_COUNT\nFROM DASH_TEST_DB.SV.TPCH_SALES_SV\nWHERE\n    {{ filter('order_year') }}\n    AND {{ filter('market_segment') }}\nGROUP BY\n    order_priority\nORDER BY\n    order_priority",
      "filter_bindings": [
        {
          "filter_id": "order_year"
        },
        {
          "filter_id": "market_segment"
        }
      ]
    },
    "ds-market_segment-options": {
      "type": "sql",
      "sql": "SELECT\n    market_segment\nFROM DASH_TEST_DB.SV.TPCH_SALES_SV\nGROUP BY\n    market_segment\nORDER BY\n    market_segment"
    }
  },
  "tabs": [
    {
      "tab_id": "tab-0",
      "title": "概要",
      "tiles": [
        {
          "tile_id": "sales-by-year",
          "type": "chart",
          "title": "年別 売上合計の推移",
          "datasources": [
            "ds-sales-by-year"
          ],
          "spec": {
            "mark": "line",
            "encoding": {
              "x": {
                "field": "ORDER_YEAR",
                "type": "ordinal",
                "title": "注文年"
              },
              "y": {
                "field": "TOTAL_NET_SALES",
                "type": "quantitative",
                "title": "売上合計"
              }
            }
          },
          "scope": "segment_action"
        },
        {
          "tile_id": "sales-by-segment",
          "type": "chart",
          "title": "市場セグメント別 売上合計",
          "datasources": [
            "ds-sales-by-segment"
          ],
          "spec": {
            "mark": "bar",
            "encoding": {
              "x": {
                "field": "MARKET_SEGMENT",
                "type": "nominal",
                "title": "市場セグメント",
                "sort": "-y"
              },
              "y": {
                "field": "TOTAL_NET_SALES",
                "type": "quantitative",
                "title": "売上合計"
              }
            }
          },
          "interactions": [
            {
              "trigger": "select",
              "operator": "set_filter",
              "fields": [
                "MARKET_SEGMENT"
              ],
              "targets": [
                {
                  "scope": "segment_action",
                  "fields": [
                    "MARKET_SEGMENT"
                  ],
                  "filter": "market_segment"
                }
              ]
            }
          ]
        },
        {
          "tile_id": "orders-by-priority",
          "type": "table",
          "title": "注文優先度別 注文件数",
          "datasources": [
            "ds-orders-by-priority"
          ],
          "spec": {
            "columns": [
              {
                "field": "ORDER_PRIORITY"
              },
              {
                "field": "ORDER_COUNT"
              }
            ]
          },
          "scope": "segment_action"
        }
      ]
    }
  ],
  "layout": {
    "tab-0": {
      "meta": {
        "default": true
      },
      "positions": {
        "sales-by-year": {
          "x": 0,
          "y": 0,
          "w": 12,
          "h": 4
        },
        "sales-by-segment": {
          "x": 0,
          "y": 4,
          "w": 6,
          "h": 4
        },
        "orders-by-priority": {
          "x": 6,
          "y": 4,
          "w": 6,
          "h": 4
        }
      }
    }
  },
  "spec_version": 2,
  "spec_revision": 17,
  "scopes": {
    "segment_action": {}
  }
}
```

To show the dashboard to other users, you share it as an artifact.

First, as an administrator, set up the privileges for the viewer role. Viewer queries run with the viewer's own privileges, but when they reference a Semantic View, only SELECT on the Semantic View is needed. We don't grant any privileges on the underlying tables (`SNOWFLAKE_SAMPLE_DATA`).

```
USE ROLE ACCOUNTADMIN;

-- Privileges required for the viewer role (no privileges on the underlying tables)
GRANT USAGE ON WAREHOUSE DASH_TEST_WH TO ROLE DASH_VIEWER;
GRANT USAGE ON DATABASE DASH_TEST_DB TO ROLE DASH_VIEWER;
GRANT USAGE ON SCHEMA DASH_TEST_DB.SV TO ROLE DASH_VIEWER;
GRANT SELECT ON SEMANTIC VIEW DASH_TEST_DB.SV.TPCH_SALES_SV TO ROLE DASH_VIEWER;
```

Next, in Snowsight, with the role set to `DASH_AUTHOR` and the warehouse set to `DASH_TEST_WH`, open the `.dash` file in Workspaces and select `Publish artifact`.

In the dialog that opens, do the following:

`Who has access`, add the `DASH_VIEWER` role.`Can view`.` Publish`.
When you click `Publish`, the artifact link is copied to your clipboard.

You can find published dashboards under `Workspaces` → `Artifacts` in Snowsight.

**How the artifact relates to the original `.dash` file, and how updates work**

A shared artifact is stored separately from the original `.dash` file.

[https://docs.snowflake.com/en/user-guide/snowflake-artifacts-create-share](https://docs.snowflake.com/en/user-guide/snowflake-artifacts-create-share)

An artifact is a securable object identified by a UUID. The UUID appears after `/artifacts/` in the artifact's URL.

You can grant three types of privileges:

| Privilege | Description | 
|---|---|
| `OWNERSHIP` | Delete and transfer ownership | 
| `READ` | View the contents | 
| `WRITE` | View and modify (including publishing changes to the artifact) | 

If you already granted `Can view` to `DASH_VIEWER` through the UI earlier, the SQL below isn't a required step. I'm including it as **an example of how to grant privileges with SQL**, run by an administrator (`ACCOUNTADMIN`).

```
USE ROLE ACCOUNTADMIN;

-- Example: grant READ to the viewer role
GRANT READ
  ON ARTIFACT "<artifact UUID>"
  TO ROLE DASH_VIEWER;
```

SQL for checking artifacts, such as `SHOW ARTIFACTS` and `DESCRIBE ARTIFACT`, is listed as "Coming soon" as of November 13, 2026. So this article doesn't check privileges with SQL.

The user who created the dashboard owns the artifact, so I checked viewer access with a different user. As an administrator, grant the viewer role to an existing test user.

```
USE ROLE ACCOUNTADMIN;
GRANT ROLE DASH_VIEWER TO USER <viewer_user>;
```

This assumes the test user doesn't have `DASH_AUTHOR` or any administrator role.

I logged in to Snowsight as this user and opened `Workspaces` → `Artifacts`. The dashboard was viewable, as shown below.

I tried Snowflake Dashboards 2.0 together with a Semantic View and confirmed that sharing through roles works.

It's an AI-first, code-based dashboard, and I found it very easy to use. Give it a try!
