Quickstart

Build and run your first complete app with SBT: three tiny tables, two models, one app. Every step verified on SBT 1.0.10.

This guide walks you through building a small but complete app with Seek Build Tools (SBT): three source tables, a SQL model that joins them, a Python model on top, bundled into an app you can run. Each step links to a deeper guide.

Before you start: install SBT on your machine (Setting Up SBT). That's one-time setup; everything here happens inside a project.

1. Create the project

Decide where your projects will live and go there:

mkdir -p ~/sbt-projects && cd ~/sbt-projects 

Create the project:

sbt init my-first-app 

This creates:

my-first-app/
├── models/        (with empty placeholder files test.sql, test.yml)
├── modules/
├── sources/       (with empty placeholder file test.yml)
└── requirements.txt

Delete the empty test.* placeholder files. Then step inside:

cd my-first-app 

This matters more than it looks. SBT reads its configuration, models, and sources from the folder you're standing in. Every sbt command in this guide must run from inside the project folder. If a command can't find your files, check where you are with pwd.

Also create one folder init doesn't make, which you'll need in step 6:

mkdir helpers 

2. Configure it

Run sbt init-config. It asks you questions and writes the answers to a new file, sbtconf.toml, in your project root. Have two things ready:

  • Snowflake connection details: account, user (your email), warehouse, database, schema, role. Use your team's standard dev values; ask your lead if you don't have them. Asked for a private_key? Leave it empty; SSO users don't need one.

  • An Insight Cloud API key. Create one per API Keys.

Then confirm everything connects (a browser window opens for Okta login; that's normal):

sbt check-conn 

sbtconf.toml holds credentials: never commit it or paste its contents anywhere. For every setting explained, extra environments, and switching between them, see Configuring Your Project (sbtconf.toml).

3. Create practice data (optional but recommended)

If you have your own sandbox database, make three tiny tables to learn with. In a Snowflake worksheet:

USE ROLE SEEK_INSIGHTS_ADMIN;
USE WAREHOUSE SNOWFLAKE_LEARNING_WH;

CREATE OR REPLACE TABLE SANDBOX_<YOURNAME>.APP.PRODUCTS (
  product_id INT, product_name VARCHAR, category VARCHAR, unit_price NUMBER(6,2)
);
INSERT INTO SANDBOX_<YOURNAME>.APP.PRODUCTS VALUES
  (1,'Sea Salt Chips','Chips',2.99),(2,'BBQ Chips','Chips',2.99),
  (3,'Gummy Bears','Candy',1.25),(4,'Chocolate Bar','Candy',1.75),
  (5,'Cola','Drinks',2.99),(6,'Sparkling Water','Drinks',1.99);

CREATE OR REPLACE TABLE SANDBOX_<YOURNAME>.APP.STORES (
  store_id INT, store_name VARCHAR, region VARCHAR
);
INSERT INTO SANDBOX_<YOURNAME>.APP.STORES VALUES
  (10,'Downtown','East'),(20,'Airport','East'),(30,'Suburbs','West');

CREATE OR REPLACE TABLE SANDBOX_<YOURNAME>.APP.SNACK_SALES (
  sale_date DATE, store_id INT, product_id INT, units INT
);
INSERT INTO SANDBOX_<YOURNAME>.APP.SNACK_SALES VALUES
  ('2026-08-03',10,1,5),('2026-08-03',10,3,12),('2026-08-03',20,5,9),
  ('2026-08-03',30,4,7),('2026-08-04',10,2,6),('2026-08-04',20,3,15),
  ('2026-08-04',20,6,8),('2026-08-04',30,1,4),('2026-08-05',10,5,11),
  ('2026-08-05',20,4,10),('2026-08-05',30,6,6),('2026-08-05',30,2,9),
  ('2026-08-06',10,4,8),('2026-08-06',20,1,7),('2026-08-06',30,3,14),
  ('2026-08-06',30,5,10);

One fact table (sales), two dimension tables (products, stores). That's the shape of real apps.

4. Define your sources

Sources are the database tables your app reads, declared in YAML. Create sources/sources.yml:

sources:
  - name: app
    description: "Practice tables for the quickstart"
    database: SANDBOX_<YOURNAME>
    tables:
      - name: products
      - name: stores
      - name: snack_sales

The group name is not a label. It must match the schema your tables live in. These tables are in the APP schema, so the group is named app. Models reference tables as group.table (really schema.table), like app.products.

More source options, including bundled sources: Sources.

5. Create your first model (SQL)

A model is one unit of logic that produces one output table. Two files. First models/config.yml:

models:
  - name: revenue_by_region
    config:
      language: sql
      materialized: table
      type: output

type is required: output is the normal kind (one table per app variant). The other types, mapping and shared, are for advanced sharing; see Model Reuse.

Then the model itself, models/revenue_by_region.sql. SQL models reference sources through the run context, no quotes inside the braces:

select
    st.region,
    p.category,
    sum(s.units) as units,
    sum(s.units * p.unit_price) as revenue
from {seek.sources[app.snack_sales]} s
join {seek.sources[app.products]} p on p.product_id = s.product_id
join {seek.sources[app.stores]}  st on st.store_id  = s.store_id
group by st.region, p.category

Test it:

sbt run-model revenue_by_region 

Local runs write with a _test suffix (you'll see target_name='revenue_by_region_test' in the output), so they never collide with production tables. Expect about 10 seconds of startup overhead per run; that's fixed cost, not your data being slow.

More: Models · Helpers & the Run Context · SBT Command References

6. Add a Python model

Python models define everything inline with the @model decorator; no YAML entry needed. seek.models['name'] gives you a table name, which you load through session. Create models/region_share.py:

from sbt.models import model
from snowflake.snowpark.functions import col, sum as sum_, round as round_

@model(name='region_share', materialized='table', type='output')
def region_share(session, seek):
    revenue = session.table(seek.models['revenue_by_region'])
    total = revenue.agg(sum_(col('REVENUE'))).collect()[0][0]
    return revenue.with_column('PCT_OF_TOTAL', round_(col('REVENUE') * 100 / total, 1))

Test it: sbt run-model region_share. SBT automatically resolves revenue_by_region to your _test table from step 5.

(If VS Code underlines the imports with "could not be resolved," press Cmd+Shift+P, run "Python: Select Interpreter," and pick Python 3.11. Cosmetic only; the run works either way.)

7. Bundle both models into an app

An app is a bundle of models that run together. First, apps need a freshness helper: a query returning a single date that tells the platform how new your data is. Create helpers/fresh.sql:

select max(sale_date) as date from {seek.sources[app.snack_sales]} 

Then add the app to the bottom of models/config.yml:

apps:
  - name: my_first_app
    requires:
      - region_share
    config:
      sigma_workbook_id: your_sigma_workbook_id
      sigma_db_attribute: test_db
      freshness_query: fresh
      publisher: "your_publisher_key"
      version: "1"

Line by line: you only list region_share because SBT follows the dependency chain and pulls in revenue_by_region automatically. sigma_workbook_id connects the app to its Sigma dashboard (Data Visualizations with Sigma); a placeholder value is fine for local runs. freshness_query: fresh points at helpers/fresh.sql by filename. publisher is your org's publisher key, created when your org enables the Publisher Portal (Org Management > General > Platform Addons); any string works for local runs, but publishing needs the real key. version is your app's version string.

Full app config options: Apps.

8. Run it

sbt run-app my_first_app 

SBT runs both models in dependency order, then prints your data's freshness date. If you see two green "Success!" boxes: congratulations, you've built a working app.

Where to go next

If something breaks

What you see

Fix

Source X not found in sources

Your source group name must match the schema name, and references are group.table (see step 4)

ValidationError ... type: Field required

Add type: output to your model config (step 5)

ValidationError ... config.publisher: Field required

Add publisher and version to your app config (step 7)

422 Unprocessable Entity for /api/task_runs/ on run-app

Known dependency bug on fresh installs; see Troubleshooting App Installs & Runs

Two pydantic_settings UserWarnings before every command

Harmless; ignore