Get started
Install Freshet, plan your first backfill, and learn the loop every setup follows: plan, run, then commit.
Install
One binary, freshet, with no runtime dependencies. Pick whichever fits where it will run.
$ curl -sSL https://github.com/lucheeseng827/freshet/releases/download/v0.1.0/freshet-v0.1.0-x86_64-unknown-linux-musl.tar.gz | tar xz
$ sudo mv freshet-v0.1.0-x86_64-unknown-linux-musl/freshet freshet-v0.1.0-x86_64-unknown-linux-musl/freshet-serve /usr/local/bin/
$ freshet --version
freshet 0.1.0$ cargo install freshet-cli --locked
$ docker run --rm --user "$(id -u):$(id -g)" -v "$PWD:/work" mancube/freshet --version
freshet 0.1.0The image runs as an unprivileged user, so pass --user as above when it
should write state.json into a directory you own. Every release lists its
binaries and checksums on GitHub. Python users can
pip install freshet-planner for the planner in-process.
Your first plan
Freshet reads a directory where every .sql file is one model, named after
the file. Make three: a staging model and two marts that read it.
$ mkdir -p models
$ echo 'select a, b from raw' > models/stg.sql
$ echo 'select a as x from stg' > models/mart_a.sql
$ echo 'select b as y from stg' > models/mart_b.sql
$ echo '{"raw":["a","b"]}' > models/sources.json
$ echo '{"stg":"incremental","mart_a":"incremental"}' > models/materializations.jsonsources.json names the columns of tables you read but don't build.
materializations.json marks which models are incremental; everything else
is a table. Now ask what must rebuild. Nothing has been built yet, so everything does:
$ freshet plan --project models --state state.json --commit
backfill plan (3 model(s), topological order):
stg full (directly changed)
mart_a full (directly changed)
mart_b full (directly changed)
rebuild work: 3.000 CU planned, 0.000 CU (0%) avoided — rebuilding everything downstream of the change whole would be 3.000 CU
committed state -> state.json
$ freshet plan --project models --state state.json
nothing to backfill — state is up to date.--commit saved each model's fingerprint in state.json, so the
second plan has nothing to do. Late data arrives for one day of stg; restate
that partition and the plan follows lineage from it:
$ freshet restate --project models --model stg --partitions 2026-06-20
backfill plan (3 model(s), topological order):
stg partitions[2026-06-20] (directly changed)
mart_a partitions[2026-06-20] (downstream of stg via [x])
mart_b full (downstream of stg via [y])
rebuild work: 1.200 CU planned, 1.800 CU (60%) avoided — rebuilding everything downstream of the change whole would be 3.000 CURead a plan
| Part | What it tells you |
|---|---|
| Order | Models are listed in topological order: run them top to bottom. |
full / partitions[…] | Rebuild the whole model, or only these partitions. Only incremental models can be partition-scoped; mart_b is a table, so it rebuilds in full. |
| Reason | directly changed, or downstream of X via [columns]: the columns through which the change reaches this model. |
| Rebuild work | The plan priced in compute units (CU). One CU is one full rebuild of one table; a partition is 0.1; a view 0.05. The "avoided" figure is against rebuilding everything downstream of the change whole. |
| Widened | When a partition-scoped write can't be honoured, the plan rebuilds more and says why. It never rebuilds less than it must. |
The one rule: commit after the run
Freshet plans; something else runs the plan. State must record only what was actually rebuilt, so the loop is always the same three steps:
- Plan with
freshet plan(add--jsonfor machines,--sqlfor statements). - Run it: dbt, dagron, Airflow, or the rendered SQL.
- Commit with
freshet plan --commit, only if the run succeeded.
Never commit before the run finishes. A commit records the models as up to date; if the run then fails, the next plan skips work that was never done.
Plan a backfill covers real projects: partitions, SQL for your warehouse, budgets, and dbt. Run it on your cluster covers step 2.