* feat: attach text files to chat messages, read on demand via file tools
* fix: resolve name collisions and sync message files with the transcript
* refactor: render message file chips with the shared context badge
* fix: suffix same-name attachments, sync registry on compaction, bound file bytes
* feat: carry message files across compaction, drop them from the roster
* refactor: merge context, dom and file badges into one wrapping row
* fix: dedupe identical attachments and make badge list keys collision-proof
* fix: name carried files inside the collapsed summary instead of badges
* fix: serialize registry reconciliation and correct the attachment budget
* fix: reserve pending bytes so overlapping reads honor the attachment budget
* fix: share attachment byte budget across concurrent composers
The bottom composer and the edit box are both mounted while editing an
earlier message, but each enforced MAX_CONVERSATION_FILE_BYTES against
only its own staged files plus the transcript. Two attaches near the cap
could each pass independently and overflow the persisted transcript.
Each composer now publishes its staged bytes (committed attachments +
in-flight reads) to the manager, keyed per instance, and the attach-time
budget subtracts every other live composer's stage. A message an open
composer is editing is skipped from the transcript sum since that
composer's stage stands in for it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: charge an edited message at its persisted size until the edit commits
An edit is not committed until send, so the edited message's persisted
attachments return if the edit is cancelled. Substituting only the edit
box's (possibly emptied) stage let the always-mounted bottom composer
claim headroom that vanishes on cancel: remove the near-limit files in
the editor, fill the bottom draft, cancel, and the persisted transcript
overflows MAX_CONVERSATION_FILE_BYTES.
attachmentBytesExcluding now charges a message another composer is
editing at max(persisted size, editor stage), so freed space only
becomes available once the edit actually commits.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: preserve a message file's exact name when a session row clashes on rebuild
syncMessageScoped rebuilds message-scoped rows from the transcript through
collision-suffixing addFiles. Session rows load first (on restore), so one
holding a wanted name pushed the rebuilt message row to a "(2)" suffix while
the persisted prompt still referenced the bare name — get() then resolved the
reference to the session asset and the model read the wrong content.
Free the name from the conflicting session row before the rebuild so the
message row reclaims its exact reference. The rename is in-memory only: it is
deterministic and re-applied on every load, and the session roster is
regenerated live each send, so the session asset stays addressable under the
suffix without a persisted-record update.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: reserve resent files across the edit-resend gap
The edit box unmounts (dropping its staged-byte entry) the instant the user
submits an edit, but restartGeneration then awaits registry sync and beforeSend
before the optimistic bubble lands in the transcript. During that gap the
resent files were reserved nowhere, so the always-mounted bottom composer could
attach into the temporary headroom and the resend would then push the persisted
transcript past MAX_CONVERSATION_FILE_BYTES.
restartGeneration now reserves the resent files' bytes in shared manager state
before the transcript slice; sendRequest releases the reservation once it
installs the bubble (or restores the files to the composer on a pre-install
bail). The reservation bridges the gap so the budget stays honored throughout.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: re-target an in-flight index when a session row is renamed on rebuild
#freeNameForMessageRow renames a restored session row so a same-named message
attachment can reclaim its exact name. But the row's #indexFile, started under
the old name during restore, stamps via #patchFile(oldName, file) — after the
rename that no longer matches, leaving the row stuck 'indexing' so read_file
rejects it and search_files excludes it.
Re-kick #indexFile under the new name when the renamed row is still indexing;
the stale completion then no-ops (its name is gone).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: release resend reservation on local-command sends; surface compaction-orphaned files
Two follow-ups to the message-attachment work:
- A resend edited to /clear or /compact runs the local-command path and returns
before installing a bubble, so the #RESEND_KEY reservation set by
restartGeneration was never released and its bytes stayed charged, blocking
later attachments. Release it on every sendRequest path that exits before
install (via #releaseResendReservation).
- Drop-oldest compaction (summary fallback) removes API messages without a
summary, so a folded message's `## ATTACHED FILES` reference no longer reaches
the model even though the file stays readable. The roster omits message-scoped
files, so the model loses awareness of them. orphanedMessageFileNames() finds
message files whose only referencing message went negative-index, and the
roster now advertises them (summary compaction already carries its own).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: key the resend reservation per send so unrelated sends can't release it
The resend reservation used a single shared key, so a normal or concurrent
sendRequest released it at its own install/early-return even though it didn't
own it — dropping an in-flight resend's reservation and letting attachments
staged before the resend bubble lands under-count against the byte cap.
restartGeneration now mints a per-resend token, reserves under it, and threads
it through sendRequest as resendReservationKey; releases act only on that key.
A send with no token (every normal send) releases nothing.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: reserve the whole wanted-name set when freeing session rows on rebuild
Freeing a session row for a message-scoped rebuild suffixed it against current
rows only. With the transcript referencing both notes (2).md and notes.md and a
session row named notes.md, freeing notes.md renamed it onto notes (2).md — also
a wanted reference — so that message row cascaded to notes (3).md while its
persisted reference stayed notes (2).md, and read_file returned the session file.
#uniqueName now accepts a reserved set; the rebuild frees each session clash
clear of the entire wanted-name set, so every message row reclaims its exact
reference regardless of collision order.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: orphan summary-carried files when drop-oldest removes the summary
orphanedMessageFileNames classified summary rows as always-live, but a summary
carries its folded files' reference on its own API message. When summary
compaction succeeds and a later summarization fails, drop-oldest can remove that
API message, yet the files stayed off the roster — so the model lost their
reference even though they remained readable.
The summary display row now tracks its API index (slot 0 at creation, re-based by
drop-oldest); a negative index reads as "counterpart gone" and its files move to
the roster, mirroring user-message orphans. The index is used only for orphan
detection, never as a restart target.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: reserve outgoing file bytes for normal and queued sends too
The resend reservation covered edit/retry, but a normal or queued send has the
same gap: the composer (or queue) clears its files the instant sendRequest is
called, dropping the staged-byte entry, while sendRequest then awaits
regrantLocked()/refreshFolders() before the bubble lands. With a locked or slow
linked folder the composer stays enabled, so a fresh drop can spend the same
headroom and overflow the 5 MB cap once the first bubble installs.
Generalize the reservation: sendRequest mints a per-send token and reserves the
outgoing files' bytes just before attachment upkeep (reusing restartGeneration's
token when present), and releases it on install or any pre-install exit.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor: join attachment layers on a stable content-hash id
Sixteen review rounds kept finding the same bug family: a message file's
identity was its display name, joined by hand across the registry, the
transcript, the prompt reference, and the render keys — every same-name
collision or interleaved rebuild made two of those copies disagree.
Give each message attachment a deterministic id, attachedTextFileId(name,
content) — a synchronous pure-JS hash (works on plain-HTTP deployments) whose
exact value is pinned by test, since persisted transcripts reference it. The
prompt and roster list the id, the file tools resolve id-first (bare names
remain a fallback for legacy chats and session links), and pre-id transcripts
hydrate on load by recomputing the same hash — no migration state.
Names become display-only and may collide freely, which deletes the machinery
that defended them: the suffix-readback registration loop, session-row renaming
on rebuild (#freeNameForMessageRow, reserved-set #uniqueName), and the
reconciler's serialization guards (#syncSeq/#syncChain) — syncMessageScoped now
compares ids instead of awaiting blob text, so it is synchronous and cannot
interleave. A same-name clash within one draft gets a composer-local courtesy
rename before the id is minted.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: resolve bare names to session rows and scope id searches to one row
A bare name is the roster's namespace: session links are advertised by
filename and have no other handle, so a same-named message attachment (which
is addressed by id) must not shadow them. get() now resolves session rows
first, keeping the message-row name lookup only as the fallback for
transcripts persisted before ids existed.
search_files restricted an id reference by mapping it back to the display
name and letting the worker filter on it — same-named files were then
searched together under one label. The tool now passes the resolved row
itself, so an id-scoped search can only ever hit its own file.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: label search hits with resolvable ids, normalize attach batches at commit
An unscoped search_files reported hits by display name only. Names may
collide, so a hit could not be mapped back to the row that produced it —
a follow-up read_file on the bare name could return a different same-named
file. Rows carrying an id are now labeled `name (file id: …)` in hit lines,
so every hit names the reference that resolves to exactly that row.
addTextFiles normalized (deduped, courtesy-renamed) each file against a
snapshot taken during its read loop. Attach batches overlap, so a file
committed by another batch between reads escaped both checks — duplicate or
same-named unsuffixed entries in one message. Normalization now runs in the
single synchronous commit step against the live list (foldIntoDraft), where
nothing can interleave.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: dedupe renamed re-drops in foldIntoDraft, truncate queued file chips
"Same file dropped twice" means same original (name, content), but a
courtesy rename erases the original name — an identical re-drop then missed
the duplicate check and landed as a further-suffixed copy. The dedupe now
also matches entries whose suffix-stripped base name equals the read's name.
Queued file chip labels get min-w-0 so long filenames truncate inside
max-w-36 instead of overflowing.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: address cubic review — line counts, chip clicks, reference robustness
Five fixes from the cubic pass:
- The prompt advertised split('\n').length lines, one more than read_file
reports for newline-terminated files — textLineCount now matches the tool's
numbering (0 for empty, no phantom trailing line).
- Clicking a sent message's badge opened edit mode (the wrapper's
click-to-edit), unmounting the preview popover as it opened; the badge row
now keeps clicks to itself.
- resolve() accepts the composite label rosters and search hits print
(`name (file id: x)`) — models echo references verbatim, so the printed
form must resolve.
- fileToAttachedTextFile enforces MAX_TEXT_FILE_BYTES itself (raw size +
decoded byte length), so no ingestion path can persist an oversized
attachment past the composer's pre-check.
- Duplicate detection after a courtesy rename now uses an explicit sourceName
instead of inferring provenance from the display name — a user's real
`report (2).md` is not a rename of `report.md`.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: exact names win over label parsing, commit recheck uses decoded bytes
resolve() parsed any `name (file id: x)`-shaped reference as a printed label,
so a session file literally named that way became unreachable by its exact
name (the dead-id fallback resolved the base name instead). Exact id and
exact-name lookups now run before label interpretation.
Attachment admission and the pending reservation use raw File.size, but the
committed charge is the decoded UTF-8 length — malformed input decodes each
invalid byte to a 3-byte replacement character, so a file passing the 8KB
text sniff could inflate past the conversation cap. The synchronous commit
step now re-checks the live budget against decoded sizes
(admitWithinByteBudget) and drops what no longer fits, with the budget toast.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: normalize files folded into the queued message
Repeated submissions during a stream aggregate into one queued message, but
their files were concatenated raw: an identical re-attach duplicated its chip
and ate a slot (possibly displacing a distinct file at the eight-file cap),
and a same-name clash skipped the courtesy rename. The queue now folds new
files through the same commit normalization as the composer — the queued
entry is a message draft like any other.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* style: compaction boundary label uses text-normal text-2xs
* fix: fold provenance survives pass-through, dequeued files fold into the draft
foldIntoDraft recorded sourceName only for renames it performed itself, so a
file already courtesy-renamed by the composer lost its provenance when folded
into the queue — a later re-attach of the original escaped dedupe. Folds now
compose: the original source name rides through every fold, and dedupe
matches on it.
dequeueMessage restored queued files into a possibly-populated composer by
raw concatenation; prependText now folds them like every other draft
aggregation (dedupe, courtesy rename) before applying the cap.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* refactor: one MessageDraft owns the lanes that ship with a send
Review rounds kept finding the same P2 shape: an aggregation point where
files join a draft (composer commit, queue append, dequeue restore) that
forgot one of the draft rules — fold dedupe, courtesy rename, slot caps,
byte admission, lanes moving together. The rules existed only as convention
re-implemented per site.
MessageDraft owns them once: text, pastes, images, and text files live on
one object with addFiles (fold + optional decoded-byte admission + cap),
addImages (cap), prepend (restore-merge), replaceIfEmpty (occupied-guard
restore), and take (all lanes leave together). The composer holds a draft
instead of four state vars, and the queue is a draft behind the existing
queuedMessage/queuedImages/queuedFiles accessors — an aggregation point can
no longer skip a rule, because there is no raw array to concatenate into.
Deliberately not moved: @context and DOM picks (ContextManager owns their
lifecycle), the conversation byte budget's cross-composer ledger (store-side
follow-up), and sendRequest's options shape (it decomposes immediately and
is pinned by the manager test suite).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs: correct the drop-routing comment, condense the budget doc
* fix: address cubic review — name sanitization, drop hold, merge restores
Four fixes from the cubic pass:
- Attachment display names render into model-facing prompt blocks, and OS
filenames may legally contain control characters — sanitizeAttachmentName
strips them at attach and again at every prompt-render site (legacy names
predate the attach-time pass), so a crafted name cannot inject prompt lines.
- Drop routing awaits handle/entry resolution before it can call
addTextFiles; a send during that window landed the dropped files on the
next message. The drop handler now holds sending (holdSendForIngestion,
taken before the first await) until routing completes.
- Restoring a taken queue after a failed auto-send replaced the queued draft
wholesale, silently losing a follow-up queued during the preflight. Both
#restoreQueue and the unmounted-input requeue now merge via draft.prepend —
the taken entry's text lands above the newer follow-up.
- restartGeneration validated the API restart index only after reserving the
resend bytes and truncating the transcript, so a stale index threw with the
reservation leaked and the display transcript half-mutated. The index is
resolved and validated before anything is touched.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: restored drafts keep chronological priority, store names stay resolvable
A failed auto-send's restore folded the taken (older) draft's attachments
AFTER a follow-up queued during preflight, so at the slot caps the older
attachments silently dropped despite the text landing first — and only one
entry's pinned context survived. prepend() now puts the restored lanes ahead
(the cap drops the newest additions) and #restoreQueue unions both pinned
contexts by identity.
Session filenames were sanitized only at prompt render, so an id-less file
whose stored name carries control characters was advertised under a name
that resolve() could not match. Names are now sanitized at every store
row-creation site (attach, folder expansion, refresh, and persisted-row
restore for pre-sanitization records), making the advertised name the stored
name everywhere — render-site sanitization remains as defense in depth.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* chore: update ee-repo-ref to aaa6cb89b05b76139252c64f057e53b94d12ac60
This commit updates the EE repository reference after PR #680 was merged in windmill-ee-private.
Previous ee-repo-ref: 4c08634af953db5c1125b1fb03f5af211fe21db3
New ee-repo-ref: aaa6cb89b05b76139252c64f057e53b94d12ac60
Automated by sync-ee-ref workflow.
* chore: repin ee-repo-ref to main's eb3690a34b (aaa6cb89 needs the unmerged AmqpTrigger OSS companion)
* Revert "chore: repin ee-repo-ref to main's eb3690a34b (aaa6cb89 needs the unmerged AmqpTrigger OSS companion)"
This reverts commit a782e6f1c5.
* refactor: one name seam per scope — claim on the store, sanitize in the fold
Name rules (sanitize, uniquify, dedupe, resolve) lived as convention at every
creation site: four sites carried verbatim copies, two skipped uniquify
(restore, file placeholders — legacy names could collapse to one display
name), and the dedupe check compared the raw name against sanitized stored
names, so re-linking a control-char file added a spurious copy.
Store side: #claimName(raw) = sanitize + uniquify is now the only way a
session row gets its display name; addFiles derives the sanitized name once
at the top of the loop, so dedupe, uniqueness, and the stored row all see the
same string, while relPath and folder keep the raw on-disk keys they must
match. Draft side: foldIntoDraft sanitizes its reads itself instead of
assuming the reader did, making the fold self-contained. Folder names — raw
grouping keys by design — are sanitized at their model-facing render sites
(roster folder lines, not-found listing), mirroring rosterLine.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: a display-name collision is not a duplicate
Sanitizing names before the dedupe compare made two DISTINCT files whose raw
names sanitize identically look like re-links — the second was silently
discarded instead of claiming a suffixed name like the restore path does.
Dedupe now means "the same file re-linked": matching stats (size + mtime)
plus a matching name, pre-suffix sourceName, or raw path. #claimName records
the pre-suffix name when uniquifying renamed a row, so re-linking a suffixed
file's original still dedupes. A same-named file with different stats links
as its own row rather than being silently swallowed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: the re-link identity is the raw name, persisted with the row
Dedupe still compared sanitized display names, so two distinct raw names
that sanitize identically collapsed whenever their stats also matched — and
the provenance recorded for suffixed rows lived only in memory, so after a
reload re-linking the original behind a suffixed row stacked another copy.
sourceName now records the RAW pre-sanitization name on every session row
(display names lose information twice — sanitize, then suffix), rides the
persisted record, and is re-derived on restore. Dedupe matches stats plus
raw identity (sourceName, or relPath for folder children) and never compares
display names. #claimName returns to a pure name function; restore claims
the display name from the persisted raw identity.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.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 - Contributor's guide
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 |
| 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 |
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
Contributors
Copyright
© 2023-2026 Windmill Labs, Inc.






