Skip to main content
Technology & EngineeringAutomation Workflow Services263 lines

Windmill

Build scripts, flows, and apps with Windmill using TypeScript and Python runtimes.

Quick Summary15 lines
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 lines
Paste into your CLAUDE.md or agent config

Windmill 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

  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.

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

Get CLI access →