CodeWorlds
Back to collections
Guide20 min readCodeWorlds Team

Windmill, scripts and flows as an internal tool

Windmill runs Python, TypeScript and Go scripts as flows with generated forms. Version 1.795.0, a licence split by compile flag, and the real limits.

Windmill, scripts and flows as an internal tool

Windmill takes an ordinary Python, TypeScript, Go or Bash script, reads its signature and turns it into a form, an endpoint and a flow step. The current version is 1.795.0, released on 22 August 2026. The licence is the most interesting part here, because the line between open and proprietary code runs through a compile flag rather than through a directory.

What Windmill actually does

The building block is a script, not a node in a graphical editor. You write a function, Windmill parses its signature and derives the input schema from it. Hence the fourteen separate parsers compiled to WebAssembly inside the command line package, one per language: windmill-parser-wasm-py, windmill-parser-wasm-ts, windmill-parser-wasm-go, windmill-parser-wasm-php, windmill-parser-wasm-java, windmill-parser-wasm-ruby, windmill-parser-wasm-rust, windmill-parser-wasm-csharp, windmill-parser-wasm-nu, windmill-parser-wasm-r and a few helpers. The input schema becomes a form, the request body for an endpoint, and the list of fields to fill in when the script is a step inside a flow.

The set of server-side languages can be read straight out of backend/Cargo.toml, because each one is a separate compile flag: python, rust, php, csharp, java, ruby, rlang, nu, plus the query engines mysql, mssql, oracledb, bigquery, snowflake, duckdb, with TypeScript going through deno_core. The all_languages meta flag switches them on together. If you build the image yourself and leave out duckdb, that language simply does not exist in that executable.

Above the scripts sit flows written in the OpenFlow format. A flow is a list of steps, where a step is either a reference to a script in the workspace or code embedded directly in the definition. On top of that come loops over a collection, branches, retries, sleeps and steps waiting for human approval. Triggers are schedules, HTTP endpoints, WebSocket, listening on Postgres, MQTT, AMQP and email. Kafka, NATS, SQS, GCP and Azure Event Grid sit in the ee_core flag group, meaning the paid edition only, and this matches the table on the pricing page.

The execution layer is workers reading a queue out of Postgres. In the repository's docker-compose.yml the same image runs with MODE=server or MODE=worker, and the native group additionally starts with NATIVE_MODE=true. The default database in that file is postgres:16, so knowing PostgreSQL matters more here than with a typical automation tool.

The licence, a splitter instead of a licence

The LICENSE file in the repository root is not a licence. It is a splitter, stating that the code is variously licensed: part under Apache 2.0 from the LICENSE-APACHE file, part under AGPLv3 from the LICENSE-AGPL file, and part of the enterprise features under a proprietary licence.

The split works like this. The python-client/, deno-client/, go-client/ and powershell-client/ directories are Apache 2.0, as are the OpenAPI files together with the OpenFlow specification. Everything else is AGPLv3 by default. Files under backend/ are AGPLv3 except snippets of code under the enterprise compile flag, and files under frontend/ are AGPLv3 except snippets that need a positive licence check to activate. Those exceptions are proprietary and commercial. The splitter adds explicitly that private and public forks must not contain that code.

So the line runs through a flag, not a directory. In practice there are two flags, visible in the meta flag section of backend/Cargo.toml.

backend/Cargo.toml
TOML
# backend/Cargo.toml, "Edition meta-features" section, excerpt
oss     = ["oss_core", "all_languages", "no_auth"]
ce_core = ["oss_core", "private", "operator"]
ce_rpi  = ["ce_core", "all_languages"]
ce      = ["ce_rpi", "jemalloc", "dind", "agent_worker_server"]
ee      = ["ce", "ee_core", "ee_server", "kafka-gssapi"]

The private flag pulls in modules that are absent from the public repository. The public tree contains files with an _oss suffix, and under that flag they re-export the ee modules. I checked under the v1.795.0 release tag: backend/windmill-common/src/ee.rs and backend/windmill-git-sync/src/git_sync_ee.rs both return a 404. The second flag is enterprise, which switches on paid edition behaviour and drags in the license flag. The oss set contains neither of them, the ce set contains private without enterprise, and the ee set contains both.

What the code behind the flag does is visible from the stubs alone. Below is an excerpt from backend/windmill-git-sync/src/git_sync_oss.rs.

backend/windmill-git-sync/src/git_sync_oss.rs
Rust
// backend/windmill-git-sync/src/git_sync_oss.rs, excerpt
#[cfg(feature = "private")]
pub use crate::git_sync_ee::*;

#[cfg(not(feature = "private"))]
pub async fn handle_deployment_metadata<'c>(
    _email: &str,
    _created_by: &str,
    _db: &DB,
    _w_id: &str,
    _obj: DeployedObject,
    _deployment_message: Option<String>,
    _skip_db_insert: bool,
    _renamed_from: Option<&str>,
) -> Result<()> {
    // comment in the original: git sync is an enterprise feature
    return Ok(());
}

Similarly, backend/windmill-api/src/ee_oss.rs holds a validate_license_key function that in the open variant returns an error saying the key cannot be validated in the community edition. In backend/windmill-common/src/ee_oss.rs sit the states LICENSE_KEY_VALID, LICENSE_OFFLINE_OVER_CU_CAP and LICENSE_OFFLINE_OVER_SEAT_CAP plus an OfflineMetadata struct with the fields v, kind, hash, seats and cu_limit. That shows an offline licence carries a seat count and a compute unit cap, and the instance itself watches whether it went over them.

On the front end the check is a single call. The frontend/src/lib/enterpriseUtils.ts file exports a setLicense function that calls SettingsService.getLicenseId() and stores the result in the enterpriseLicense store. The interface elements reading that store are the proprietary snippets from the splitter. You cannot list them reliably from the public repository, because they are ordinary conditions inside components, so the only practical inventory remains the comparison table on the pricing page.

Building from source with the oss flag gives you plain AGPLv3, because the splitter states outright that the binary compiled without the enterprise flag is open source under AGPLv3 terms. Checking what you built is indirect, since there is no endpoint printing the flag list. Three observations do the job. Starting in agent mode on a binary without the enterprise flag ends in a panic saying agent mode is only available in the paid edition. Entering a licence key ends in the error described above. The full text indexer reports that the tantivy library did not make it into this binary or image.

The most important consequence concerns images. The splitter states that the community edition available in images under ghcr.io/windmill-labs/windmill and in the binary releases on GitHub contains the AGPLv3 and Apache 2.0 files, but also non-public proprietary code. The ce_core set, which contains private, confirms this. The terms for that edition are its own: you may use all of its features for free within the limits and quotas set in the software, and you may distribute it as is, but you may not sell it, resell it, offer it as a managed service, modify it or wrap it without a separate agreement. The repository's .env file shows both image addresses: ghcr.io/windmill-labs/windmill:main for the community edition and ghcr.io/windmill-labs/windmill-ee:main for the paid one.

The answer to which licence you are under is therefore: it depends on where your binary came from. From the official image you are under Windmill Labs' own terms, not AGPLv3, and you may not modify that image. From your own build with the oss flag you are under AGPLv3 and you may modify it, minus the features that sit behind private.

When AGPLv3 obligations actually bite for an internal tool. Running the unmodified program inside a company requires nothing beyond keeping the licence notices. Section 13 of the licence covers making a modified version available to users over a network, and then those users are owed the corresponding source code. Employees clicking through the panel in a browser are such users. So if you patch the backend and expose it on the intranet, the patched source is owed to the people using it, and to nobody else. Your scripts and flows are not derivative works of the server, and the client libraries sit under Apache 2.0 precisely so that importing wmill in a script does not drag copyleft in. This is a reading of the licence text, not legal advice.

Version, packages and project health

Releases arrive almost daily. The releases feed shows 1.790.0 on 15 August and 1.795.0 on 22 August 2026, meaning nine releases in eight days, including the patch releases 1.792.1, 1.792.2 and 1.794.1. That cadence cuts both ways: fixes reach you quickly, but pinning the main or latest tag in production means the instance changes every day.

The windmill-cli command line tool has 765 versions in the npm registry, the latest being 1.795.0 published on 22 August 2026. No version is marked as deprecated. Besides the latest tag there is a second one, gitsync, pointing at 1.777.2-gitsync.0, so installing without naming a version always takes latest. Unpacked the package is roughly 3.8 MB, declares fifteen dependencies pinned to exact versions, including esbuild 0.28.0 and fourteen WebAssembly parsers. Those parsers clearly lag behind the tool itself: windmill-parser-wasm-py is at 1.782.0, windmill-parser-wasm-ts at 1.695.0, and windmill-parser-wasm-csharp, -java and -nu at 1.510.1. This is not a bug, only the effect of publishing a parser when it changes, but a dependency audit will show fifteen different version numbers for one family of packages.

There is one inconsistency in the npm package. The license field in the registry reads Apache 2.0, which is not a valid SPDX identifier, and the only licence file included is not the Apache text but the splitter from the repository root. The tool itself fits the spirit of the Apache 2.0 clients, yet the splitter does not name the cli/ directory explicitly. An automated metadata harvester will label the package Apache 2.0 and miss that the bundled file talks about three licences at once.

The wmill Python client on PyPI is at 1.795.0, has the licence field Apache-2.0, a single dependency httpx>=0.24 and no yanked releases. The image builds on rust:1.97-slim-trixie and node:24-alpine. The npm package declares no engines field, so your package manager will not warn you on an old Node.

Where the code lives and how you version it

Scripts, flows and apps live in Windmill's database, not in a repository. This is the same situation we described with Knock: business logic outside the repository means no change review, no blame history and no rollback by reverting a commit. Windmill keeps object versions in the database and lets you go back to a previous one, but that is history in a panel, not in Git.

The way out is the command line tool. It works against any instance, community edition included.

Code
Bash
npm install -g windmill-cli
wmill workspace add
wmill init

# pull the whole workspace into the current directory
wmill sync pull --yaml

git add . && git commit -m "workspace state before the change"

# push changes from the directory back to the instance
wmill sync push

# run a single script with input data
wmill script run u/alice/cleanup --data '{"days": 30}'

Sync behaviour is described by a wmill.yaml file in the root of the directory. The field names below come from the unpacked 1.795.0 package.

Code
YAML
defaultTs: bun
includes:
  - f/**
excludes:
  - f/scratch/**
skipVariables: false
skipResources: false
skipSecrets: true
skipScripts: false
skipFlows: false
skipApps: false
includeUsers: false
includeGroups: false
includeSchedules: true
includeTriggers: true
gitBranches: {}

A few details from that file. A missing defaultTs field produces a warning and makes bun the default TypeScript runtime. The syncBehavior field carries a sync behaviour version number, and the tool refuses to work when the file asks for a version newer than it supports, telling you to run wmill upgrade. Setting skipSecrets to true is a sensible starting state, otherwise secret values end up in the working directory. The gitBranches section ties branches to push settings, and codebases handles bundles built before upload.

This is where the licence comes in, because sync has two directions and only one of them is free. From repository to instance is a plain command line call inside a continuous integration job and works everywhere. The reverse direction, automatically pushing every deployment from the panel into a repository, is handled by handle_deployment_metadata, which in a build without the private flag does nothing and returns success. The non-public module adds enqueue_git_pull_job, reconcile_and_enqueue_pull, persist_auto_pull_state and record_auto_pull_failure there. The pricing table meanwhile states that git sync works for up to two users on the free self-hosted tier. There is no contradiction, but there is a conclusion: that free allowance exists in the official community image, which does carry the non-public code, and not in a binary built from public sources. The cut-off mechanism after the second user cannot be read at all, because it lives in closed code.

A practical arrangement for a team therefore looks like this: a development workspace for clicking around, wmill sync pull into the repository as the source of truth, change review in a pull request, and wmill sync push into the production workspace from continuous integration. The panel for pushing changes between workspaces and the deploy to production view are both marked paid in the table.

Pricing and limits

Without JavaScript the pricing page renders only the self-hosted tab. The comparison table in the raw markup carries the identifiers tier-free-selfhost and tier-enterprise-selfhost, while the cloud and white label tabs never appear in the content. I am not quoting figures for the cloud plan, because they are not there. The only trace of it is the page's structured data, listing an offer named Team at 10 dollars per seat per month. That entry appears nowhere in the visible part of the page, and an answer in the frequently asked questions section talks about a Teams plan billed on actual usage. I report both forms and flag the discrepancy.

The free self-hosted variant is the community edition with no cap on the number of executions. The limits are elsewhere: three workspaces, at most fifty users, at most four groups, ten users with single sign-on, a 10 GiB quota for workspace object storage, retention of job run details up to thirty days, and one hundred messages a day for the email trigger. Workspace forks and dev workspaces count towards the limit of three. Out of reach are audit logs, autoscaling, agent workers, concurrency limits, dedicated workers, external secret backends and triggers on Kafka, NATS, SQS, GCP and Azure.

The paid edition bills two things at once. Seats cost 20 dollars a month per developer and 10 dollars per operator, meaning a user who can run scripts, flows and apps but does not create them. Every unique user authenticated with an external JWT counts the same as an operator, with uniqueness counted per the triple of name or email address, scope and instance, active in the last thirty days. The second axis is compute units at 50 dollars a month each. One unit is two gigabytes of worker memory for a month. A two gigabyte worker is one unit, a worker with more than two gigabytes on self-hosted is two, the minimum billed per worker is half a unit, and a native worker always counts as one unit regardless of memory. Eight native subworkers make one unit.

The arithmetic of the default simulation on the page adds up to the cent: one developer at 20 dollars, two standard two gigabyte workers as two units at 100 dollars and eight native subworkers as one unit at 50 dollars gives three units and a total of 170 dollars a month. The offer card above the simulator, however, quotes a price from 120 dollars a month. That amount matches one developer and two units, but the page never explains it, so I treat 120 dollars as the advertised entry threshold and 170 as the result of the default configuration. Both numbers come from the same page.

The definition of an execution helps with comparisons: it is a single job lasting less than a second, every further second counts as another execution, and a flow execution is the sum of its steps. A job gets one virtual core and two gigabytes of memory. Going over your purchased terms does not kill the licence key immediately: you get thirty five days to sort it out before it expires. Usage is reported through telemetry, with compute units counted only from instances marked as production and seats counted across all instances, each user counted once. The Pro plan is available to individuals, businesses under ten employees and 250 thousand dollars of revenue, and seed stage startups, self-hosted only, while non-profits and universities get 60 percent off the paid edition. Payments go through Stripe. Asked about the company shutting down, the vendor answers that keys will be extended indefinitely, and an instance keeps running without an internet connection for as long as the key is valid.

Windmill and the alternatives

TraitWindmilln8nTemporalInngest and Trigger.devRetool
Building blocka script in a filea node in an editora function in application codea function in application codea component in an editor
Script languagesPython, TypeScript, Go, Bash, PHP, Java, Ruby, R, C#, SQLJavaScript and Python in code nodesdepends on the toolkitTypeScript and PythonJavaScript in fields
Where code executeson Windmill workerson the n8n instancein your processin your processin your application
Form for a non-programmergenerated from the signaturelimitednonenonethe main product
Public sourcesyes, with a split licenceyesyesyesno
Natural useinternal tools and periodic jobsintegrations between serviceslong running processesbackground jobs for an apppanels over a database

A sensible split looks like this. n8n wins when the task is clicking together integrations between ready-made services and nobody wants to write code. Temporal wins when a process runs for days, carries state and needs recovery guarantees after a crash, with the code staying inside your application. Inngest and Trigger.dev win when you need a background job queue for an application you already have and do not want a separate portal. Retool wins when a panel for the operations team is the goal in itself and closed source is not a problem. Prefect sits closer to Windmill in the orchestration layer, but aims at data pipelines and generates no interface for an end user.

Windmill fits the gap between those worlds: a programmer writes the code, but somebody else runs it and looks at it, through a form or a panel, and the whole thing can sit on your own server next to your other services, for instance through Dokploy. The price of that arrangement is one specific thing: the code lives in somebody else's database by default, and you have to deliberately organise its route into a repository.

Common mistakes

The first is assuming the official image is AGPLv3. It is not. It contains non-public code and falls under its own community edition terms, which forbid modifying and wrapping. If your legal team approved AGPLv3 while the deployment comes from the image, they approved something else.

The second is counting on git sync in a binary built from public sources. The function handling a deployment returns success there and does nothing, with no error in the log.

The third is planning triggers on Kafka, NATS or SQS on the free tier. Those flags belong to the ee_core group and are absent from the community edition, which the pricing table confirms.

The fourth is hitting the three workspace limit. Workspace forks and dev workspaces count towards the same three, so a layout split into development, staging and production consumes the whole allowance and leaves nothing for experiments.

The fifth is pinning the main tag in production. At nine releases in eight days the instance updates on every image pull. Pin an exact release number and raise it deliberately.

The sixth is relying on audit logs to establish who deployed what. The community edition has none, and job run details disappear after thirty days. Without git sync you have no change history either.

The seventh is running the npm package's license field through a licence scanner without opening the file. The field says Apache 2.0, while the bundled file describes three licences at once, one of them proprietary.

The eighth is pulling a workspace without skipSecrets. Secret values land in the working directory, and from there they reach a repository easily.

FAQ

Am I under AGPLv3 when I use the official Docker image?

No. The splitter in the repository says outright that the community edition from images under ghcr.io/windmill-labs/windmill and from the binary releases on GitHub also contains non-public proprietary code. The terms are its own: free use of all features within the limits set in the software, and distribution as is, without the right to sell, resell, offer as a managed service, modify or wrap. Plain AGPLv3 only comes from compiling yourself without the enterprise and private flags.

Do I have to publish the code of my scripts and flows?

No. Scripts and flows are your data running on the platform, not a derivative work of the server. The client libraries in python-client/, deno-client/, go-client/ and powershell-client/ are Apache 2.0 precisely so that importing them does not drag copyleft in. The obligation from section 13 of AGPLv3 concerns modified server code, and only towards people who use it over a network.

Does git sync work on the free tier?

In the official community image yes, with the two user limit stated in the pricing table. In a binary built from public sources without the private flag it does not work at all, because handle_deployment_metadata is a stub returning success. The reverse direction, pushing from a repository to an instance with wmill sync push, always works.

What does the paid edition cost for two developers and one worker?

By the seat and unit pricing that comes to 40 dollars for two developer seats plus 50 dollars for a standard two gigabyte worker, so 90 dollars a month. The offer card, however, quotes a price from 120 dollars a month and does not explain whether that is a floor. Treat 120 dollars as the opening figure for a conversation and confirm it with the vendor.

How do I check whether my binary contains proprietary code?

There is no endpoint printing the list of compile flags, so the check is indirect. Entering a licence key fails when the private flag is missing. Starting in agent mode panics when the enterprise flag is missing. The full text search indexer reports a missing tantivy library and points you at the paid image. The most reliable source stays the command you built with, meaning the contents of --features.

When is Windmill the wrong choice?

When you need processes running for days with full state recovery guarantees, because that is Temporal territory. When you want the logic to stay in the application repository with no separate portal, in which case a job queue fits better. When nobody on the team writes code, because a graphical editor gives them more. And when you plan to sell a product built on somebody else's image, because the community edition terms forbid it.

The source code and the licence splitter are in the repository on GitHub, while the paid terms and the simulator are on the pricing page.

Read next

We use cookies to enhance your experience on the site