Windmill vs dbt
Windmill runs unmodified dbt projects as scripts of their own, so the question is not Windmill or dbt. It is where your dbt runs, what it costs, and what runs beside it: Windmill, dbt Cloud, or an Airflow stack you operate. Eight questions, answered honestly.
- 01Running dbtWhere your dbt project actually runs
- 02What you can buildWhich data work fits on each
- 03Build experienceHow you import, edit and ship a project
- 04Orchestration & lineageScheduling, triggers and the graph around dbt
- 05Migration & lock-inHow hard to get in, how hard to get out
- 06Enterprise requirementsAudit logs, observability, security, performance
- 07Licensing & pricingOpen source, pricing, self-hosting
- 08VerdictThe verdict
An open-source developer platform that runs your dbt project unmodified on your own workers, with scheduling, triggers, permissions and run history around it, and its models on the same asset graph as everything else you run: native DuckDB pipelines, workflows, AI agents and apps. Any language.
The de-facto standard for SQL transformation in the warehouse: models with a ref() / source() lineage DAG, tests, snapshots, macros and a large package ecosystem. dbt Core is transform-only and needs something to run it on a cadence, which is what dbt Cloud sells: a hosted IDE, job scheduling, CI, the Catalog and the Semantic Layer, priced per seat plus successful model builds.
Where does your dbt project actually run?
A dbt project needs a machine, a warehouse connection and something that runs it on a cadence. There are three common answers: dbt Cloud hosts all three and meters model builds; Airflow (self-run or on Astronomer) gives you an orchestrator you operate, with the dbt install kept apart from it; Windmill takes the project as-is and runs it on your own workers, with the scheduling, triggers and history built in.
dbt on Airflow | |||
|---|---|---|---|
| What you add to the project | Nothing. An optional wm_dbt.yaml if you want to pin an engine or a warehouse | Nothing. You connect the repository | A Python DAG file per project (Cosmos), plus the dbt install it points at |
| Where it runs | Your own workers, on the dbt worker tag | Hosted by dbt Labs. PrivateLink and hybrid projects are Enterprise+ | Your Airflow deployment, or Astro |
| Which dbt | dbt Core 1.x, dbt Core 2.x or Fusion, chosen per project and cached on the worker | Managed by dbt Labs | Whatever you install. dbt-core conflicts with Airflow dependencies, so it lives in a venv or a container of its own |
| Scheduling | Built in | Built in (jobs) | Built in (Airflow) |
| Event triggers | Webhooks, Kafka, Postgres CDC, SQS, MQTT, NATS | Cron, the API, CI on pull requests, and chaining after another job | Sensors and datasets you write yourself |
| Lineage beyond dbt | Models sit on the same asset graph as Python, TypeScript and DuckDB scripts | dbt models only, in the Catalog | Airflow tasks and datasets, not a data catalog |
| Priced on | Seats and workers, or free when self-hosted | Seats plus successful model builds per month | Your own infrastructure, or Astro deployment and worker hours |
How the project lands
One dbt project is one Windmill script: the project files ride with it as its module bundle, and the worker materializes them into the job directory before invoking dbt. dbt_project.yml is what marks the bundle as a project. Nothing is cloned at run time, and the version of the project is the version of the script, so a deploy is atomic and a rollback is a redeploy.
dbt Cloud connects to the repository and runs the branch you point a job at. Self-managed, the project is a checkout on whatever machine runs it, and keeping that checkout, the dbt install and the orchestrator in step is your job.
Which dbt runs it
Three engines, chosen per project: dbt-core-1x (the default, a uv virtualenv resolved per adapter), dbt-core-2x and fusion. None is baked into the image: each is fetched on first use and cached on the worker, and an operator can pre-stage one in a derived image for an air-gapped instance.
dbt Cloud manages the engine for you, including Fusion. On Airflow it is yours to install, and because dbt-core conflicts with Airflow dependencies, the recommended setups run it in a separate virtualenv or a container, which is one more thing to build and upgrade.
The warehouse connection
Warehouses are configured once per workspace (a name, a resource and an optional target), and a project reaches one by name. The adapter is inferred from the resource type, and Windmill renders profiles.yml before dbt runs. A project that would rather keep its own profiles.yml can, with credentials injected as Windmill variables.
profiles.yml plus environment variables for dbt Core, maintained per environment and per machine. dbt Cloud manages connections in the UI instead, with governed connections and fine-grained access on the paid tiers.
What a run looks like
One dbt build per job, with dbt providing the parallelism and Windmill the observability: the project graph turns green model by model while the run is in flight, every node ends with its status, timing, row count and dbt message, test severity is honored, and Preview rows dispatches a dbt show for one node. A failed run resumes from its failure point.
dbt Cloud has mature run history, alerting and the Catalog, and can also retry from the failure point. On Airflow, Cosmos turns each model into a task, so you get Airflow-grade retries and a task graph, at the cost of task overhead per model and a stack to operate.
Which data work can you do on each?
On the dbt layer the two columns are mostly the same tool, because Windmill invokes dbt itself. The differences are on either side of it: dbt Cloud owns the catalog, the docs site, the semantic layer and column-level lineage over dbt models, while Windmill adds the compute, the triggers, a lineage graph that reaches non-dbt code, and a native pipeline runtime for the transforms you would rather not put in a warehouse.
The dbt project itself
| Capability | ||
|---|---|---|
It is dbt on both sides: Windmill invokes dbt build against the unmodified project | ||
Windmill resolves them once at deploy and pins the result in the script lockfile | ||
dbt Core 1.x, dbt Core 2.x or Fusion, per project. None is baked into the image: each is fetched and cached on the worker | ||
Postgres, Redshift, MySQL, DuckDB, Snowflake, BigQuery and Databricks are inferred from a resource; MS SQL Server and Oracle need Enterprise; anything else runs through a profiles.yml kept in the project | ||
Rebuild only what a failed run left failed or skipped | ||
Nodes turn green on the project graph while the run is in flight (dbt Core 1.x engine) | dbt Cloud | |
Browser dev loop for models Compile, preview and branch from the browser. Windmill has a project editor with a parsed model graph and row previews, but the dev loop stays local | Partial | dbt Cloud |
Column-level lineage on dbt models manifest.json carries declared column metadata but no column-to-column edges, so Windmill draws none for dbt | dbt Cloud | |
Docs site and catalog A browsable, generated documentation site of the project (dbt Explorer / Catalog) | dbt Cloud | |
Semantic layer and metrics Central metric definitions queried by BI tools | dbt Cloud |
What runs around it
| Capability | ||
|---|---|---|
Run the project on a cron with retries, alerts and run history | dbt Cloud | |
Webhooks, Kafka, Postgres CDC, SQS, MQTT and NATS on the same project | ||
Inside your network, on machines you size (dbt Cloud hybrid projects and PrivateLink are Enterprise+) | Enterprise+ | |
A Python, TypeScript or DuckDB script that names a dbt:// relation appears beside the model that writes it | ||
Native pipelines: DuckDB transforms materialized into managed DuckLake tables, no warehouse in the loop | ||
Read any managed table AT (VERSION => n), on native pipelines | ||
Daily, hourly, weekly, monthly or dynamic slices backfilled as a range of idempotent runs, on native pipelines (dbt: microbatch incremental) | Partial | |
Python, TypeScript, Go, Bash and 20+ more next to SQL (dbt: Python models on some adapters) | Partial | |
General flows, HTTP endpoints, AI agents and internal apps on the same runtime | ||
dbt Core is free to self-host but you supply the scheduler and the machines it runs on | dbt Core |
On the Windmill side, the dbt runtime, its editor and the model graph are all Community Edition: only the mssql and oracle adapters need Enterprise Edition. Rows about the native pipeline runtime (time travel, partitions, materialization) are a separate feature set from the dbt runtime, and within it range backfill, the freshness watchdog and write-audit-publish are Enterprise.
How do you import, edit and ship a project?
dbt Cloud owns the browser dev loop and per-PR CI, and that is where it is strongest. Windmill treats the project as the unit of deployment: copy it in, push, and the engine, the packages and the connection are resolved and pinned for you, with the local dbt loop untouched.
The project is the unit of deployment. Copy it in and push: the files ride with the script as its bundle, the worker materializes them before invoking dbt, and nothing is cloned at run time. The wm_dbt.yaml descriptor is optional: it pins the engine, the warehouse and the selection, and turns a {{ }} var into a run argument.
- Import
- wm_dbt.yaml
# The project is copied in as-is. Nothing to rewrite.
mkdir -p f/analytics/analytics__dbt
cp -r my-dbt-project/. f/analytics/analytics__dbt/
# Deploys the script f/analytics/analytics, parses the
# project and stores its model graph.
wmill sync push
# From there, the schedules, triggers, permissions and
# run history belong to the platform, not the project.
# f/analytics/analytics__dbt/wm_dbt.yaml - optional
engine: dbt-core-1x # or dbt-core-2x, or fusion
profile:
warehouse: main # configured once, in workspace settings
select: ["tag:nightly+"]
vars:
run_date: "{{ day }}" # becomes a required run argument
retry_failed_nodes:
attempts: 2
delay_seconds: 30
dbt Cloud asks for nothing in the repository either: you connect it and configure jobs in the UI. Self-orchestrating is where the wrapper appears. With Cosmos the project becomes an Airflow DAG defined in Python, pointed at a dbt install kept apart from Airflow. The warehouse connection stays yours to maintain in profiles.yml.
- analytics_dbt.py
- profiles.yml
# dags/analytics_dbt.py - Airflow + astronomer-cosmos
from cosmos import DbtDag, ProjectConfig
from cosmos import ProfileConfig, ExecutionConfig
from cosmos.profiles import (
PostgresUserPasswordProfileMapping,
)
profile_config = ProfileConfig(
profile_name="analytics",
target_name="prod",
profile_mapping=PostgresUserPasswordProfileMapping(
conn_id="warehouse",
),
)
analytics = DbtDag(
dag_id="analytics",
schedule="@daily",
project_config=ProjectConfig("/opt/dbt/analytics"),
profile_config=profile_config,
# dbt-core conflicts with Airflow deps, so it lives
# in a virtualenv (or a container) of its own.
execution_config=ExecutionConfig(
dbt_executable_path="/opt/dbt_venv/bin/dbt",
),
)
# profiles.yml - the connection, maintained per environment
analytics:
target: prod
outputs:
prod:
type: postgres
host: "{{ env_var('WAREHOUSE_HOST') }}"
user: "{{ env_var('WAREHOUSE_USER') }}"
password: "{{ env_var('WAREHOUSE_PASSWORD') }}"
dbname: analytics
schema: marts
threads: 8
Getting the project in
dbt Cloud connects the repository and takes it from there, which is about as low-friction as it gets. Self-orchestrated, the project has to be delivered to the runner: baked into an image, cloned at run time, or mounted, and that plumbing is yours.
Editing
A dbt script opens in an editor shaped like a project: a file tree, the descriptor, the run form and a model graph. Refresh models redraws that graph from the buffer by running a real dbt parse on your workers, so it agrees with dbt about enabled, macro-built refs and package models. The browser is not where a dbt project is developed, though: that stays a local dbt run loop.
The dbt Cloud IDE (and Studio) is a full browser dev loop: branch, compile, preview, run and open a pull request, with Copilot on the paid tiers. For dbt Core, your own editor and the CLI, which is what most teams use day to day.
Configuration
Optional, in wm_dbt.yaml inside the project: the engine, the warehouse, select / exclude (dbt's own grammar, passed verbatim), vars, threads, full_refresh, in-job retry of failed nodes, and an env map that resolves Windmill variables for the project env_var() lookups. A {{ }} placeholder in vars becomes a required run argument, so a date-parameterized project gets a real run form, webhook payload and schedule argument.
dbt_project.yml for the project, profiles.yml for the connection, and job settings (commands, schedule, environment, threads) in the dbt Cloud UI. On Airflow, the same settings move into the DAG file and the Airflow connection.
Dependencies
packages.yml is resolved once, at deploy, and pinned in the script lockfile alongside the engine and adapter versions. A run restores the package tree from a worker-local cache keyed on that resolution, and a worker that resolves anything else is refused rather than run.
dbt deps re-resolves ranges on every invocation unless package-lock.yml is committed, which is dbt's own recommendation. dbt Hub is a genuine advantage here: hundreds of mature community packages, and they work the same on Windmill since it is the same dbt.
Git & CI
Everything is files: the script, the project bundle, resources, schedules and permissions, deployed by CLI or Git sync (free up to 2 users, Enterprise only beyond). Workspace forks give each branch its own environment.
dbt is Git-native by design, and dbt Cloud CI jobs that build and test only the models a pull request changed are one of the most mature parts of the product. Windmill does not have a per-PR model-diff CI of that shape.
What schedules the project, and what does the graph show?
dbt Cloud schedules dbt and catalogs dbt, with the better catalog of the two. Windmill schedules it from anything an event can reach and puts the models on one graph with the rest of your code, which is where the lineage stops being dbt-shaped.
Scheduling & triggers
dbt Cloud jobs run on a cron or an interval, from the API, on pull requests (CI), or after another job finishes. There are no event triggers of the Kafka or CDC kind: that is what the Airflow route is usually for, and then you are operating Airflow.
Models on the graph
Every model, seed, snapshot and source becomes an asset named dbt://<warehouse>/<schema>/<name>, with dbt's own ref() lineage as edges, taken from the manifest rather than from a scan. Two projects pointing at the same warehouse share their nodes instead of drawing two islands.
dbt Explorer and the Catalog draw the project graph with model, test and source metadata, and add column-level lineage. It is the more mature catalog, and it covers dbt models (plus what dbt Mesh links across projects).
Lineage past dbt
A Python, TypeScript, DuckDB or Ansible script that names a dbt:// relation is detected as a reader of that exact node, so the script that ships the mart to a customer appears beside the model that writes it. Column-level lineage is not available for dbt models, since the manifest carries no column-to-column edges; native pipelines do infer it from the SQL.
The graph stops where dbt stops. Whatever ingests into the warehouse or consumes from it lives in another tool, and stitching those together is what a separate orchestrator or catalog is for.
Cascading runs
A dbt run does not fire the scripts downstream of it. dbt already orders its own DAG, and a run select can build any subset, so Windmill refuses to draw a cascade arrow that would not reliably fire; you schedule the consumer or run it from the graph. Native pipelines do cascade: a write triggers every reader.
Same boundary. dbt orders models within a project, dbt Mesh links projects on Enterprise, and anything outside dbt is chained by a job trigger or by the orchestrator you brought.
How hard to get in, and how hard to get out?
dbt keeps your models portable wherever you run them, and the data stays in your warehouse. Windmill takes the project unchanged and hands it back unchanged, so the cost of trying it is a copy, and the cost of leaving is the scheduling around it.
Getting in
A copy and a push. The project is not rewritten, not converted and not annotated, and the only Windmill file is a descriptor you can skip. The bundle carries the authored files and leaves out what dbt generates (target, dbt_packages, logs) as well as .env files, whose contents belong in variables instead.
dbt Cloud is a repository connection and a job. Airflow is a DAG file per project plus a dbt install kept separate from Airflow, which is more work but stays entirely in your infrastructure.
Getting out
wmill sync pull writes the project back verbatim, and the tree stays a canonical dbt project that dbt itself runs with --project-dir. What you leave behind is the scheduling and the graph, not the models. Adopting the native pipeline runtime instead of dbt is the one path that is a real rewrite, so it mostly makes sense for new projects.
Model SQL is portable in every direction, which is dbt's biggest strength here. What does not move is the layer around it: dbt Cloud jobs, environments, Catalog and Semantic Layer configuration are Cloud, and a Cosmos DAG is Airflow.
Audit logs, observability, security, performance
Both gate governance behind paid tiers, and both are SOC 2 audited. dbt Cloud puts SSO, audit logs, PrivateLink and Mesh on Enterprise and Enterprise+. Windmill keeps the dbt runtime itself in the open-source edition and gates the platform-wide governance features instead.
Observability
Real-time streaming logs, per-run inputs, outputs and duration, the project graph with per-model status, timing and row counts, and a Prometheus exporter. Every job is a Windmill job, so dbt runs sit in the same run history, alerting and metrics as everything else in the workspace.
dbt Cloud has run history, notifications, dbt Explorer and Insights, all focused on the transformation graph. Self-orchestrated, observability is whatever your orchestrator gives you plus dbt artifacts.
Audit logs & security
SOC 2 Type II. RBAC, SSO (up to 10 users), encrypted secrets and sandboxed execution in open source. Uncapped SSO, extended audit-log retention, SCIM and SAML are Enterprise only. A warehouse is reached by any user allowed to run the script that names it, so warehouse access is granted through the script permissions.
SOC 2. RBAC, SSO (SAML), audit logging and granular permissions are dbt Cloud Enterprise features, with PrivateLink, IP restrictions and hybrid projects on Enterprise+. dbt Core self-hosted has no built-in RBAC or SSO.
Multi-tenancy
Multiple isolated workspaces on one instance, each with its own users, resources, warehouses and secrets. The free tier is capped at 3 workspaces; unlimited is Enterprise only. Any number of dbt projects per workspace.
Projects are the unit, and the tier caps how many you get: one on the free Developer and Starter tiers, more on Enterprise. dbt Mesh, which lets projects reference each other, is an Enterprise feature.
Performance & scale
Runs land on your own workers, on the dbt worker tag, so a project reaching a private warehouse runs on a worker that can reach it and a heavy project can be pinned to a bigger pool. Engines and package trees are cached per worker, so the cold start is paid once. The warehouse still does the SQL, at whatever threads you set.
Performance is the warehouse, plus how fast the runner starts. dbt Cloud manages that for you (with Fusion aimed squarely at parse and compile time); on Airflow it depends on the execution mode you picked, since container-per-model isolation costs startup time on every node.
Open source, pricing, and self-hosting?
dbt Core is Apache 2.0 and free wherever you run it, but the products that run it for you are not: dbt Cloud meters successful model builds, Astro bills deployment and worker hours. Windmill publishes per-seat and per-worker pricing, and the dbt runtime is in the free, self-hostable edition.
Open-source license
AGPLv3 core, free and unlimited to self-host, and the dbt runtime is part of it: only the mssql and oracle adapters need a license. Other Enterprise features (uncapped SSO, dedicated workers, audit logs, external secret backends) ship in a separate proprietary codebase. Managed cloud available.
dbt Core is Apache 2.0, more permissive for modify-and-redistribute, and Windmill runs it unchanged. The newer Fusion engine is Elastic License 2.0 (source-available, not OSI-approved) and subject to the dbt Labs license agreement, and dbt Cloud is fully proprietary.
Pricing
Public per-seat and per-worker pricing on the pricing page: developers around $20/month, operators $10/month, standard workers $50/month. Nothing is metered per model build, and the open-source core is free however many models you build.
Seats plus usage: the free Developer tier is 1 seat and 3,000 successful model builds per month, Starter is $100 per user/month for up to 5 seats and 15,000 builds, and Enterprise and Enterprise+ pricing is not public. Running dbt on Astro instead moves the bill to deployment and worker hours (from $0.35/hr per deployment and $0.13/hr per worker), and self-hosting Airflow moves it to your own infrastructure and the time to operate it.
The verdict
This is not a choice between two transformation frameworks. Windmill runs dbt: one project is one script, the files ride with it unmodified, and the engine is dbt Core 1.x, dbt Core 2.x or Fusion, whichever the project names. The real comparison is with what you would otherwise put around dbt, which is dbt Cloud, or an Airflow stack (self-run or on Astronomer) with the dbt install kept apart from it.
dbt Cloud is the right call if the analytics team lives in the browser IDE, per-PR CI on changed models matters, and you want the Catalog, column-level lineage over dbt models and the Semantic Layer as a product rather than a project to assemble. Windmill has none of those, and says so above. The trade is a hosted runtime priced per seat and per successful model build.
Windmill is the stronger fit if the project should run on machines you own, inside your network, without a per-model-build meter, and if what surrounds dbt matters as much as dbt: schedules and event triggers (webhooks, Kafka, Postgres CDC, SQS) on the same project, run history and permissions shared with everything else, and models on one asset graph with the Python, TypeScript and DuckDB scripts that feed and consume them. For transforms you would rather not push into a warehouse at all, the native pipeline runtime does them in-platform with DuckDB and DuckLake, with data tests, SCD2 history, partitions, backfill and column-level lineage.
The honest framing is that these compose. Keep dbt for the modeling, and choose where it runs. Trying Windmill costs a cp -r and a push, and wmill sync pull gives the project back exactly as it was.
Frequently asked questions
Build your internal platform on Windmill
Scripts, flows, apps, and infrastructure in one place.
dbt on Airflow