Skip to main content

Pipelines quickstart

Alpha

Pipelines are in alpha. The entry point is deliberately tucked away while we develop the feature, and the annotation syntax and behavior described in this guide are still evolving and may change in future releases. We would love your feedback - share it on Discord or GitHub.

This guide builds your first pipeline: a set of scripts in a folder wired together automatically by the data they read and write, no manual orchestration. It uses the recommended DuckDB + DuckLake path, where each step materializes a table (this is what makes the writes idempotent, versioned and testable). It takes about five minutes.

DuckLake is not required, though. Pipelines wire scripts in any language (Python, TypeScript, ...) by the plain assets they exchange - S3 objects, resources, data tables or volumes - with the same -- on model and no materialization. See inputs and outputs for that path; this guide shows DuckLake because it is the most powerful default.

Prerequisite

Because this guide uses DuckLake, you need a workspace storage and a DuckLake configured. The default DuckLake is named main; this guide uses it. (A plain S3-based pipeline needs only the workspace storage.)

On a fresh workspace the pipelines page shows a setup checklist whenever either prerequisite is missing, with a link straight to the workspace object storage and DuckLake settings that fix it. Nothing materializes until both are set.

1. Create a pipeline

On the home page, click New and select Data pipelines (badged Alpha) in the popover.

Create a pipeline from the New popover

You can also open the pipelines index page at /pipeline: it lists existing pipelines with their script counts and lets you pick or create a folder. When you edit a SQL or DuckDB script that is not yet part of a pipeline, a dismissible hint below the toolbar links there too.

The pipelines index page

Pipelines live in a folder (for example f/demo); every script you add to it and mark with -- pipeline becomes part of the same pipeline graph.

2. Add a producer script

Create a DuckDB script f/demo/ingest. The -- pipeline line places it in the folder's pipeline, and -- materialize tells Windmill to own the write: it creates the ducklake://main/events table from the trailing SELECT and records a snapshot and row count.

-- pipeline
-- materialize ducklake://main/events

SELECT * FROM (VALUES
(1, 'click', TIMESTAMP '2026-01-01 10:00'),
(2, 'view', TIMESTAMP '2026-01-01 10:05'),
(3, 'click', TIMESTAMP '2026-01-01 11:00')
) AS t(id, kind, ts);

3. Add a consumer script

Create a DuckDB script f/demo/rollup. The -- on ducklake://main/events annotation declares the table it reads, which becomes an incoming edge: this script runs automatically whenever events is materialized. It materializes its own aggregate table.

-- pipeline
-- on ducklake://main/events
-- materialize ducklake://main/events_by_kind

ATTACH 'ducklake://main' AS dl;

SELECT kind, count(*) AS n
FROM dl.events
GROUP BY kind;

4. Open the pipeline graph

Open the folder and select the pipeline view. You will see the lineage:

ingestducklake://main/eventsrollupducklake://main/events_by_kind

5. Run it

On the ingest node, choose "Run + downstream". ingest materializes events, and the asset cascade automatically fires rollup, which materializes events_by_kind. Each node shows live status, its DuckLake snapshot and row count as the run progresses, and the result panel previews the materialized table.

That is the whole model: mark scripts with -- pipeline, declare inputs with -- on, and Windmill infers and runs the graph from asset lineage. Adding -- materialize (the optional DuckLake layer used here) is what also gives you idempotent re-runs, time-travel, data tests and backfill with no extra work.

Next steps

Add materialization strategies (merge, append, SCD2 history), partitions, schedules, AND/OR joins, data tests and debounce. Every annotation and option is documented, with examples, on the concept page.