Run it on your cluster
Freshet decides what to rebuild; your runner rebuilds it. Hand the plan to dagron, Airflow, or your warehouse directly.
The loop
Wherever the work runs, the shape is the one from Get started: plan, run, and commit only if the run succeeded. Pick the runner you already have.
| You run | Hand it the plan with |
|---|---|
| dagron (self-hosted) | freshet submit: one run, one task per model, wired from the plan's own edges |
| Airflow | freshet-airflow: the plan as a DAG |
| SQL, or anything else | --sql and --json, and a few lines of shell |
| Your own platform | the freshet-serve daemon, the Python package, or the C ABI |
On your own dagron
If you run dagron, its API accepts a plan
at /api/state/plans/submit and turns it into one run with a task per model,
ordered by the plan's edges. --command is the command each task runs;
{{ model }} is filled in per task by dagron, not Freshet.
$ export DAGRON_API=http://dagron.internal:8080 DAGRON_TOKEN=… # a dagron API token
$ RUN=$(freshet submit --project models --state state.json \
--to "$DAGRON_API/api/state/plans/submit" \
--header "authorization: Bearer $DAGRON_TOKEN" \
--command 'dbt run --select {{ model }}' | sed -n 's/^submitted .* -> run //p')
$ STATUS=$(curl -s "$DAGRON_API/api/runs/$RUN/wait?timeout_secs=3600" \
-H "authorization: Bearer $DAGRON_TOKEN" | jq -r .status)
$ [ "$STATUS" = succeeded ] && freshet plan --project models --state state.json --commitsubmit never commits: it has created a run, not finished one. Use
--dry-run to print the request without sending it, and --sql to
ship the rendered statements instead of a command. An empty plan exits 0
without calling dagron.
On Airflow
pip install freshet-airflow in the environment that parses your DAGs (it has no
dependencies, so it won't disturb Airflow's pins). Write the plan where the scheduler can
read it; each model becomes a task, wired from the plan's edges.
$ freshet plan --project models --state state.json --json > dags/plan.json# dags/freshet_plan.py
from pathlib import Path
from freshet_airflow import build_dag
dag = build_dag(
Path(__file__).with_name("plan.json").read_text(),
dag_id="freshet_plan",
command_template="dbt run --select {{ model }} --vars 'run_date: {{ ds }}'",
)
# Or run the SQL Freshet rendered (plan made with --sql), through an Airflow connection:
# dag = build_dag(plan_json, dag_id="freshet_plan", conn_id="warehouse")Commit with freshet plan --commit once the DAG run succeeds, for example from a
final task or the job that deployed the DAG.
With the SQL itself
No orchestrator at all: render the plan for your warehouse and run the statements in
order. For Postgres, the whole plan goes through one psql session that stops on
the first error:
#!/usr/bin/env bash
# run-plan.sh PLAN.json — execute a plan rendered with --sql postgres, in plan order.
# One psql session that stops on the first error: a failed run must exit non-zero so
# the commit step never runs. No --single-transaction: statements that need a
# transaction (recreating a table) carry their own BEGIN ... COMMIT.
set -euo pipefail
jq -r '.models[].sql.statements[] | rtrimstr(";") + ";"' "$1" \
| psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -q$ freshet plan --project models --state state.json --sql postgres --json > plan.json
$ ./run-plan.sh plan.json && freshet plan --project models --state state.json --commitFor other warehouses, feed the same statements to snowsql, bq query
or databricks. Check sql.atomic: false means a failure part
way can leave a table half-written (today only a Databricks delete_insert).
Inside your own platform
freshet-serve keeps the parsed project warm and answers over HTTP, so a replan
on 10,240 models takes tens of milliseconds instead of seconds:
$ freshet-serve --project models --sources models/sources.json \
--materializations models/materializations.json --port 8788
freshet-serve listening on http://127.0.0.1:8788
$ curl -s localhost:8788/rpc -d '{"op":"plan"}' | jq -c '{status, token, models: [.plan.models[].name]}'
{"status":"plan","token":0,"models":["events","daily_rollup","user_dim"]}
$ curl -s localhost:8788/rpc -d '{"op":"commit","token":0}'
{"status":"committed","revision":1}The daemon has no authentication yet. It binds to loopback by default; keep it behind your own proxy or network policy.
Also available: pip install freshet-planner (import freshet_planner)
runs the planner in-process in Python, and freshet-ffi is a C ABI for any
language with an FFI. The request and response shapes are in the
API reference.
Where state lives
--state state.json is a file you keep: in the repo, a bucket, or a volume.
It is last-writer-wins, so let only one job commit at a time. For more than one writer
(several CI jobs, several engineers), use hosted state, where every commit is a
compare-and-swap: dagron Cloud.