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 runHand it the plan with
dagron (self-hosted)freshet submit: one run, one task per model, wired from the plan's own edges
Airflowfreshet-airflow: the plan as a DAG
SQL, or anything else--sql and --json, and a few lines of shell
Your own platformthe 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.

submit, wait, commit
$ 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 --commit

submit 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.

write the plan
$ freshet plan --project models --state state.json --json > dags/plan.json
dags/freshet_plan.py
# 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:

run-plan.sh
#!/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
plan, run, commit
$ freshet plan --project models --state state.json --sql postgres --json > plan.json
$ ./run-plan.sh plan.json && freshet plan --project models --state state.json --commit

For 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:

the daemon
$ 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.