Documentation menu

Writing models#

A model is a .sql file containing one select statement. Supernova materializes
it as a table in your lake and rebuilds it every 4 hours, in dependency order with
every other model. There is no configuration file, no scheduler to set up — the
SQL itself is the whole definition.

Where models live#

Models are files in your data repo, under models/. Edit them on the Files
page, or clone the repo and use your own editor — it's git either way, with full
history.

text
models/
  monthly_revenue.sql
  revenue/
    active_subscriptions.sql
    churn_risk.sql

The file's basename is the table name: models/revenue/churn_risk.sql
materializes a table named churn_risk. Folders organize files; they don't change
the table name.

A complete model#

sql
-- name: Active subscriptions
-- schema: revenue
select
  s.id,
  s.customer_id,
  c.email,
  s.status,
  s.created_at
from titan.stripe.subscriptions s
join titan.stripe.customers c on c.id = s.customer_id
where s.status in ('active', 'past_due')
  and not s._deleted

This produces revenue.active_subscriptions, queryable like any synced table.
Without the -- schema: line it would land in the default models schema.

A model can select from other models — just name their tables. Supernova reads
the SQL, builds the dependency graph, and runs everything in the right order; a
model that reads its own output table is understood as reading the previous
run's result, which is how running totals and slowly-built history are done.

The header directives#

Directives are comment lines in the file's first 20 lines. All are optional.

DirectiveEffect
-- name: Monthly revenueDisplay name in the file browser and run history
-- schema: revenueOutput schema (default models; lowercase, can't shadow a synced source's schema)
-- cadence: manualExcluded from scheduled runs; rebuilds only when you run it
-- depends_on: stripe.chargesAdds a dependency the SQL alone doesn't reveal
-- unique_key: idIncremental updates by key instead of a full rebuild — see below
-- lint:Per-model diagnostic policy, one TS0000: allow or deny per line

A malformed directive doesn't half-work: the model refuses to run, with an error
naming the line. Better a loud stop than a silently ignored typo.

Rebuild or upsert#

By default each run rebuilds the model's table from scratch — simplest, and right
for most models. Declaring a key changes the strategy:

sql
-- unique_key: id

Now each run upserts by that key: rows with a matching key are replaced, new
rows are added, everything else is left alone. Use it when the model accumulates
history that a rebuild would have to recompute, or when downstream tools watch the
table and shouldn't see it vanish mid-rebuild.

Runs#

Every 4 hours, one run per organization executes all models in dependency order —
up to 4 at a time where the graph allows. The Models page shows the graph,
each model's last result, and a Run now button; a running model gets up to 4
hours before the run marks it failed and moves on to what doesn't depend on it.

Type checking#

Every model is checked against your lake's actual schema before it runs — the
same checker as the editor, so a column dropped by a source or a type changed
upstream is caught as a named diagnostic, not discovered as a broken dashboard.
The -- lint: header tunes severity per model:

sql
-- lint:
--   TS0400: allow
--   TS0609: deny

allow demotes a diagnostic to a warning; deny promotes it to a blocking
error. Add type annotations to the expressions
you most depend on and the checker holds them to it, run after run.