Windmill Labs
Windmill

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.

WindmillWindmill in one sentence

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.

dbtdbt in one sentence

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.

01 · Running dbt

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.

Windmilldbt on Windmilldbtdbt CloudAirflowdbt on Airflow
What you add to the projectNothing. An optional wm_dbt.yaml if you want to pin an engine or a warehouseNothing. You connect the repositoryA Python DAG file per project (Cosmos), plus the dbt install it points at
Where it runsYour own workers, on the dbt worker tagHosted by dbt Labs. PrivateLink and hybrid projects are Enterprise+Your Airflow deployment, or Astro
Which dbtdbt Core 1.x, dbt Core 2.x or Fusion, chosen per project and cached on the workerManaged by dbt LabsWhatever you install. dbt-core conflicts with Airflow dependencies, so it lives in a venv or a container of its own
SchedulingBuilt inBuilt in (jobs)Built in (Airflow)
Event triggersWebhooks, Kafka, Postgres CDC, SQS, MQTT, NATSCron, the API, CI on pull requests, and chaining after another jobSensors and datasets you write yourself
Lineage beyond dbtModels sit on the same asset graph as Python, TypeScript and DuckDB scriptsdbt models only, in the CatalogAirflow tasks and datasets, not a data catalog
Priced onSeats and workers, or free when self-hostedSeats plus successful model builds per monthYour own infrastructure, or Astro deployment and worker hours

How the project lands

WindmillWindmill

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.

dbtdbt

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

WindmillWindmill

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.

dbtdbt

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

WindmillWindmill

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.

dbtdbt

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

WindmillWindmill

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.

dbtdbt

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.

02 · What you can build

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

CapabilityWindmillWindmilldbtdbt
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
Partialdbt 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

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

03 · Build experience

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.

WindmillWindmill

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.

# 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.
dbtdbt, elsewhere

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.

# 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",
),
)

Getting the project in

WindmillWindmill

Copy the project into a <script>__dbt/ folder and push it. There is no transformation step and no Windmill-specific file to add. A team whose repository must stay canonical keeps it and lets Git sync push the project in; a team without one pushes from a working copy.

dbtdbt

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

WindmillWindmill

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.

dbtdbt

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

WindmillWindmill

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.

dbtdbt

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

WindmillWindmill

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.

dbtdbt

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

WindmillWindmill

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.

dbtdbt

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.

04 · Orchestration & lineage

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

WindmillWindmill

Native. The project runs on a schedule, from a trigger (HTTP, Kafka, Postgres CDC, SQS, MQTT, NATS), from a flow step, the CLI or the API. Concurrency limits keep a project that must not run twice at once from doing so, retries included.

dbtdbt

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

WindmillWindmill

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.

dbtdbt

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

WindmillWindmill

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.

dbtdbt

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

WindmillWindmill

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.

dbtdbt

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.

05 · Migration & lock-in

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

WindmillWindmill

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.

dbtdbt

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

WindmillWindmill

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.

dbtdbt

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.

06 · Enterprise requirements

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

WindmillWindmill

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.

dbtdbt

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

WindmillWindmill

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.

dbtdbt

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

WindmillWindmill

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.

dbtdbt

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

WindmillWindmill

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.

dbtdbt

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.

07 · Licensing & pricing

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

WindmillWindmill

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.

dbtdbt

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

WindmillWindmill

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.

dbtdbt

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.

08 · Verdict

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.