Skip to main content

Workflow-as-code: orchestration in pure code

· 5 min read
Ruben Fiszel

Workflow-as-code

Day 5 of Windmill launch week. You can now define complex workflows entirely in TypeScript or Python. Windmill handles checkpointing, parallelism, and fault tolerance. You write functions.

The problem

Windmill's flow editor is powerful for visual workflows. But some orchestration logic is easier to express in code: dynamic branching, complex error handling, loops over variable-length data, or workflows that need to live in your codebase alongside the rest of your application.

Other workflow-as-code frameworks (Temporal, Inngest) require dedicated infrastructure, complex SDKs, or proprietary runtimes. We wanted the same capabilities with two annotations and zero new infrastructure.

Workflow-as-code: functions, not YAML

A workflow is a regular script with @workflow and @task annotations. Each task runs as a separate Windmill job with its own logs, timeline entry, and retry policy. Between tasks, the workflow fully suspends and releases its worker.

import { task, workflow, sleep, parallel } from 'windmill-client';

export async function main(urls: string[]) {
const results = await parallel(urls, async (url) => {
const data = await task(async () => {
const res = await fetch(url);
return res.json();
});
return await task(async () => {
return transform(data);
});
}, { concurrency: 5 });

await sleep(60); // suspend for 60s, release worker

await task(async () => {
await saveResults(results);
});

return { processed: results.length };
}

No YAML, no DSL, no drag-and-drop. Standard TypeScript or Python with full IDE support, type checking, and version control.

How checkpointing works

Workflow-as-code uses a checkpoint/replay model:

  1. The workflow runs until it hits a task(), sleep(), or waitForApproval() call.
  2. The script exits and the checkpoint is saved to the database.
  3. The worker is released back to the pool. No resources are wasted while waiting.
  4. Child jobs run independently on any available worker.
  5. On replay, all previously completed steps return cached results instantly.

This means a workflow that sleeps for 24 hours consumes zero worker time during the wait. A workflow with 100 parallel tasks does not hold 100 workers.

Core primitives

PrimitiveDescription
task()Run a function as a separate Windmill job
step()Run inline, persist the result for replay stability
sleep(seconds)Suspend the workflow, release the worker
waitForApproval()Suspend until a human approves or rejects
parallel(items, fn)Process a list with concurrency control
taskScript(path)Dispatch to an existing Windmill script
taskFlow(path)Dispatch to an existing Windmill flow

Each task() supports options for timeout, worker tag, cache TTL, priority, and concurrency limits.

Why we built it this way

Three design choices drove the architecture:

Zero worker waste. When a workflow suspends (sleep, approval, waiting for child jobs), the worker is fully released. Other frameworks hold a thread or container open. Windmill's checkpoint model means you pay only for compute you actually use.

Standard language, standard tooling. Workflows are regular TypeScript or Python files. You get IDE autocomplete, type checking, unit testing, and Git diffs. No proprietary DSL to learn.

Composable with flows. Workflow-as-code scripts can call existing Windmill scripts and flows via taskScript() and taskFlow(). You can also use them as steps inside visual flows. The two models are fully interoperable.

Script modules

For complex workflows, you can split logic into companion modules in a __mod/ folder:

my_workflow.ts
my_workflow__mod/
├── extract.ts
├── transform.ts
└── load.ts

Each module has its own dependencies and lock file. Import with relative paths, reference via taskScript().

When to use workflow-as-code vs flows

Workflow-as-codeVisual flows
DefinitionTypeScript or PythonDrag-and-drop editor
Best forDynamic logic, complex branching, code-first teamsLinear pipelines, visual overview, low-code users
Version controlStandard Git diffsJSON diffs
Local devFull IDE supportWeb editor
InteropCan call flows via taskFlow()Can include WAC scripts as steps

Getting started

  1. Create a new script in TypeScript or Python.
  2. Import task and workflow from windmill-client (TS) or wmill (Python).
  3. Annotate your main function and wrap each unit of work in task().
  4. Run it. Each task appears as a separate job in the Windmill UI.

That's a wrap

Thanks for following along this week. Five days, five features: Data Tables & Ducklake, full-code apps, AI sandboxes, Git sync & workspace forks, and workflow-as-code. All available now. Try them out.

Windmill Logo
Windmill is an open-source and self-hostable serverless runtime and platform combining the power of code with the velocity of low-code. We turn your scripts into internal apps and composable steps of flows that automate repetitive workflows.

You can self-host Windmill using a docker compose up, or go with the cloud app.