* feat(ai-sessions): turn skills on by default, and group them by folder A skill is instructions the workspace wrote for the assistant to use, so what carrying one costs is context rather than access. Selecting each one before it applied made publishing a skill a two-step affair, and left most of them unused. Skills now default to on. No storage is rewritten to get there: the preference keeps its key and holds a decision per path, so the older array of enabled paths still reads as "these were on" and only the paths nobody decided about move. MCP servers stay opt-in through the same factory — their tools reach an external system, which is a different question from context. The Skills settings list groups into a tree once skills span more than one folder, with a switch per folder acting on everything beneath it, and the list answers the keyboard: Up/Down walk it, Left/Right fold, Space flips the switch under the highlight, Enter opens the skill. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JWUQ867ZJCZJmkWUxqHija * fix: give a modal the option to stand only as tall as the window `AIPromptsModal` asks for 1000px of height, which is taller than a laptop window: the dialog then scrolled inside the overlay while its list scrolled inside the dialog — two scrollbars, one of them moving the modal itself. The cap `Modal2` appeared to have, `max-h-screen-80`, is defined nowhere in the tailwind config, so it never applied to anything. `fixedHeight="viewport"` is a new value that stands as tall as the window allows. Deliberately a definite height rather than a max-height: bodies here size against the box with `h-full` / `grow min-h-0` and scroll inside it, and a max-height leaves them nothing to resolve against — they grow past the surface instead. Every existing size keeps the height it has today, so no other modal moves. The two classes that resolved to nothing are removed. The prompts modal and the assistant settings modal take the new value; both already scroll inside themselves. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JWUQ867ZJCZJmkWUxqHija * fix: pin read_skill's gate in its test, and say who a delete affects The refusal `read_skill` gives for a path that is not a skill changed shape — it checks the workspace listing now, not just the off-switch — and its test was still asserting the old wording against an unmocked listing. The delete confirmation said everyone "who selected it" loses the skill, which stopped being true when skills started defaulting to on. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JWUQ867ZJCZJmkWUxqHija * fix: keep the keyboard walk when the list scrolls under the pointer The mouse takes the skills list back on a real movement over it, not on `mouseenter`. The browser fires that one whenever rows arrive under a stationary pointer — every scroll the keyboard itself causes, and every folder collapse — so walking Down past the bottom of the list handed control back to a mouse nobody had touched, and the next press restarted at the top. Also from the review round: the "+" menu sorted skills on-first, a key that is constant now that they start on, and pushed the one row it did move — a skill just turned off there — out of the shortcut that turns it back on. It orders by path. The remaining "selection" wording follows the vocabulary the rest of this change moved to. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JWUQ867ZJCZJmkWUxqHija * fix: carry the keyboard walk on from the row the mouse left it on Handing the list to the mouse dropped the highlight, so the next arrow press started again at the top. It moves to the row under the pointer instead — invisible while the mouse leads, since drawing and acting both wait on the keyboard being in charge, and exactly where someone would expect the walk to carry on from. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JWUQ867ZJCZJmkWUxqHija * fix: cap the AI prompts modal from its own call site Reverts `Modal2` and the assistant settings modal to what they were. The prompts modal asks for `xxl`, 1000px, which is taller than a laptop window, so the dialog scrolled inside the overlay while its list scrolled inside the dialog. It now passes `max-h-[80vh]` through the `css.popup` the component already forwards. The height stays definite underneath, which is what lets the list bound its own scroller, and nothing outside this one modal changes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JWUQ867ZJCZJmkWUxqHija * refactor: drive the skills list highlight with useListHighlight The Tools section next door already had this: `useListHighlight` owns the highlighted index, wrapping, `scrollIntoView`, and the rule that a scroll under a resting pointer must not hand the list back to the mouse — the bug this section rediscovered the hard way. Reusing it drops the parallel implementation. What stays local is what is actually a tree: Left and Right fold a folder or step into it, Space flips the switch under the highlight, and Enter opens the lit skill. `restingIndex` is what keeps the highlight on a folder through a fold, where a search would instead send it back to its top hit. The keys are answered at the window rather than on the list: leaving the editor parks focus elsewhere, and a container-scoped handler goes silent when it does. `move` is now returned by the composable, for the step into a folder's children. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JWUQ867ZJCZJmkWUxqHija * fix: stop the fold's sticky row resetting the keyboard walk `stickyKey` is read through `restingIndex`, which `useListHighlight` calls inside the effect that reacts to the row count. As `$state` it was also a dependency of that effect, so clearing it on the next arrow re-ran the effect and wrote the highlight back to nothing: after collapsing a folder, one Down lit nothing and the one after it started again at the top. It is a plain variable now, read when the effect runs and invalidating nothing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JWUQ867ZJCZJmkWUxqHija * fix: keep one lit row, and keep the fold's sticky row to its fold Three from the review of the `useListHighlight` swap: The sticky row a fold takes is now given up as soon as that fold has rendered. Held until the next arrow, it pulled the highlight back to that folder on any later change — another fold, a save, a delete, a workspace switch. Space and Enter on a focused control bring the highlight to that control's row before the control answers them. A switch keeps focus after a plain click, and the row drawn as highlighted was then a different one from the row that flipped. Up and Down carry on from a row reached with Tab. `useListHighlight` cannot see that by itself: `ListRow` puts the row's id on its outer div while focus sits on the button inside it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JWUQ867ZJCZJmkWUxqHija * fix: land the highlight on a named row rather than stepping to it `move` counts steps from wherever the highlight is, and from nothing lit it can only reach an end of the list — so the three places that meant "put it on this row" (a row reached with Tab, the row of a focused control, a folder's parent) sent it to the first row whenever nothing was lit yet. `useListHighlight` grows a `moveTo` for naming the row outright, and those three use it. The handler's own doc still said the keys are answered on the list; they went back to the window when the editor's page transition proved able to take focus away from it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JWUQ867ZJCZJmkWUxqHija * fix: fold from the header click the way every other fold does The header's own click wrote `collapsed` directly instead of going through `fold`, so the row count changed with no row named to keep: the highlight reset, and since the highlight is the header's only hover feedback, it went flat under a pointer that had not moved and stayed flat. Also from the round: a duplicated `svelte-ignore`, the missing one on the header wrapper that takes `onmouseenter`, and a trailing comma prettier wanted gone. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JWUQ867ZJCZJmkWUxqHija --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Open-source developer platform for internal code: APIs, background jobs, workflows and UIs. Self-hostable alternative to Retool, Pipedream, Superblocks and a simplified Temporal with autogenerated UIs and custom UIs to trigger workflows and scripts as internal apps.
Scripts are turned into sharable UIs automatically, and can be composed together into flows or used into richer apps built with low-code. Supported languages: Python, TypeScript, Go, Bash, SQL, GraphQL, PowerShell, Rust, and more.
Try it - Website - Docs - Discord - Hub - Contributing
Windmill - Developer platform for APIs, background jobs, workflows and UIs
Windmill is fully open-sourced (AGPLv3) and Windmill Labs offers dedicated instances and commercial support and licenses.
https://github.com/user-attachments/assets/d80de1d9-64de-4d89-aacd-6df23fa81fc4
- Windmill - Developer platform for APIs, background jobs, workflows and UIs
Main Concepts
- Define a minimal and generic script in Python, TypeScript, Go or Bash that solves a specific task. The code can be defined in the provided Web IDE or synchronized with your own GitHub repo (e.g. through VS Code extension): provided Web IDE or synchronized with your own GitHub repo (e.g. through VS Code extension):
- Your scripts parameters are automatically parsed and generate a frontend.
- Make it flow! You can chain your scripts or scripts made by the community shared on WindmillHub.
- Build complex UIs on top of your scripts and flows.
Scripts and flows can be triggered by schedules, webhooks, HTTP routes, Kafka, WebSockets, emails, and more.
Build your entire infra on top of Windmill!
Show me some actual script code
//import any dependency from npm
import * as wmill from "windmill-client";
import * as cowsay from "cowsay@1.5.0";
// fill the type, or use the +Resource type to get a type-safe reference to a resource
type Postgresql = {
host: string;
port: number;
user: string;
dbname: string;
sslmode: string;
password: string;
};
export async function main(
a: number,
b: "my" | "enum",
c: Postgresql,
d = "inferred type string from default arg",
e = { nested: "object" }
//f: wmill.Base64
) {
const email = process.env["WM_EMAIL"];
// variables are permissioned and by path
let variable = await wmill.getVariable("f/company-folder/my_secret");
const lastTimeRun = await wmill.getState();
// logs are printed and always inspectable
console.log(cowsay.say({ text: "hello " + email + " " + lastTimeRun }));
await wmill.setState(Date.now());
// return is serialized as JSON
return { foo: d, variable };
}
Local Development
Windmill supports multiple ways to develop locally and sync with your instance:
| Tool | Description |
|---|---|
| CLI | Sync scripts from local files or GitHub, run scripts/flows from the command line |
| VS Code Extension | Edit and test scripts & flows directly from VS Code / Cursor with full IDE support |
| Git Sync | Two-way sync between Windmill and your Git repository |
| Claude Code | AI-assisted development with Claude for scripts, flows, and apps |
https://github.com/user-attachments/assets/c541c326-e9ae-4602-a09a-1989aaded1e9
You can run scripts locally by passing the right environment variables for the wmill client library to fetch resources and variables from your instance. See local development docs.
Stack
- Database: Postgres (compatible with Aurora, Cloud SQL, Neon, Azure PostgreSQL)
- Backend: Rust - stateless API servers and workers pulling jobs from a Postgres queue
- Frontend: Svelte 5
- Sandboxing: nsjail and PID namespace isolation
- Runtimes:
- TypeScript/JavaScript: Bun (default) and Deno
- Python: python3 with uv for dependency management
- Go, Bash, PowerShell, PHP, Rust, C#, Java, Ansible
Fastest Self-Hostable Workflow Engine
We have compared Windmill to other self-hostable workflow engines (Airflow, Prefect & Temporal) and Windmill is the most performant solution for both benchmarks: one flow composed of 40 lightweight tasks & one flow composed of 10 long-running tasks.
All methodology & results on our Benchmarks page.
Security
- Sandboxing: nsjail for filesystem/resource isolation, and PID namespace isolation (enabled by default) to prevent jobs from accessing worker process memory
- Secrets: One encryption key per workspace for credentials stored in Windmill's K/V store. We recommend encrypting the Postgres database as well.
See Security documentation for details.
Performance
Once a job started, there is no overhead compared to running the same script on the node with its corresponding runner (Deno/Go/Python/Bash). The added latency from a job being pulled from the queue, started, and then having its result sent back to the database is ~50ms. A typical lightweight deno job will take around 100ms total.
Architecture
How to self-host
For detailed setup options, see Self-Host documentation.
Docker compose
Deploy Windmill with 3 files (docker-compose.yml, Caddyfile, .env):
curl https://raw.githubusercontent.com/windmill-labs/windmill/main/docker-compose.yml -o docker-compose.yml
curl https://raw.githubusercontent.com/windmill-labs/windmill/main/Caddyfile -o Caddyfile
curl https://raw.githubusercontent.com/windmill-labs/windmill/main/.env -o .env
docker compose up -d
Go to http://localhost - default credentials: admin@windmill.dev / changeme
Using an external database: Set DATABASE_URL in .env to point to your managed Postgres (AWS RDS, GCP Cloud SQL, Azure, Neon, etc.) and set db replicas to 0.
Kubernetes (Helm charts)
helm repo add windmill https://windmill-labs.github.io/windmill-helm-charts/
helm install windmill-chart windmill/windmill --namespace=windmill --create-namespace
See windmill-helm-charts for configuration options.
Cloud providers
Windmill works on AWS (EKS/ECS), GCP, Azure, Ubicloud, Fly.io, Render.com, Hetzner, Digital Ocean, and others. Rule of thumb: 1 worker per 1vCPU and 1-2 GB RAM.
OAuth, SSO & SMTP
Configure OAuth and SSO (Google Workspace, Microsoft/Azure, Okta) directly from the superadmin UI. See documentation.
License
The Community Edition is free to use internally. For commercial redistribution or managed services, contact sales@windmill.dev. See LICENSE and Pricing for details.
The "Community Edition" of Windmill available in the docker images hosted under ghcr.io/windmill-labs/windmill and the github binary releases contains the files under the AGPLv3 and Apache 2 sources but also includes proprietary and non-public code and features which are not open source and under the following terms: Windmill Labs, Inc. grants a right to use all the features of the "Community Edition" for free without restrictions other than the limits and quotas set in the software and a right to distribute the community edition as is but not to sell, resell, serve Windmill as a managed service, modify or wrap under any form without an explicit agreement.
The binary compilable from source code in this repository without the "enterprise" feature flag is open-source under the LICENSE-AGPLv3 License terms and conditions.
To re-expose directly any Windmill parts to your users as a feature of your product, with the exception of iframed public Windmill "apps", or to build a feature on top of "Windmill Community Edition" that you sell commercially or embed in a distributable product or binary, you must get a commercial license. Contact us at sales@windmill.dev if you have any questions. To do the same from the binary compiled from the source code in this repository without the "enterprise" feature flag, you must comply with the AGPLv3 license terms and conditions or get a commercial license from Windmill Labs, Inc.
To use Windmill "Community Edition" as is internally in your organization, or to use its APIs as is, you do NOT need a commercial license.
Integrations
In Windmill, integrations are referred to as resources and resource types. Each Resource has a Resource Type that defines the schema that the resource needs to implement.
On self-hosted instances, you might want to import all the approved resource types from WindmillHub. A setup script will prompt you to have it being synced automatically everyday.
Environment Variables
| Environment Variable name | Default | Description | Api Server/Worker/All |
|---|---|---|---|
| DATABASE_URL | The Postgres database url. | All | |
| WORKER_GROUP | default | The worker group the worker belongs to and get its configuration pulled from | Worker |
| MODE | standalone | The mode if the binary. Possible values: standalone, worker, server, agent | All |
| METRICS_ADDR | None | (ee only) The socket addr at which to expose Prometheus metrics at the /metrics path. Set to "true" to expose it on port 8001 | All |
| JSON_FMT | false | Output the logs in json format instead of logfmt | All |
| BASE_URL | http://localhost:8000 | The base url that is exposed publicly to access your instance. Is overriden by the instance settings if any. | Server |
| ZOMBIE_JOB_TIMEOUT | 30 | The timeout after which a job is considered to be zombie if the worker did not send pings about processing the job (every server check for zombie jobs every 30s) | Server |
| RESTART_ZOMBIE_JOBS | true | If true then a zombie job is restarted (in-place with the same uuid and some logs), if false the zombie job is failed | Server |
| NATIVE_MODE | false | Enable native mode: sets NUM_WORKERS=8, rejects non-native jobs (nativets, postgresql, mysql, etc.) | Worker |
| SLEEP_QUEUE | 50 | The number of ms to sleep in between the last check for new jobs in the DB. It is multiplied by NUM_WORKERS such that in average, for one worker instance, there is one pull every SLEEP_QUEUE ms. | Worker |
| KEEP_JOB_DIR | false | Keep the job directory after the job is done. Useful for debugging. | Worker |
| EXIT_AFTER_N_JOBS | None | Exit the worker process after it has executed that many jobs, so that a supervisor restarts it and no process runs more than that many, bar the steps of a same-worker flow it has started, which it always finishes (set it to 1 for a process per job; jobs handed to a dedicated worker, and the worker's own init and periodic scripts, do not count). Not counting the init and periodic scripts means they run again on every restart: an init script's runtime is added to the latency of every batch of that many jobs, and a periodic script fires once per process start whatever its interval says. The worker's shell in the workers page also starts backed off rather than after the two minutes it otherwise takes, since a process due to be recycled cannot count on living that long: the first command of a session can wait up to 15s, later ones are immediate. For deployments that isolate executions by process lifetime rather than with nsjail; note that a container restart resets the process, not the container filesystem, so caches and /tmp survive it. The worker name is then derived from the hostname instead of being random, so the restarted worker keeps its row in the workers list (an agent worker keeps the row but restarts its job count). Use one worker per process: workers of one process share its environment, so the first to reach the limit shuts the others down too. |
Worker |
| WORKER_SUFFIX | None | Pins the last part of the worker name, which is otherwise random, so that a restarted worker keeps its row in the workers list. Only needed when several worker processes of the same worker group run on one host, since the name is derived from the hostname: give each of them a distinct value, as two processes sharing one must never happen. At most 64 letters, digits and underscores; anything else is refused at startup. | Worker |
| LICENSE_KEY (EE only) | None | License key checked at startup for the Enterprise Edition of Windmill | Worker |
| SLACK_SIGNING_SECRET | None | The signing secret of your Slack app. See Slack documentation | Server |
| COOKIE_DOMAIN | None | The domain of the cookie. If not set, the cookie will be set by the browser based on the full origin | Server |
| DENO_PATH | /usr/bin/deno | The path to the deno binary. | Worker |
| PYTHON_PATH | The path to the python binary if wanting to not have it managed by uv. | Worker | |
| GO_PATH | /usr/bin/go | The path to the go binary. | Worker |
| GOPRIVATE | The GOPRIVATE env variable to use private go modules | Worker | |
| GOPROXY | The GOPROXY env variable to use | Worker | |
| NETRC | The netrc content to use a private go registry | Worker | |
| PY_CONCURRENT_DOWNLOADS | 20 | Sets the maximum number of in-flight concurrent python downloads that windmill will perform at any given time. | Worker |
| PATH | None | The path environment variable, usually inherited | Worker |
| HOME | None | The home directory to use for Go and Bash , usually inherited | Worker |
| DATABASE_CONNECTIONS | 50 (Server)/3 (Worker) | The max number of connections in the database connection pool | All |
| SUPERADMIN_SECRET | None | A token that would let the caller act as a virtual superadmin superadmin@windmill.dev | Server |
| TIMEOUT_WAIT_RESULT | 20 | The number of seconds to wait before timeout on the 'run_wait_result' endpoint | Worker |
| QUEUE_LIMIT_WAIT_RESULT | None | The number of max jobs in the queue before rejecting immediately the request in 'run_wait_result' endpoint. Takes precedence on the query arg. If none is specified, there are no limit. | Worker |
| DENO_AUTH_TOKENS | None | Custom DENO_AUTH_TOKENS to pass to worker to allow the use of private modules | Worker |
| DISABLE_RESPONSE_LOGS | false | Disable response logs | Server |
| CREATE_WORKSPACE_REQUIRE_SUPERADMIN | true | If true, only superadmins can create new workspaces | Server |
| MIN_FREE_DISK_SPACE_MB | 15000 | Minimum amount of free space on worker. Sends critical alert if worker has less free space. | Worker |
| RUN_UPDATE_CA_CERTIFICATE_AT_START | false | If true, runs CA certificate update command at startup before other initialization | All |
| RUN_UPDATE_CA_CERTIFICATE_PATH | /usr/sbin/update-ca-certificates | Path to the CA certificate update command/script to run when RUN_UPDATE_CA_CERTIFICATE_AT_START is true | All |
| GOOGLE_APPLICATION_CREDENTIALS | None | (ee only) Credentials file for GCP Pub/Sub triggers that authenticate as the instance rather than through a gcloud resource (workspace admins only). Application default credentials also resolve the gcloud well-known file and the GCE metadata server. Workload Identity Federation files work with the file, url and aws credential sources; the executable source is not supported. |
Server |
Run a local dev setup
We recommend using Nix. See ./frontend/README_DEV.md for all options.
Frontend only
Uses the backend of https://app.windmill.dev with local frontend (hot-reload):
cd frontend
npm install
npm run generate-backend-client # or generate-backend-client-mac on Mac
npm run dev
Windmill available at http://localhost/
Backend + Frontend
See the ./frontend/README_DEV.md file for all running options.
- Start a local Postgres database using for instance the
start-dev-db.shscript which will make a database available atpostgres://postgres:changeme@localhost:5432/windmillThen run the migrations using the following command:This will also avoid compile time issue with sqlx'scargo install sqlx-cli env DATABASE_URL=<YOUR_DATABASE_URL> sqlx migrate runquery!macro. - (optional, linux only) Install nsjail and have it accessible in your PATH
- Install bun, deno and python3 (+ any languages you want to use), have the bins at
/usr/bin/bun,/usr/bin/deno, and/usr/local/bin/python3or set the corresponding environment variables. - (optional) Install the lld linker
- Go to
frontend/:npm install,npm run generate-backend-clientthenREMOTE=http://localhost:8000 npm run dev- You might need to set some extra heap space for the node runtime
export NODE_OPTIONS="--max-old-space-size=4096" - Create an empty
frontend/buildfolder usingmkdir frontend/build
- Go to
backend/:env DATABASE_URL=<YOUR_DATABASE_URL> RUST_LOG=info cargo run- You can specify any feature flag you want to enable, for example
cargo run --features pythonto enable the python executor.
- Windmill should be available at
http://localhost:3000
Contributing
At this time, we are not seeking outside contribution. Bug reports and feature requests remain very welcome, and small, trivially-verified PRs that fix a problem are still accepted. See CONTRIBUTING.md for the full policy.
Contributors
Copyright
© 2023-2026 Windmill Labs, Inc.






