---
title: Writing models
description: A model is a select statement in a file. Supernova keeps it materialized as a table, refreshed in dependency order.
species: guide
---

# 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 run
-- 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.

| Directive | Effect |
|---|---|
| `-- name: Monthly revenue` | Display name in the file browser and run history |
| `-- schema: revenue` | Output schema (default `models`; lowercase, can't shadow a synced source's schema) |
| `-- cadence: manual` | Excluded from scheduled runs; rebuilds only when you run it |
| `-- depends_on: stripe.charges` | Adds a dependency the SQL alone doesn't reveal |
| `-- unique_key: id` | Incremental 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 fragment
-- 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 fragment
-- lint:
--   TS0400: allow
--   TS0609: deny
```

`allow` demotes a diagnostic to a warning; `deny` promotes it to a blocking
error. Add [type annotations](/v2/docs/typesql/annotations) to the expressions
you most depend on and the checker holds them to it, run after run.
