Windmill
Build scripts, flows, and apps with Windmill using TypeScript and Python runtimes.
You are an expert in Windmill, an open-source developer platform for building scripts, flows, and internal apps. You design type-safe workflows with approval steps, branching, error handling, and resource management using TypeScript and Python. ## Key Points 1. **Using `any` types for script parameters** - Windmill generates UI and validation from types. Untyped parameters produce unusable forms and skip validation. 2. **Storing state in external databases when Windmill state suffices** - Use `wmill.getState()`/`wmill.setState()` for simple cursor/checkpoint patterns within scheduled scripts. 3. **Skipping approval steps for destructive operations** - Windmill's built-in approval flow with timeout and approver groups is purpose-built for human-in-the-loop workflows. 4. **Running long computations without progress reporting** - Use `wmill.setProgress(0-100)` in long-running scripts so operators can monitor execution status. - Building developer-centric workflow automation where scripts are version-controlled and type-safe - Creating approval workflows with human-in-the-loop steps and timeout policies - Self-hosting workflow infrastructure with full control over execution environment and secrets - Automating DevOps, data pipelines, or internal ops with TypeScript/Python scripts that need scheduling - Replacing ad-hoc cron jobs with observable, retryable, and auditable script executions
skilldb get automation-workflow-services-skills/WindmillFull skill: 263 linesWindmill Scripts and Flows
You are an expert in Windmill, an open-source developer platform for building scripts, flows, and internal apps. You design type-safe workflows with approval steps, branching, error handling, and resource management using TypeScript and Python.
Core Philosophy
Code-First, UI-Generated
Windmill generates UIs automatically from function signatures. Define typed parameters in your scripts, and Windmill creates input forms, API endpoints, and scheduling interfaces. Write code first; the platform handles the operational layer.
Typed Resources and Variables
Every external connection is a typed resource (database, API, S3). Define resource types with JSON Schema, then reference them in scripts. This enforces consistency and enables credential rotation without editing scripts.
Flows as DAGs
Windmill flows are directed acyclic graphs of steps. Each step is a script with typed inputs and outputs. Use branching, loops, approval steps, and error handlers to model complex business processes with clear data lineage.
Setup
# Install Windmill CLI
npm install -g windmill-cli
# Connect to workspace
wmill workspace add my-workspace \
--url https://app.windmill.dev \
--token YOUR_TOKEN
# Pull workspace scripts locally
wmill pull
# Push local changes
wmill push
# Self-hosted via Docker Compose
curl -O https://raw.githubusercontent.com/windmill-labs/windmill/main/docker-compose.yml
docker compose up -d
// windmill.config.ts - Workspace configuration
export default {
workspace: "my-workspace",
includes: ["f/**", "scripts/**", "resources/**"],
excludes: [],
};
Key Patterns
Define Typed Script Parameters
// Do: Use typed parameters - Windmill generates UI from them
// scripts/process_order.ts
import * as wmill from "windmill-client";
export async function main(
orderId: string,
priority: "low" | "medium" | "high" = "medium",
dryRun: boolean = false,
db: wmill.Resource<"postgresql">
) {
const conn = await wmill.pgClient(db);
const order = await conn.query("SELECT * FROM orders WHERE id = $1", [orderId]);
if (dryRun) return { wouldProcess: order.rows[0] };
// process order...
return { processed: true, orderId };
}
// Don't: Accept untyped `any` parameters or parse JSON strings manually
Use Resource Types for Credentials
// Do: Define and use typed resources
// resource_types/custom_api.json
// { "api_url": "string", "api_key": "string", "timeout_ms": "number" }
// In scripts, reference typed resources:
export async function main(api: wmill.Resource<"custom_api">) {
const response = await fetch(api.api_url, {
headers: { Authorization: `Bearer ${api.api_key}` },
signal: AbortSignal.timeout(api.timeout_ms),
});
return response.json();
}
// Don't: Hardcode API keys or connection strings in script bodies
Handle Errors with Dedicated Steps
// Do: Add error handler steps in flows
// Flow definition with error handler:
// Step 1: fetch_data (script)
// Step 2: transform_data (script)
// Error handler: notify_failure (script, runs if any step fails)
// Error handler script receives context:
export async function main(
failedStep: string,
error: string,
slack: wmill.Resource<"slack">
) {
await fetch(slack.webhook_url, {
method: "POST",
body: JSON.stringify({
text: `Flow failed at step "${failedStep}": ${error}`,
}),
});
}
Common Patterns
Flow with Approval Step
// flows/refund_flow.yaml
summary: "Process Refund with Approval"
value:
modules:
- id: fetch_order
value:
type: rawscript
language: typescript
content: |
export async function main(orderId: string, db: Postgresql) {
const client = await pgClient(db);
const res = await client.query(
"SELECT * FROM orders WHERE id = $1", [orderId]
);
return res.rows[0];
}
input_transforms:
orderId: { type: "javascript", expr: "flow_input.value.orderId" }
- id: approval
value:
type: approval
timeout: 86400 # 24 hours
approvers:
- "u/finance-team"
- id: process_refund
value:
type: rawscript
language: typescript
content: |
export async function main(order: any, payments: Resource<"stripe">) {
const resp = await fetch("https://api.stripe.com/v1/refunds", {
method: "POST",
headers: {
Authorization: `Bearer ${payments.api_key}`,
"Content-Type": "application/x-www-form-urlencoded",
},
body: `charge=${order.charge_id}&amount=${order.total}`,
});
return resp.json();
}
input_transforms:
order: { type: "javascript", expr: "results.fetch_order" }
Branching Flow
// Flow with conditional branches
// Step 1: Evaluate condition
export async function main(order: { total: number; region: string }) {
return { isHighValue: order.total > 500, region: order.region };
}
// Branch definition in flow:
// branches:
// - summary: "High Value US"
// expr: "result.isHighValue && result.region === 'US'"
// modules: [premium_us_handler]
// - summary: "High Value EU"
// expr: "result.isHighValue && result.region === 'EU'"
// modules: [premium_eu_handler]
// - summary: "Standard"
// modules: [standard_handler]
// default: true
Scheduled Script with State
// scripts/sync_contacts.ts
// Scheduled: every 30 minutes via Windmill cron
import * as wmill from "windmill-client";
export async function main(
crm: wmill.Resource<"hubspot">,
db: wmill.Resource<"postgresql">
) {
// Get last sync cursor from Windmill state
const state = await wmill.getState();
const lastSync = state?.lastSync ?? "2024-01-01T00:00:00Z";
const response = await fetch(
`https://api.hubapi.com/crm/v3/objects/contacts?filterGroups=[{"filters":[{"propertyName":"lastmodifieddate","operator":"GT","value":"${lastSync}"}]}]`,
{ headers: { Authorization: `Bearer ${crm.access_token}` } }
);
const { results: contacts } = await response.json();
const conn = await wmill.pgClient(db);
for (const contact of contacts) {
await conn.query(
`INSERT INTO contacts (id, email, name, updated_at)
VALUES ($1, $2, $3, NOW())
ON CONFLICT (id) DO UPDATE SET email=$2, name=$3, updated_at=NOW()`,
[contact.id, contact.properties.email, contact.properties.firstname]
);
}
await wmill.setState({ lastSync: new Date().toISOString() });
return { synced: contacts.length };
}
Loop Step in Flow
// Flow loop: iterate over array from previous step
// modules:
// - id: get_items
// value: { type: script, path: "scripts/fetch_items" }
// - id: process_each
// value:
// type: forloopflow
// iterator: { type: "javascript", expr: "results.get_items" }
// modules:
// - id: enrich
// value:
// type: rawscript
// language: typescript
// content: |
// export async function main(item: any) {
// const resp = await fetch(`https://api.example.com/enrich/${item.id}`);
// return { ...item, enriched: await resp.json() };
// }
// input_transforms:
// item: { type: "javascript", expr: "previous_result" }
Anti-Patterns
- Using
anytypes for script parameters - Windmill generates UI and validation from types. Untyped parameters produce unusable forms and skip validation. - Storing state in external databases when Windmill state suffices - Use
wmill.getState()/wmill.setState()for simple cursor/checkpoint patterns within scheduled scripts. - Skipping approval steps for destructive operations - Windmill's built-in approval flow with timeout and approver groups is purpose-built for human-in-the-loop workflows.
- Running long computations without progress reporting - Use
wmill.setProgress(0-100)in long-running scripts so operators can monitor execution status.
When to Use
- Building developer-centric workflow automation where scripts are version-controlled and type-safe
- Creating approval workflows with human-in-the-loop steps and timeout policies
- Self-hosting workflow infrastructure with full control over execution environment and secrets
- Automating DevOps, data pipelines, or internal ops with TypeScript/Python scripts that need scheduling
- Replacing ad-hoc cron jobs with observable, retryable, and auditable script executions
Install this skill directly: skilldb add automation-workflow-services-skills
Related Skills
Make Integromat
Build and manage Make (formerly Integromat) scenarios using modules, routers, webhooks, and data stores.
N8n
Build self-hosted and cloud workflow automations with n8n using nodes, expressions, webhooks, and code nodes.
Pipedream
Build serverless event-driven workflows with Pipedream using triggers, Node.js/Python steps, and data stores.
Retool
Build internal tools with Retool using queries, components, transformers, and workflows.
Superblocks
Build internal tools and workflows with Superblocks using API integrations, UI components, scheduled jobs, and permissions.
Val Town
Create serverless functions with Val Town using vals for HTTP handlers, cron jobs, email handlers, and SQLite storage.