
Windmill models automation as composition: small, typed scripts are arranged into flows that execute as a single job. Flows are represented as directed acyclic graphs in which each step can be a script from the workspace or the Windmill Hub, inline code, a loop, or a branch. Building a first flow raises three concrete questions: what the flow accepts as input, how each step consumes the results of earlier steps, and what the flow returns when it finishes. Windmill's answers are a piped input model, expression-based bindings between steps, and a return rule defined by the last executed step. This guide covers those mechanics in the visual flow editor, the incremental testing workflow that supports them, and the code-first alternative that defines jobs and their dependencies in Python or TypeScript.

In Windmill a flow is a state machine expressed as a directed acyclic graph (DAG) that stitches scripts together into a single job — the foundation for all reusable automations and apps【2†url】. When you create a new flow the editor shows only two blocks: an Input step that declares the flow’s parameters and a Result step that collects the final output. You add intermediate steps with the + button, arranging them in any order that respects the DAG’s no‑cycle rule.
A step can host one of five script kinds, each selectable from the workspace, the public Windmill Hub, or written inline in the flow editor【0†url】:
| Kind | Default role |
|---|---|
| Action | Standard executable script (default) |
| Trigger | Starts the flow based on external events |
| Approval | Pauses execution until a user approves |
| Error handler | Runs when a previous step fails |
| Preprocessor | Pre‑runs transformation before the main logic |
Choosing a saved workspace or Hub script is encouraged when the step is likely to be reused, because it decouples the logic from the flow definition and enables versioning across projects.
By default Windmill pipes results between steps: the JSON output of a step becomes a named variable that later steps can reference (e.g., step_3.result). The Result block automatically receives the output of the last executed step, making it straightforward to feed the flow’s output to another flow or an app UI【2†url】. This model works well for lightweight automation where payloads remain modest.
When dealing with large or non‑JSON data—common in heavy ETL pipelines—piping can become inefficient. Windmill therefore offers a shared directory (./shared) that steps can read from or write to, bypassing the result‑piping mechanism. Using the shared folder is recommended only for sizable payloads; otherwise keep steps small, reuse saved scripts, and let the DAG’s native piping handle data flow.
./shared – use it only for large binary blobs or massive tabular data that would strain JSON piping.Understanding this DAG‑centric data model is the first step toward building composable, maintainable Windmill automations.

Flows become reusable components when they expose their variable parts as flow inputs. The first step in productionising a flow is to define those inputs explicitly, rather than hard‑coding values inside script steps.
spreadsheet_id and a worksheet_name for a gsheets/getValues flow.object, number, or other JSON‑schema types, matching the downstream script’s expectations.The defined inputs drive the auto‑generated UI for the flow’s run dialog, so callers can fill out a form without writing any front‑end code. Windmill also supports richer input schemas:
oneOf – Allows a single input to present a selectable list of distinct object schemas, rendering a drop‑down that lets the user pick the desired shape [Windmill Changelog – oneOf support][https://www.windmill.dev/changelog].Version‑sensitivity – The exact behavior of
oneOfand dynamic selects depends on the installed Windmill version; the features were introduced in specific releases documented in the changelog. Verify the runtime version in your deployment before relying on these UI patterns.
Best‑practice checklist
spreadsheet_id, not id1) [Windmill Docs – Metadata recommendations][https://www.windmill.dev/docs/getting_started/flows_quickstart].By following this sequence, a flow is transformed into a generic, composable component that can be invoked from other flows, the Hub UI, or programmatic APIs while preserving type safety and a clean, auto‑generated user experience.

Windmill flows accept inputs through a piped model that lets each step pull values from three distinct sources.
A static binding is a literal entered directly into the step’s input field. It can be a string, number, or JSON fragment and remains unchanged for every execution of the flow. Because the value is baked into the step definition, static bindings are ideal for true constants such as fixed URLs or hard‑coded thresholds【0†content】.
Flow‑level constants are exposed via flow_env.VARIABLE_NAME. They may hold strings, JSON objects, or resources (e.g., database connections). Workspace variables can be linked to a flow env string through the $ button, allowing secrets or external references to be injected at run time【0†content】【1†content】. Use this mechanism when the value varies per‑deployment but should be available to every step—common cases include API keys, file paths, or shared configuration objects.
When a step needs data computed earlier in the DAG, bind its input to the result of another step. You can reference a prior step by its identifier (e.g., step_5) or, more interactively, click the plug logo next to the target variable. After the flow or the upstream step has been tested, the UI presents a list of reachable outputs; selecting one creates an expression like step_5.result【0†content】. For scripts that produce large payloads, store the data in ./shared and pass only a reference to avoid bloating JSON results.
If you prefer a code‑first approach, type the expression directly, e.g., flow_input.my_param or flow_env.API_KEY, into the input field【4†content】.
| Binding type | When to use | Performance tip |
|---|---|---|
| Static | Values that never change (constants, flags) | No runtime lookup |
| Flow env | Secrets, resources, per‑run configuration | Keeps sensitive data out of step JSON |
| Dynamic | Data derived upstream (API responses, computed aggregates) | For large blobs, write to ./shared instead of passing through JSON |
Note: Some community tutorials show placeholder syntax such as
${previous_step.result}. Windmill’s official syntax is expression‑based, using step IDs,flow_input, andflow_env. Treat the placeholder form as a paraphrase, not the canonical binding language.

In Windmill a flow’s Result block is simply the output of the step that finishes last. The platform treats the flow as a pipeline, so whatever value – JSON object, scalar, or stream – the final step returns becomes the flow’s return value that callers (other flows, UI widgets, APIs) consume【2†url】.
Because the return contract is that simple, arranging steps deliberately is essential. The step you intend to expose as the public payload should be placed last (or be the early‑return step). Treat any incidental step that happens to run last as a designated return step to avoid accidental payload changes.
Windmill lets you tune per‑step error behavior in the Advanced tab:
error field that downstream steps can inspect to distinguish the different outcomes【1†url】.Starting with the 2025.3 release, the last step may return a stream (any AsyncGenerator or iterator‑compatible object). While the job remains in the queue, Windmill streams results via Server‑Sent Events; once the job finishes, the full stream becomes the flow’s final result, or it is attached as wm_stream if a non‑stream result already exists【6†url】. This capability is useful for large LLM responses or progressive data feeds, but it is only available in recent releases, so check your platform version before relying on it.
Following these guidelines makes a flow’s return contract explicit, predictable, and aligned with the consuming components’ expectations.

Windmill’s visual flow editor lets you enrich a DAG without sacrificing the ability to reuse steps. The three main control‑flow primitives—loops, branches, and inner flows—are each a distinct step type that can be combined with the platform’s error policy.
item) that feeds every step inside the loop. The loop runs once per element and automatically wires the item variable to downstream expressions 【source】(https://www.windmill.dev/docs/getting_started/flows_quickstart).status == 'ready'”.A flow can invoke another flow as a step. The called flow runs in isolation, and its final output becomes the ordinary result of the calling step. This encourages a library of reusable sub‑flows: when a group of steps repeats in several places, extract it to a separate flow and reference it instead of duplicating the graph 【source】(https://www.windmill.dev/docs/triggers).
branch‑one to examine the error field and perform custom handling 【source】(https://www.windmill.dev/changelog).error field, which downstream conditions can test without extra bookkeeping 【source】(https://www.windmill.dev/changelog).continue‑on‑error if you need to react to failures inside the same branch.error field, so you can distinguish success, failure, and human‑driven outcomes directly in branch conditions.
Windmill treats every step as a testable unit. After configuring a step you can click the Test button to run the flow up to that point, letting you see the intermediate output before later steps exist. The Test up to x action (available from the step’s context menu) executes the DAG from the flow input through the selected node, returning the exact values that downstream steps would receive. This pattern lets you catch wiring mistakes early, without having to run the whole job.
Triggers can be exercised in test mode, which records the incoming webhook payload without invoking the production handler. The captured payload can then be reused as a concrete example for the flow’s input schema. Windmill even lets you update the flow’s JSON schema directly from this test data, automatically inferring argument types such as numbers or strings 【5†source】. This makes the schema‑driven validation step repeatable and version‑controlled.
When a flow finishes, the Logs tab opens a log explorer that presents a tree view of every step. Each node shows a live status indicator and expands to reveal the exact input passed to the step and the result it returned. The flow status viewer adds a high‑level overview: branches (branchall, branchone), while‑loops, and for‑loops expose their current branch or iteration status in the top node. For‑loop detail pages list every iteration’s outcome without pre‑loading the entire list, allowing you to jump to any iteration on demand 【2†source】.
A changelog entry introduced a tests‑as‑scripts mechanism. Adding a comment of the form
// test: path/to/target
to any script converts it into an automated test for the specified target script or flow. These tests run automatically on every deployment of the target, and their pass/fail/running status appears on the target’s detail page 【4†source】. Built‑in templates for TypeScript and Python simplify writing such tests, and CI badges can be displayed on workspace comparison pages.
Once all steps pass local testing and the run inspection looks correct, the editor offers four deployment options:
These actions complete the iterative workflow: design → step‑level test → run inspection → automated test integration → production rollout.

Windmill’s Workflows as Code lets you author orchestrations directly in Python or TypeScript, bypassing the visual flow editor. The approach is a single script that defines jobs and their dependencies using a small set of primitives. Because the definition lives in code, it can be generated, templated, or conditionally assembled at runtime—capabilities that are awkward in a static graph UI.
workflow() – entry point that registers the orchestration.task() – launches a child job; each task gets its own log and timeline.taskScript() / taskFlow() – invoke existing Windmill scripts or flows by path, preserving reuse.step() – records a lightweight inline result (e.g., a timestamp or random ID) that is replay‑deterministic.sleep() – server‑side delay that fully releases the worker slot.waitForApproval() – pauses execution until a human approves; the worker is released during the wait.parallel() – runs a collection of tasks concurrently, optionally bounded by a concurrency limit.These primitives were introduced in WAC v2, which switched to checkpoint‑based orchestration. After each primitive, the workflow suspends, releases its worker, and stores a checkpoint. This design eliminates worker waste during sleeps, approvals, or child‑task execution, prevents deadlocks, and allows unlimited parallelism across any number of workers【1†url】.
WAC is currently an on‑demand trigger mechanism that applies only to scripts (not to visual flows). The workflow must be invoked explicitly—e.g., via an API call or a schedule—rather than through a flow‑type trigger such as a webhook attached to a visual pipeline. This restriction is documented in the product changelog and should be considered when choosing an orchestration style.
Both the TypeScript and Python SDKs expose a flag to control approver identity:
selfApproval (default true).self_approval (default True).Setting the flag to false forces the approval step to require someone other than the workflow triggerer, matching the visual editor’s “require external approver” toggle【2†url】.
| Scenario | Recommended approach |
|---|---|
| Fixed, simple pipelines that benefit from visual inspection | Visual flow editor |
| Dynamically generated pipelines, conditional branches, or programmatic reuse of sub‑flows | Workflows as Code |
| Need for fine‑grained control over parallelism, checkpointing, or zero‑waste sleeping | Workflows as Code |
| Teams that already maintain Python/TypeScript tooling and want to keep orchestration versioned alongside application code | Workflows as Code |
In practice, developers often start with the visual editor for rapid prototyping, then migrate to WAC once the pipeline stabilises or when the orchestration logic becomes too complex to represent cleanly on a canvas.
import { workflow, task, sleep, waitForApproval, parallel } from "windmill";
workflow(async () => {
const data = await taskScript("scripts/fetch_data");
await sleep(30); // releases the worker for 30 seconds
const approvals = await waitForApproval({
selfApproval: false, // require external approver
message: "Approve data processing?"
});
if (!approvals.approved) return;
await parallel([
task(() => taskScript("scripts/transform_a", { data })),
task(() => taskScript("scripts/transform_b", { data }))
]);
const result = await step(() => ({
timestamp: Date.now(),
id: crypto.randomUUID()
}));
return result;
});
The snippet demonstrates checkpoint suspension (sleep, waitForApproval), child‑job dispatch (taskScript), deterministic inline results (step), and parallel execution (parallel). Because each primitive creates a checkpoint, the workflow can resume after any interruption without re‑executing completed steps. This model provides deterministic, resource‑efficient orchestration that scales automatically with the available worker pool.

Create the required Input and Result steps
final_report).Declare input variables
Bind step inputs
flow_env.VARIABLE_NAME (including resource references), orflow_input.{variable_name} can be typed manually to link a step input to a flow input without using the UI【{"id":2,"cursor":1}】.Configure control structures
Manage large payloads
./shared directory and reference them instead of returning the full payload in JSON results.Set retry policies
Document metadata
Validate against changelog‑driven features
Choose a finishing route