Skip to content
Go back

Windmill vs Activepieces

By KingPin 12 min read
Windmill vs Activepieces
Contents

Your Zap Bill Has Opinions About Your Weekend

You built one automation to copy form submissions into a spreadsheet. Then it was ten. Then Zapier sent a usage email and you started wondering what a rack of your own would cost. Fair. Two open-source projects keep showing up in that conversation: Windmill and Activepieces.

Activepieces is the one you want if your automations are “when X happens in app A, do Y in app B” and you want a friendly drag-and-drop builder, like Zapier with the lid off. Windmill is the one you want if half your automations are really scripts that need a scheduler, an input form, and a permissions model. Picking Windmill to connect Gmail to Slack is hiring a forklift to move a couch. Picking Activepieces to run your Python backup checker is bringing a couch to a forklift rally. Both work, and both are awkward.

Full example: Clone the working files at github.com/KingPin/sumguy-examples/self-hosting/windmill-vs-activepieces

Versions in this post are current as of October 2026: Windmill v1.832.0 and Activepieces 0.92.3, both read from the GitHub releases API.

What Each One Actually Is

Windmill: scripts first, flows second

Windmill’s backend is Rust. API servers and workers are stateless, and workers pull jobs from a queue that lives in Postgres. You write a script, and Windmill reads the main function signature to generate an input form, an API endpoint, and a schedule target. Then you chain scripts into flows.

The README lists the languages: TypeScript (Bun by default, Deno only on the windmill-full images), Python (with uv for dependencies), Go, Bash, PowerShell, PHP, Rust, C#, Java, and Ansible, plus SQL. Jobs can run under nsjail (opt-in, off in the default compose file) and PID namespace isolation (on by default).

Here is a complete Windmill script. The type hints are the whole UI definition:

disk_alert.ts
export async function main(host: string, used_percent: number, threshold: number = 85) {
const alert = used_percent >= threshold;
return { host, used_percent, threshold, alert, message: alert ? `${host} is at ${used_percent}% disk` : "ok" };
}

Save it, and Windmill renders a form with a text field, a number field, and a number field defaulting to 85. Python works the same way:

disk_alert.py
def main(host: str, used_percent: int, threshold: int = 85):
alert = used_percent >= threshold
return {"host": host, "used_percent": used_percent, "alert": alert}

Activepieces: pieces first, code when you must

Activepieces is TypeScript end to end. Integrations are called pieces, and they are npm packages you can write yourself. The README says 200+ pieces, and that all of them are also exposed as MCP servers (it says 280+ there, so the two numbers do not match; treat both as “a few hundred”). It also says 60% of the pieces come from the community.

You build flows in a browser canvas: pick a trigger, add steps, map fields. There is a Code piece for the moments when the canvas runs out, and an HTTP piece for anything with an API. The vendor’s own FAQ says the same: if your tool has no piece, use HTTP or a webhook.

The Compose Files, Side by Side

Both projects ship an official docker-compose.yml. They show very different philosophies.

Windmill: a small fleet

The official file starts Postgres 18, one server, three default workers (2 GB memory limit each), one native worker for lightweight jobs, a combined “extra” container for the editor’s language server and debugger, and Caddy in front.

docker-compose.yml (windmill, trimmed)
services:
db:
image: postgres:18
volumes:
- db_data:/var/lib/postgresql
windmill_server:
image: ${WM_IMAGE}
environment:
- DATABASE_URL=${DATABASE_URL}
- MODE=server
windmill_worker:
image: ${WM_IMAGE}
deploy:
replicas: 3
environment:
- DATABASE_URL=${DATABASE_URL}
- MODE=worker
- WORKER_GROUP=default
caddy:
image: ghcr.io/windmill-labs/caddy-l4:2.11.4-1
ports:
- 80:80
- 25:25

Same image everywhere, and MODE picks the role. Port 25 forwards to the server’s port 2525 for email triggers. Note the volume mount on Postgres: since version 18 the official image wants the parent directory, and the compose file carries a long comment about migrating a Postgres 16 volume. Read that comment before you upgrade an old install. The default worker also runs privileged: true so PID namespace isolation works. That is a real trade-off on a shared box, so know it is there.

Terminal window
cd windmill
cp .env.example .env
docker compose up -d
# open http://localhost, log in with [email protected] / changeme, then change it

Activepieces: four containers

Activepieces is leaner. An app container, workers, Postgres (the pgvector/pgvector:0.8.0-pg14 image), and Redis.

docker-compose.yml (activepieces, trimmed)
services:
app:
image: ghcr.io/activepieces/activepieces:0.92.3
ports:
- '8080:80'
env_file: .env
environment:
- AP_CONTAINER_TYPE=APP
worker:
image: ghcr.io/activepieces/activepieces:0.92.3
env_file: .env
environment:
- AP_CONTAINER_TYPE=WORKER
deploy:
replicas: 5
postgres:
image: 'pgvector/pgvector:0.8.0-pg14'
redis:
image: 'redis:7.0.7'

Same story: one image, a variable (AP_CONTAINER_TYPE) picks the role. The .env is where you can hurt yourself. You set AP_ENCRYPTION_KEY, AP_JWT_SECRET, AP_API_KEY, and the Postgres password. The docs say that without AP_ENCRYPTION_KEY, stored connections cannot be decrypted, even from a full database backup. Back that key up with the database dump, because your 2 AM self will not remember where it lives.

Terminal window
cd activepieces
cp .env.example .env
# fill AP_API_KEY, AP_ENCRYPTION_KEY, AP_JWT_SECRET, AP_POSTGRES_PASSWORD
docker compose up -d
# open http://localhost:8080

Both files pass docker compose config -q in the companion repo. I validated them. I did not boot them for the post, so treat the examples as a starting point.

Triggering Each One From curl

A webhook is the fastest way to feel the difference.

Windmill gives every script an endpoint. The path is /api/w/<workspace>/jobs/run/p/<script path> for async, and run_wait_result in place of run for sync. You send a bearer token:

Terminal window
curl -X POST "http://localhost/api/w/demo/jobs/run/p/u/admin/disk_alert" \
-H "Authorization: Bearer $WM_TOKEN" \
-H "Content-Type: application/json" \
-d '{"host":"nas01","used_percent":91}'

The JSON keys match your main parameters. No flow, no trigger step. The script is the API.

In Activepieces, you add a Catch Webhook trigger and the builder shows you the URL. The server routes /api/v1/webhooks/<flow id> for async calls and /api/v1/webhooks/<flow id>/sync for callers that wait for the flow’s response. The docs only show the /sync form.

Terminal window
curl -X POST "http://localhost:8080/api/v1/webhooks/<FLOW_ID>" \
-H "Content-Type: application/json" \
-d '{"host":"nas01","used_percent":91}'

The payload lands in the trigger step, and later steps map fields from it by clicking. That is great when the data comes from an app with a proper piece. It is more ceremony when the “app” is your own script.

Writing Custom Logic

This is where the two split hardest.

In Windmill, custom logic is the default path. You write the script above, and you get versioning, a form, a schedule, and an API for free. Dependencies resolve from the imports, so there is no build step.

In Activepieces, small logic goes in a Code step. Bigger logic becomes a piece, which is a TypeScript package with typed props. Here is the shape of a custom action, following the pattern in their piece docs:

disk-alert-action.ts
import { createAction, Property } from '@activepieces/pieces-framework';
export const diskAlert = createAction({
name: 'disk_alert',
displayName: 'Disk Alert',
description: 'Flags a host whose disk usage crosses a threshold',
props: {
host: Property.ShortText({ displayName: 'Host', required: true }),
usedPercent: Property.Number({ displayName: 'Used %', required: true }),
threshold: Property.Number({ displayName: 'Threshold %', required: false, defaultValue: 85 }),
},
async run(context) {
const { host, usedPercent, threshold } = context.propsValue;
return { host, alert: usedPercent >= (threshold ?? 85) };
},
});

More plumbing than the Windmill version, and you build it inside a clone of the Activepieces repo with a dev environment. The payoff is that the action then shows up in the builder for everyone, with a proper form and typed fields. You are building a Lego brick, not a script. If a non-developer teammate builds flows, bricks beat scripts.

Licensing: Read This Before You Bet a Business on It

Both projects are open source with a catch, and the catches differ.

Activepieces is MIT, with an exception. The LICENSE file says everything outside packages/ee/ and packages/server/api/src/app/ee is “MIT Expat”. The ee directories fall under a separate commercial license. The Community Edition is a clean MIT core. Their pricing FAQ says that paying buys access control, audit, team separation, safe change shipping, and agents and chat.

Windmill is messier. The LICENSE file says backend and frontend are AGPLv3, except code compiled under the enterprise flag, which is proprietary. The client libraries and OpenAPI files are Apache 2.0. Here is the gotcha: the Docker image you pull from ghcr.io/windmill-labs/windmill is the “Community Edition”, and the LICENSE says that image contains AGPLv3 and Apache 2 code plus proprietary, non-public code. You may use all its features free, within the limits and quotas built into the software. You may not sell it, resell it, serve it as a managed service, modify it, or wrap it without an agreement.

Running it for your own homelab or your own company is fine under those terms. Building a product on top of it is a call to their sales team. If you need pure AGPL, you build from source without the enterprise flag.

Pricing, If You Skip Self-Hosting

As of October 2026, the Activepieces pricing page says the Free plan gives you 1,000 credits per month with no card. A run costs one credit however many steps it has. On the Plus and Team plans, extra credits cost $0.007 each. AI steps cost more: 2 credits on a fast model, 10 on a smart one, 20 on a frontier model, unless you bring your own provider key (Plus and up), which drops each AI step to 1 credit. That is per-run billing, which is friendlier than counting every step.

I did not fetch current Windmill or Zapier prices, so I will not quote them. Check both pricing pages before you decide.

Sandboxing and the 2 AM Question

If strangers or interns can edit flows, sandboxing matters. Windmill supports nsjail as an opt-in sandbox, which the default compose file leaves off. That compose file does turn on PID namespace isolation with FAVOR_UNSHARE_PID=true and privileged: true on the default worker, and the worker memory is capped at 2 GB.

Activepieces uses AP_EXECUTION_MODE. The .env.example sets UNSANDBOXED, which means flow code runs without isolation. Their sandboxing docs call the mode “the most important security choice in a self-hosted deployment” and recommend SANDBOX_CODE_ONLY (V8 isolation) for enterprise deployments. On a single-user homelab, the default is acceptable. On a box where other people write flows, change it on day one.

Which One Fits Whom

Pick Activepieces if:

Pick Windmill if:

For a typical homelab, my pick is Windmill if you already write scripts, and Activepieces if you currently live in Zapier’s canvas. Migrating off Zapier with Activepieces feels familiar because the mental model matches: trigger, steps, mapped fields. Migrating with Windmill means rewriting each Zap as a script, which is more work and a better result.

Do not run both on one small VPS unless you enjoy resource contention. Windmill’s default compose file alone starts three 2 GB-limited workers plus a native one.

Common Questions

Is Activepieces free to self-host?

Yes. Activepieces Community Edition is MIT licensed, so you can self-host it for free with no execution limits imposed by the license. The code under the ee directories is commercial and needs a paid plan. Paid plans add access control, audit logs, team separation, and agents.

Is Windmill really open source?

Partly. Windmill’s backend and frontend are AGPLv3, but the Docker image also contains proprietary code under a free-use license. You can use every feature free within built-in limits. You cannot resell Windmill, serve it as a managed service, or modify and wrap the image without an agreement.

What hardware does Activepieces need?

The docs give one number: a recommended 0.5 vCPU and 1 GB of RAM per worker in a dedicated execution tier, with about 300 MB of that used as warm-process overhead. The official compose file starts five workers by default, so lower replicas on a small box.

Can Windmill or Activepieces replace Zapier?

Yes, for self-hosted use, with some rewriting. Activepieces has pieces for common apps, and its HTTP piece covers any API. Windmill covers any API too, but through scripts instead of ready-made connectors. Check that each app you use has a piece before you migrate.


Share this post on:

Send a Webmention

Written about this post on your own site? Send a webmention and it'll show up above once verified.


Previous Post
What Actually Fills Your Loki Storage
Next Post
Sizing 7 Days to Die and Other RAM Hogs

Discussion

Powered by Garrul . Sign in with GitHub or Google, or post anonymously.

Related Posts