* feat: add Deploy to Hub workspace settings tab * Init record logic * Fix wordings * Add publish-app drawer with per-app rate limit mock - Publish drawer on raw_apps/apps exposes public URL, copy-iframe, unpublish - Inline per-app rate limit config (req/min, burst, per-IP toggle) - Rename workspace settings "Default app" tab header to "Apps" to cover both default app and public rate limiting Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Simplify publish drawer to show workspace-wide rate limit only Drop per-app rate limit fields (req/min, burst, per-IP) — none of these are supported by the backend. The drawer now shows the existing workspace-level rate limit read-only with a link to edit it in Workspace settings → Apps. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Rename publish-app drawer wording to 'Share as iframe' 'Publish publicly' was ambiguous (publish to Hub vs make public URL). Use 'Share as iframe' for the button and drawer title, and 'Generate iframe' for the confirm action. Intro text now explicitly mentions iframe embedding use cases (Hub, docs page, own site). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Wire DeployToHub to real workspace data - Fetch apps, raw_apps, flows, scripts, resources via their services - Fetch workspace rate limit via WorkspaceService.getSettings - Share-as-iframe flips app policy.execution_mode to 'anonymous' via AppService.updateApp and resolves the real public URL via getPublicSecretOfApp + computeSecretUrl - Detect already-public apps from listApps execution_mode field - Filter out app_theme resources (noise, present in every workspace) - Hub bundle/version push and recording remain mocked (no backend yet) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Wire recordings to real jobs with run-preview UX - Recording flow now fetches the real schema, runs the job, and polls getCompletedJobResultMaybe to surface success/failure before saving. - Drawer shows a sticky status box (loader / success / failure) with a result preview, a job link, and an in-context Save CTA. - Only successful runs can be saved as a recording. Failures show the error and offer re-run. - Filter cache/state/app_theme internal resource types (mirrors workspaces_export.rs filter). - Added "What is a recording?" explainer banner above the items list. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Add draft/review state machine and submission gating - Phases: predeploy → draft → under_review → live, with workflow step indicator and contextual footer actions per phase - Bundle drawer collects name + readme before pushing the draft - draftItems snapshot frozen at deploy time; workspaceItems keep refreshing without affecting the draft - Folder MultiSelect lets users scope the bundle to one or more folders; empty = whole workspace - Submit-for-review disabled until every script and flow in the draft has a recording (progress bar + counter) - Recordings now run the real job and poll for success/failure; only successful runs can be saved - under_review phase locks editing, sharing, and recording - Dark mode variants on every coloured banner - Steps card shows the full 3-step process always, highlighting the current step Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Make recordings optional, encourage them for discoverability - Submit for review no longer gated on full recordings - Footer hint now frames recordings as boosting approval speed and public Hub featuring, not as a hard requirement - Progress card label switched from 'Recordings needed' to 'Recordings recommended' - Items without a recording display a yellow 'No recording' badge in every phase so the gap stays visible after submission Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Allow per-item selection inside the bundle scope - Items in predeploy now have checkboxes (all selected by default) - Select all / Deselect all act on the current folder filter - manualDeselected resets when the folder filter changes - Bundle button uses the selected count, disabled when zero - Draft snapshot keeps only the selected items - Checkboxes hidden in draft / under_review / live phases Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Add diff button once approved by admins * Small fix * Nits * fix(deploy-to-hub): paginate workspace list and cancel stale record polls - loadWorkspace fetches all pages instead of capping at 100 items per kind - pollJobUntilComplete now bails when recordRunSeq advances (new record target, re-run, or drawer close), preventing late completion of a previous run from overwriting current state Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * perf(deploy-to-hub): parallelize public-app URL resolution resolvePublicUrl now runs once per anonymous app via Promise.all instead of serially inside the items loop, removing N round-trips from initial tab load. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(indexer): tell admins when ingress routes search to wrong pod (#9274) * [ee] fix(indexer): tell admins when ingress routes search to wrong pod When the IndexReader is absent on the pod handling a search request but another pod is actively holding the indexer lock, the EE handler now returns a tailored error pointing at the ingress/load-balancer configuration instead of the generic "indexer not running" message. The indexer status endpoint reads the DB lock so it reports "running" from any pod, but search endpoints need the in-memory IndexReader that only exists on the lock holder. In multi-replica deployments this looks like the indexer is healthy but every search 404s. Companion: windmill-labs/windmill-ee-private#TBD Fixes WIN-1968. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: update ee-repo-ref to eb18d7b4c0e37fea3f6e1e2cc44e0fddd74ff817 This commit updates the EE repository reference after PR #586 was merged in windmill-ee-private. Previous ee-repo-ref: 7dd43d1850813071cc18ba49ba090583e7321f4b New ee-repo-ref: eb18d7b4c0e37fea3f6e1e2cc44e0fddd74ff817 Automated by sync-ee-ref workflow. --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com> * feat(cli): add `wmill init prompts` and custom override slot (#9266) * feat(cli): add `wmill init prompts` and custom override slot Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(cli): replace init prompts with refresh prompts + AGENTS.md/AGENTS.cli.md split Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(cli): dedupe claude skills via @-includes and add prompts freshness check Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(cli): drop migration-choice flags from `refresh prompts` Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(cli): add 'Running and previewing local changes' section to AGENTS.cli.md Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(cli): write full skill content to .claude/, drop @-include wrapper Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(cli): reconcile CLAUDE.md the same way as AGENTS.md Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(cli): address PR review nits — argv parsing, lazy import, comment detection, error propagation Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat: add yolo mode for ai chat tools (#9258) * feat: add yolo mode for ai chat tools * nit * fix: align chat footer controls * feat: add ai chat autonomy modes * feat: add autonomy mode dropdown * fix: highlight yolo autonomy icon * fix: auto accept flow edits * fix: hide unsupported autonomy modes * fix: handle auto-accept flow editor races * fix(debugger): add non-root user support to Dockerfile (#9277) Mirrors the main Windmill Dockerfile pattern: creates a windmill user (UID/GID 1000) and makes cache/work directories world-writable so the image runs cleanly under Kubernetes securityContext.runAsNonRoot or runAsUser: 1000 without permission errors on Bun, pip, or windmill cache writes. Fixes WIN-1969 Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(ai): enforce RLS and scope check on user-supplied X-Resource-Path (#9276) * fix(ai): enforce RLS and scope check on user-supplied X-Resource-Path The AI proxy handler accepts an X-Resource-Path header to override the configured workspace AI provider. When supplied, the handler loaded the resource value from the resource table using the root DB pool with no resources:read scope check, so any authenticated workspace user could point X-Resource-Path at a restricted AI resource (e.g. one in a folder they cannot read) and the proxy would use that resource's provider credentials for the outbound AI request. For user-supplied resource paths, now require resources:read:{path} scope and fetch the resource through user_db.begin(&authed) so RLS enforces the same folder/group boundary as the resource API. The RLS- scoped $var: resolution stays in place as defense in depth. The admin-configured workspace/instance ai_config path is unchanged. Fixes WIN-1971 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(ai): regression test for X-Resource-Path RLS enforcement Cover all four cases: - non-admin pointing X-Resource-Path at a restricted resource is rejected - non-admin pointing it at a resource they own still works - admin can point it at any resource - workspace-configured proxy flow (no X-Resource-Path) is unchanged Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat: add userdraft listing primitives (#9268) * feat: add userdraft listing primitives * fix: cancel stale userdraft discard writes * docs: remove global ai userdraft plan * feat(nsjail): optional disk-backed /tmp via instance setting (#9272) * feat(nsjail): optional disk-backed /tmp via instance setting * test(nsjail): unit-test tmp mount resolver and narrow visibility * refactor(nsjail): switch tmp backing to select + conditional UI * ui(nsjail): make tmpfs the visible default in /tmp backing select * fix(nsjail): refuse preexisting jail_tmp to block symlink escape * fix(nsjail): allow jail_tmp reuse on sequential nsjail calls Codex flagged that python/ruby/rust executors invoke nsjail twice per job_dir (install then run). The previous resolver treated any preexisting jail_tmp as hostile and silently fell back to tmpfs on the second call, so disk-backed mode never reached the main script run for those langs. Use symlink_metadata().is_dir() to distinguish a real directory left by an earlier call in the same job_dir (safe to reuse) from a symlink or other entity (still refused, as the codebase-tar escape requires). Also loosen the frontend visibility predicate: only hide nsjail settings when job_isolation is explicitly 'none' or 'unshare', so deployments that enable nsjail via DISABLE_NSJAIL=false with no DB setting can still see the controls. * chore(main): release 1.706.0 (#9270) * chore(main): release 1.706.0 * Apply automatic changes --------- Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com> * fix(nsjail): gate unix-symlink test behind cfg(unix) for Windows build (#9280) The disk_backed_refuses_preexisting_symlink_at_jail_tmp test calls std::os::unix::fs::symlink directly, which doesn't exist on Windows targets. Without a cfg gate, `cargo check --tests` fails on Windows with E0433. Other symlink call sites in this crate (php_executor, bun_executor, rust_executor, etc.) already follow this pattern. Fixes WIN-1972 Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Reduce slim image vulnerability surface (#9279) * Reduce slim image vulnerability surface * chore(docker): drop apt-get upgrade -y from slim images apt-get upgrade hurts build reproducibility (same Dockerfile + same commit at different times produces divergent images) and trips hadolint DL3005. The freshness it buys is dominated by simply rebuilding against the periodically-refreshed debian:bookworm-slim base image. The --no-install-recommends and apt-list cleanup wins are kept. --------- Co-authored-by: Ruben Fiszel <ruben@windmill.dev> * fix(git-sync): bump to hub/28234 with stateless gpg.program wrapper (WIN-1974) (#9282) * fix(git-sync): revert LATEST_GIT_SYNC_SCRIPT_PATH to hub/28230 to restore GPG-signed deploys (WIN-1974) hub/28231 (PR #9230) is the "thin" script that hands the actual `git commit` to the CLI's hidden `sync git-deploy`. The hub script still does the GPG setup (import key into a fresh GNUPGHOME, dummy `gpg -bsau` to warm the agent passphrase cache, then `git config user.signingkey` + `commit.gpgsign` locally), but the commit no longer runs in the same `git_push` flow — it runs minutes later inside the CLI after workspace API resolution, zip pull, file extraction, and lockfile autofill. By the time the spawned `git commit` asks gpg-agent for the cached passphrase, the cache state is no longer reliable (or the spawned `gpg` ends up talking to a fresh agent), so signing fails non-interactively with `gpg failed to sign the data`. hub/28230 is hub/28217's in-script logic rebuilt with windmill-cli@1.703.3: the GPG setup and the in-script `sh_run("git commit ...")` happen back-to-back in `git_push`, so the cache is always fresh. It preserves wm_deploy / fork branch behavior, the EE deployment-callback `main()` signature is unchanged, and the only min-version check in EE (`is_script_meets_min_version(28103)`) is comfortably below 28230 — so this revert is safe. Forward fix (separate PR): publish a new thin script that, alongside the existing GPG setup, writes a `gpg.program` wrapper using `--pinentry-mode loopback --passphrase-file` so signing is independent of the agent's cache state. Re-bump past 28231 then. Fixes WIN-1974 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(git-sync): check in source-of-truth for the next hub script (gpg.program wrapper) This is the script that will be published to hub.windmill.dev once verified on a customer GPG-signed deploy. It replaces hub/28231's agent-cache pre-warm (`gpg -bsau` with --passphrase) with a stateless gpg.program wrapper + chmod-600 passphrase file. Every git-invoked gpg call goes through the wrapper, which always uses --pinentry-mode loopback (and --passphrase-file when a passphrase exists). Signing no longer depends on gpg-agent having a cached passphrase by the time the CLI's `git commit` runs — which closes WIN-1974. Not wired in yet: LATEST_GIT_SYNC_SCRIPT_PATH stays on hub/28230 until this script is uploaded and the new hub id is known. This file is checked in so the diff is reviewable, future bumps have a source of truth, and a CLI regression test can `cat` it for fixture parity. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(frontend): skip format/pattern validation for $var/$res/$jsonvar references in ArgInput A resource field with a `pattern` constraint (e.g. the gpg_key.private_key field, whose pattern enforces a `-----BEGIN PGP PRIVATE KEY BLOCK-----` prefix) rejects values like `$var:u/me/gpg-private-key` with an "invalid format" error in the resource editor — even though `$var:`/`$res:`/`$jsonvar:` are placeholders the backend resolves at runtime, not the actual string that needs to match the regex. Bail out of all format/pattern checks (email, ipv4, ipv6, uuid, custom pattern) when the value is one of these references. Required/numeric bounds/array checks still apply since they're shape-level, not regex. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(git-sync): bump LATEST_GIT_SYNC_SCRIPT_PATH to hub/28234 (gpg.program-wrapper fix) hub/28234 is the forward fix for WIN-1974: replaces hub/28231's agent-cache pre-warm (which became stale by the time the CLI's `git commit` ran) with a stateless `gpg.program` wrapper that uses `--pinentry-mode loopback` (and `--passphrase-file` when a passphrase exists) on every gpg invocation. Bundled CLI is windmill-cli@1.705.0. Verified via reproducer at /tmp/git-sync-diff/test-gpg-fix.sh: deliberately killing gpg-agent between GPG setup and `git commit` reproduces the customer's `gpg failed to sign the data` error verbatim under the old flow, and the wrapper signs through it. Holds for passphrase-protected keys, split-subkey [C]+[S] layouts, and unprotected keys. Drops the local source-of-truth copy (`hub-scripts/`) — hub is canonical now that 28234 is published. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(git-sync): drop verbose comment above LATEST_GIT_SYNC_SCRIPT_PATH The git history (this PR) carries the why; the constant name + value carry the what. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(cli): wmill sync git-deploy stops committing; caller owns commit+push (#9284) Single contract for the deployment-callback path: the CLI does branch checkout + pull, the caller (hub script in production, test in test) does git add + commit + push. This restores the WIN-1974 invariant — GPG setup and `git commit` run back-to-back in the same process, so the agent's pre-warmed passphrase cache is still warm at sign time — without needing a `--skip-commit` flag for the hub case and a default "also-commit" for everything else. Same behavior in every call site. Changes: - sync.ts: drop the gitSyncDeployPush call from pull()'s deploy path (both the onlyCreateBranch fast-return and the post-pull commit). `gitSyncDeployPush` stays exported for any caller that wants the same commit/push semantics — just not invoked by the CLI subcommand. - gitsync_promotion.test.ts: e2e test now does its own git add + commit + push after `wmill sync git-deploy`, mirroring what the hub script does in production. Same regression coverage (wm_deploy branch created in Case A, main untouched; main updated in Case B, no new wm_deploy). CLI typecheck unchanged (two pre-existing TarAsZip errors at lines 2578/3307, present before this PR). All 743 unit tests still pass. The accompanying hub script (option-C — CLI for branch+pull, script for commit+push) lives at /tmp/git-sync-diff/sync-script-to-git-repo-windmill.option-C.ts. Once published, a follow-up bumps LATEST_GIT_SYNC_SCRIPT_PATH to its id. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * bump git sync to 28236 * fix: fork compare visibility for non-admins and stale-token superadmins (#9283) * fix: use fork-scoped authed for fork visibility in compare_workspaces * test: add EE end-to-end repro for fork rename visibility * chore: restore concurrency_locks sqlx cache lost in cleanup * test: add regression for stale-superadmin-token fork visibility bug * chore: update sqlx cache for new test queries * chore(main): release 1.706.1 (#9281) * chore(main): release 1.706.1 * Apply automatic changes --------- Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com> * feat: add wmill job rerun subcommand (#9275) * feat: add wmill job rerun subcommand * feat: add wmill job restart subcommand for flow restart-at-step * chore(system_prompts): point plugin skills sync at plugins/windmill/ (#9287) * chore(system_prompts): point plugin skills sync at plugins/windmill/ The plugin checkout's plugin folder is being renamed from `plugins/windmill-code-plugin/` to `plugins/windmill/` to shorten the slash-command namespace and align with the matching Cursor plugin layout. Paired with windmill-labs/windmill-claude-plugin#8. That PR must merge first so the next sync run finds the new folder. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(system_prompts): update plugin-dir example to plugins/windmill Co-authored-by: centdix <centdix@users.noreply.github.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> Co-authored-by: centdix <centdix@users.noreply.github.com> * fix(cli): wmill sync pull updates wmill-lock.yaml for raw apps (#9289) * fix: flow recording teardown crash + rename package to @windmill-labs/components (#9288) * fix: guard against null recording during FlowRecordingReplay teardown Navigating away from a flow recording inside a workspace file-tree view threw `TypeError: Cannot read properties of null (reading 'flow')` from FlowGraphViewer once during the teardown tick. Svelte 5 compiles child component props as live getters that close over `$$props.recording.flow`. When `recording` flips to null on the parent's navigation, an outer `{#if !recording?.flow}` doesn't stop those getters from firing one more time as derived effects re-evaluate before the unmount lands — so the getter dereferences null and throws. Fix at the two layers where the deref actually happens: - FlowRecordingReplay: use `recording?.flow` at the binding sites (FlowViewer + graph-snippet FlowGraphViewer) so the compiler emits an optional-chained getter, and guard the snippet branch with `{:else if recording?.flow}` so it doesn't mount when there's nothing to show. - FlowGraphViewer: finish the optional chaining the rest of the file already used everywhere else (`flow?.value?.skip_expr`, `flow?.value?.cache_ttl`, `flow?.schema`). When the upstream binding returns undefined during teardown, the graph degrades to an empty frame instead of crashing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: rename package to @windmill-labs/components - frontend/package.json: rename `windmill-components` → `@windmill-labs/components` - frontend/publish.sh: drop the in-place sed rename dance; the checked-in name now matches what's published, so `npm run package && npm publish` is enough - frontend/package-lock.json, system_prompts/auto-generated/prompts.d.ts: regenerated by `npm run package` under the new name Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(flows): restore Variables and Resources in flow editor prop picker (#9290) The design system overhaul in888837431caccidentally dropped the fallback condition that displayed the Variables and Resources sections in the prop picker by default. After that commit, these sections only appeared when the user typed `variable.` or `resource.` in their expression, which meant they effectively disappeared from the flow editor's prop picker for most users. Restore the previous behavior by showing the sections when no input match is active (the equivalent of the old `!filterActive` clause). Fixes WIN-1976 Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(auth): tighten token-owner fallback for unscoped tokens (WIN-1978) (#9293) * fix(auth): reject unscoped tokens with cross-workspace forged owners (WIN-1978) An unscoped token (workspace_id IS NULL) whose `owner` field references a user, group, or unprefixed value that is not present in the target workspace must not authenticate. The previous fallback in the `u/<username>` branch granted `(is_admin=false, is_operator=true)` when no `usr` row matched in the target workspace, letting a token holder who could mutate the `token` table cross workspace boundaries with operator privileges. The `g/<groupname>` branch likewise silently accepted any group name as a "group user", and the no-prefix branch granted operator state from arbitrary owner strings. Both are now rejected unless the owner matches a real user/group membership in the target workspace. Adds an integration regression covering all three forged-owner shapes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: drop integration regression for auth fallback The test added in the previous commit relies on a sqlx::query! that requires offline-cache regeneration; removing per code-review preference to keep this PR scoped to the auth-layer fix. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(ResourceEditor): don't reset state when `selected` reverts to undefined (#9295) The bootstrap effect tracked `selected` via its early-return check, so any time `selected` flipped back to `undefined` it would re-run and reinitialize `states[effectiveWorkspace]` to empty — wiping user input. This happens in the React SDK consumer: reactify re-syncs all Svelte props on every React render, and since `selected` isn't passed through, `$props()` reverts it. Move the `selected !== undefined` check inside the existing `untrack` so the effect only tracks `effectiveWorkspace`. Bootstrap still runs once on mount; subsequent `selected` flips no longer retrigger it. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(secret-backend): pass DB to Vault migrations + show failure details (#9292) * [ee] fix(secret-backend): pass DB to Vault migrations + surface failure details Companion to windmill-ee-private fix for WIN-1977. The HashiCorp Vault migration always failed under JWT/OIDC auth because the migration constructed VaultBackend without a DB, so every secret hit "Database connection required for JWT authentication". Creating new secrets worked because the runtime path passes the DB. Frontend: when failed_count > 0, the toast and console now show the per-secret failures (path + error, capped at 5 with "...and N more") instead of just aggregate counts. Fixes WIN-1977 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: update ee-repo-ref to 14315067c083d3361512de621b12e41dbe3b017d This commit updates the EE repository reference after PR #587 was merged in windmill-ee-private. Previous ee-repo-ref: 390ed6c851b1915f0b492897c663f8058477680f New ee-repo-ref: 14315067c083d3361512de621b12e41dbe3b017d Automated by sync-ee-ref workflow. * fix(secret-backend): escape failure fields and use <br> in migration toast Address CI review on PR #9292: - P1 (cubic/codex): backend-supplied workspace_id/path/error are now HTML-escaped before being interpolated into the migration toast, which renders through {@html processMessage(...)} in Toast.svelte. This prevents stored XSS via secret paths or backend errors that contain markup. '/' is intentionally left intact so the toast's path-highlight regex still tags workspace paths. - P2 (pi): swap '\n' for '<br>' so multi-line failure lists actually break in the toast instead of collapsing to a single run-on line. - Extend the same per-secret failure surfacing (toast + console.error) to the Azure Key Vault and AWS Secrets Manager migration handlers via a shared reportMigrationFailures() helper so all six migration paths report identically. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com> * nit react-sdk resource editor * sdk_resource * make `selected` resilient + snapshot args for React (#9298) * fix(ResourceEditor): make `selected` resilient + snapshot args for React Two issues surfaced via the React SDK (reactify wrapper re-spreads Svelte props on every host re-render): 1. The bindable `selected` prop transiently resets to undefined on each re-spread, flipping `current` through undefined and unmounting the form (input loses focus on every keystroke). Rename the prop to `selectedProp` and derive `selected = selectedProp ?? effectiveWorkspace` so the fallback insulates the component without effects. 2. The onChange dispatch passed `current.args` (a `$state` proxy) directly, so React consumers diffing by reference or JSON.stringify saw the same value forever, and the effect only tracked the args reference (not nested mutations). Wrap with `$state.snapshot` to deep-track and emit a plain object. The bootstrap effect is also restructured: it no longer writes `selected` (the derived handles defaulting) and now guards on `selected in initialStates` so workspace flips remain idempotent. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(ResourceEditor): declare effectiveWorkspace before use in selected Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * remove unused workflow * feat(typescript-client): add deleteS3File + optional workspace arg on S3 helpers (#9300) * feat(typescript-client): add deleteS3File + optional workspace arg on S3 helpers Customer-requested ergonomics for the TypeScript SDK: - New `deleteS3File(s3object, workspace?)` wrapper around the existing `HelpersService.deleteS3File` (backend endpoint is already there). Saves callers from having to either hand-roll `denoS3LightClientSettings()` + AWS SDK calls, or wire up `HelpersService` directly. - `denoS3LightClientSettings`, `loadS3File`, `loadS3FileStream`, `writeS3File`, and the new `deleteS3File` all gain an optional trailing `workspace?: string` parameter that falls back to the `WM_WORKSPACE` env var via `getWorkspace()`. Mirrors the calling convention customers already expect from helpers like `getVariable` / `runScript`. `build.sh` and `build.jsr.sh` are updated to export `deleteS3File` from both the NPM and JSR entry points. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: regenerate system_prompts auto-generated for new S3 helpers `python system_prompts/generate.py` after adding deleteS3File and the optional workspace param to the existing S3 helpers, so the agent-facing docs (CLI skills, TS SDK prompt, script skills) reflect the new signatures. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(github-app): hide cloud-only UI on self-managed + admin assignment UI (#9299) * feat(github-app): hide cloud-only UI on self-managed + admin assignment UI Two related UX fixes for the GitHub App self-managed (GHES) integration: 1. On self-managed instances, the per-installation Export button and the "Import installation from other instance" section in the workspace UI both hide. Both round-trip a JWT carrying only {installation_id, account_id} with no github_base_url, so they would produce broken cloud-style installs on a self-managed instance. The previous Export attempt also failed with "No JWT token received from server" because self-managed installs store an empty JWT by design. 2. New "Workspace assignments" panel in instance settings (GhesAppSettings.svelte) that auto-discovers installations of the configured GHES App and lets the super-admin assign them to specific workspaces. Workspace users without GitHub permissions no longer need to install the App themselves — the admin provisions the link from instance settings. Admin-provisioned installs show a "Provisioned by admin" badge in the workspace UI and can only be removed by the super-admin from instance settings. Backend support is in the EE companion PR windmill-labs/windmill-ee-private#588. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: update ee-repo-ref to da5189cf69a453de3855057f41be0d84e5910707 This commit updates the EE repository reference after PR #588 was merged in windmill-ee-private. Previous ee-repo-ref: d959b83ce413ad531e9cc28e0f8199cdecb73a31 New ee-repo-ref: da5189cf69a453de3855057f41be0d84e5910707 Automated by sync-ee-ref workflow. --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com> * chore(main): release 1.707.0 (#9285) * chore(main): release 1.707.0 * Apply automatic changes --------- Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com> * feat(queue): per-workspace fairness cap on the shared cloud worker pool (#9303) * feat(queue): cloud-only per-workspace fairness cap on the shared worker pool On `app.windmill.dev` the cluster runs a single default worker group, so a single workspace flooding the queue can degrade quality of service for everyone else. This adds an opt-in mechanism that caps any single workspace at a configurable share of the shared worker pool when it has been dominating cluster activity for more than a configurable window. Detection signal counts both currently-running jobs and jobs completed in the rolling window, so it catches workspaces hogging slots with long jobs **and** workspaces spamming many tiny jobs (where no individual job's started_at is old, but throughput share dominates). Refresh is coordinated cluster-wide via a single UPDATE on `background_task_state`: the `WHERE updated_at < now() - interval` predicate combined with row-level locking means only one process per refresh cycle actually runs the aggregation, regardless of fleet size. Every other process gets the freshly written value in the same round trip via `UNION ALL ... LIMIT 1`. Heavy aggregation rate stays at ~0.2-0.5 qps for the whole cluster. Pull queries are split: the existing query string and its bind shape stay bit-identical to today, so the planner keeps using the same indexes when fairness is off or no workspace is currently capped. A separate `WORKER_PULL_QUERIES_FAIRNESS` adds `AND workspace_id <> ALL($2::text[])` and is only materialized while the feature is enabled. Hard-gated to `CLOUD_HOSTED=true` + BASE_URL host == app.windmill.dev at three layers: frontend `cloudonly: true`, API setter rejection in `set_global_setting_internal`, runtime check in `fairness_active`. Settings are exposed under Jobs in the instance-settings UI; defaults are off so the change is a no-op for self-hosted. Two-pass pull guarantees no worker idling: if every queued job belongs to a capped workspace, the second pass uses the unmodified pull queries. Cap re-asserts on the next refresh. Fixes WIN-1982 * fix(queue): address CI review findings on workspace fairness Six fixes from the four-reviewer cross-check on #9303: 1. **Aggregation evaluation (Codex P1).** The previous `INSERT ... ON CONFLICT DO UPDATE WHERE updated_at < ...` had the heavy `v2_job_queue ∪ v2_job_completed` aggregation inlined into `VALUES`, which Postgres evaluates for every contender to build the proposed row — losing the "one heavy aggregation per cycle cluster-wide" property the design advertises. Split into three small statements: (a) cheap claim with constant `VALUES`, (b) winner-only `UPDATE ... SET value = jsonb_build_object('overloaded', <agg>)` (Postgres only evaluates `SET` per row matching `WHERE`, so losers never compute the aggregation), (c) read for everyone. Heavy query now truly runs ~0.2-0.5 qps cluster-wide regardless of fleet size. 2. **Numeric setting wraparound (cubic P1).** `u64 as u32` and downstream `u32 as i32` could silently flip sign and feed `make_interval(secs => -N)`, making `now() - interval` a future timestamp and disabling the completed-jobs half of the activity signal. Clamp `duration_secs` to [1, 86400] and `min_total_jobs` to [0, u32::MAX] before storing. 3. **`/instance_config` bypass (cubic/Claude/Codex P2).** Bulk config endpoint sidestepped `set_global_setting_internal`'s gate; a self-hosted superadmin could persist `workspace_fairness_*` rows via the bulk path. Mirror the per-key check in `set_instance_config` upsert flow. 4. **DB error coerced to false (Claude P2).** `load_workspace_fairness_enabled` collapsed `Err(_)` to `false` and unconditionally swapped the atomic — a transient DB blip during notify-event propagation toggled the feature off cluster-wide (and triggered a `store_pull_query` rebuild precisely when load is highest). Now propagates the error so the atomic stays at its prior value. 5. **Refresh failure cooldown (Claude P2).** Storing `0` removed the rate limit entirely; every subsequent pull spawned a new refresh task. Leave `LAST_REFRESH_MICROS` at `now_us` (already written by the CAS) so the natural interval acts as the cooldown. 6. **Visibility + duplication (Pi P2).** Mark `make_pull_query_fairness` as `pub(crate)`. Move the duplicated `BASE_URL host == app.windmill.dev` parser into `windmill-common::worker::is_cloud_production_host` and share it between the API setter and the runtime path. Verified locally: - `POST /api/settings/global/workspace_fairness_enabled` → 400 (per-key gate) - `PUT /api/settings/instance_config` with fairness key → 400 (bulk gate) - `cargo check --workspace --features=private,enterprise,quickjs` — clean Refs WIN-1982. * fix(queue): second round of CI review nits on workspace fairness Three issues raised by the Codex/Claude re-review of commit0b38ff2: 1. Non-cloud deletes were rejected (Codex P2). The cloud gate ran before the Null / empty-string deletion branches in both `set_global_setting_internal` and the bulk `set_instance_config`. A self-hosted instance that inherited stale `workspace_fairness_*` rows from a cloned cloud DB couldn't clear them through the API — the rows stayed in `global_settings` and continued to show up in the YAML export. Now the gate only blocks upserts; Null / empty-string deletes pass through on any host. 2. Deleted numeric knobs kept stale runtime values (Codex P2). When a cloud admin cleared `workspace_fairness_max_percent`, `..._duration_secs`, or `..._min_total_jobs`, the notify-event fired but the numeric loaders ignored `Ok(None)` and left the previous in-memory value pinned until process restart. Loaders now distinguish three outcomes: - `Err(_)`: transient — leave atomic alone (preserves the previous-round fix). - `Ok(None)` / `Ok(Some(invalid))`: reset to the documented default. - `Ok(Some(valid))`: clamp and store. Defaults are extracted to `WORKSPACE_FAIRNESS_*_DEFAULT` constants kept in sync with the `AtomicU32::new(...)` initialisers in `windmill-common/src/worker.rs`. 3. `fairness_active` was `pub` with no cross-crate caller (Claude nit). Tightened to module-private. Verified locally on this non-cloud instance: POST .../workspace_fairness_enabled body=null → 200 (delete passes) POST .../workspace_fairness_enabled body=true → 400 (set blocked) PUT .../instance_config {} → 200 (no-op passes) PUT .../instance_config with fairness key → 400 (bulk set blocked) Skipped the partial index on `v2_job_queue WHERE running = true` that Claude flagged as a residual nit — queue stays under 50k rows per the operator's measurement, so the seq-scan cost (~10 ms × 0.5 qps = ~0.5% of a DB core) is well below the noise floor and the index isn't worth the maintenance cost on job transitions. Refs WIN-1982. * chore(main): release 1.708.0 (#9304) * chore(main): release 1.708.0 * Apply automatic changes --------- Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com> * feat: add copy button to Path component (#9311) * feat: plug global chat drafts into userdraft (#9291) * refactor: move global chat drafts to userdraft * feat: share script and flow drafts with editors * feat: share trigger drafts with editors * feat: share raw app drafts with editor * feat: share resource drafts with editors * docs: rename global chat drafts copy * feat: add global chat draft discard tool * fix: resolve global chat editor draft paths * fix: remove editor draft path resolver * feat: track live editor drafts in userdraft * fix: snapshot live userdraft reads * chore: checkpoint pending global draft changes * fix: address global draft review issues * fix: defer raw app draft persistence * docs: remove pr investigation docs * fix: persist live global draft writes * refactor: move bedrock proxy handling to windmill-ai (#9309) * refactor: move bedrock proxy handling to windmill-ai * docs: track ai refactor follow-ups * fix(auth): filter resource/variable listings by token scope (WIN-1981) (#9302) A token scoped to a single resource (e.g. `resources:read:u/alice/foo`) could call `GET /api/w/{w}/resources/list_search` and receive `path` and `value` for unrelated resources in the workspace. Route-level scope checks only validate `domain:action`; per-resource handlers do a `check_scopes` against the path, but the listing endpoints did not — leaking integration credentials, API keys, and other secrets stored as resource values to narrowly-scoped tokens. Add `build_scope_path_predicate` to `windmill-api-auth` (mirrors `check_scopes` semantics but parses the token's scopes once, suitable for filtering many rows). Apply it to `list_search_resources`, `list_resources`, `list_names` (resources) and `list_variables` (non-secret value leak), so a scope-restricted token only ever sees the paths it is authorized to read. Unscoped tokens and tokens whose only scopes are `if_jobs:filter_tags:*` are unaffected. Includes regression tests covering: unscoped, tag-filter-only, single-resource, wildcard, wrong-domain, and write-implies-read. Fixes WIN-1981 Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * audit-log workspace-fairness cap transitions (#9306) * feat(queue): audit-log workspace-fairness cap transitions When the cloud per-workspace fairness mechanism adds a workspace to the capped set or releases one, write `workspace_fairness.capped` / `workspace_fairness.uncapped` audit-log entries to the affected workspace. The cluster admin can review the full timeline from the `admins` workspace audit view with `all_workspaces=true`; per-workspace owners see their own events in their normal audit list. Only the per-cycle refresh winner emits entries (matching where the heavy aggregation runs), so a fleet of N workers does not produce N duplicates per transition. The diff is computed against the value already in `background_task_state` rather than the winner's in-memory cache, so a freshly-restarted process winning the claim does not spuriously emit "newly capped" entries for workspaces that were already capped before it started. Audit writes are best-effort: failures are logged via tracing and do not abort the refresh cycle. Fixes WIN-1984 * feat(queue): scope fairness audit to admins workspace + queue-metrics pane - Write `workspace_fairness.capped` / `workspace_fairness.uncapped` to the `admins` workspace (was: per-affected-workspace) with the affected workspace_id moved to the `resource` field. Cluster admins now get the full timeline in one place without `all_workspaces=true`. - Add `GET /workers/workspace_fairness_events` returning the last 100 events. Cloud-gated (returns `[]` on non-cloud) and devops-only. - Add a `WorkspaceFairnessEvents` Section to the Queue Metrics drawer, rendered only when `isCloudHosted()` is true. Shows time / event badge / workspace / parameters with a refresh button. Fixes WIN-1984 * feat(ai-chat): expand chat question answers (#9310) * feat(ai-chat): align footer bar + DropdownV2 mode/autonomy selectors (#9308) * feat(ai-chat): align footer bar, use DropdownV2 for mode/autonomy selectors Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(dropdown): add `selected` item prop rendering a trailing check Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * style(ai-chat): add small spacing between chat input and footer bar Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(ai-chat): always offer the 3 autonomy options in the auto-accept picker Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(ai-chat): default autonomy mode to auto-accept on Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(ai-chat): use Button component for footer dropdown triggers Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * style(ai-chat): use a hand icon for the auto-accept-off autonomy state Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * style(ai-chat): use subtle Button variant for mode and model selectors Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * style(ai-chat): tighten spacing between input and footer bar Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(ai-chat): reword autonomy levels as ask/auto-accept/bypass permissions Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(button): add 2xs unified size with tighter padding Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(ai-chat): compact footer bar — 2xs buttons, AtSign context icon, short Yolo label, discreet model Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * style(ai-chat): widen the permission selector dropdown Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(dropdown): group shortcut + selected check to avoid ml-auto collision Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(ai-chat): cover getPersistedAutonomyMode default; clarify default comment Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(raw_apps): tab-based editor surface with split-with-preview (#9273) * feat(raw_apps): custom tab system for source / runnable / preview Replaces the fixed split-pane layout with a tab bar inside the editor area. Each frontend file is a tab, each selected runnable is a tab, and the Preview is pinned to the right (non-closable). Tabs are an alternative discoverability surface to the sidebar — both stay functional, but tabs make navigation viable on small screens with the sidebar collapsed. A "Split with Preview" toggle in the tab bar's trailing slot pairs the active tab with the preview side-by-side for wide-screen multitasking. The toggle hides when Preview is already the active tab. The UI Builder, runnable editor, and preview iframe all stay mounted across tab switches (toggled via `display`) — no bundler restarts, no preview state loss, no editor remounts. - New common/tabs/DraggableTabs.svelte: reusable tab strip with drag-reorder (@windmill-labs/svelte-dnd-action), pinned-left/right slots excluded from the drag zone, hover-revealed X close, middle- click close, keyboard navigation (arrows / Enter / Backspace), and a `trailing` snippet for inline toolbar add-ons. - raw_apps/RawAppEditor.svelte: - Tab state (`tabs`, `activeTabId`, `splitWithPreview`) lives in Windmill. Persisted in localStorage keyed by workspace + app path. - Sidebar file clicks (`handleSelectFile`) and runnable selection (`selectedRunnable` via `bind:`) are mirrored into tabs via an effect — the sidebar interaction is otherwise untouched. - Listener augmented: `setActiveDocument` backfills tabs for files VS Code opens by itself; `setFiles` / `runnables` updates drop stale tabs. - Bundler / inspector / rebuild toolbar moves into the tab bar's trailing slot — always visible regardless of active tab. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(raw_apps): modern tab styling + resizable split-with-preview Two polish passes on the new tab system: DraggableTabs styling: - Remove the bottom border on the tab strip + the accent-coloured border-b-2 on the active tab. The active tab now shares the surface background with the content area below it, so the boundary visually "disappears" — modern IDE-style tabs. - Inactive tabs sit on the darker surface-secondary tab strip and get a subtle right separator so they don't blur into each other. Split-with-Preview is now a real resizable Splitpanes: - The content area is rendered as a Splitpanes (always), with the source/runnable slot on the left and the preview iframe on the right. The user can drag the divider to adjust the ratio when the "Split with Preview" toggle is on. - Iframes never remount across single↔split toggles — pane sizes are driven reactively from (activeTabKind, splitWithPreview), not by adding/removing the Splitpanes itself. - The user's preferred split ratio is remembered while they're dragging and reapplied next time split is enabled. - The inner splitter is CSS-hidden in single mode so the toggle button stays the single canonical way to flip layouts. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(raw_apps): split mode moves preview tab into the right pane Cleaner mental model for split-with-preview. Instead of "split the active tab + always keep the Preview tab around", the Split toggle now physically moves the Preview tab out of the bar and into a permanent right pane. When the user toggles split off, the Preview tab reappears in the bar like any other tab. - New `displayedTabs` derived: filters out the Preview tab when splitWithPreview is on, so the user sees only file/runnable tabs in the bar and a dedicated preview pane on the right. - `toggleSplit` redirects the active tab to the most recent file/runnable when the user toggles split on with Preview active, so they don't end up staring at an empty left pane. - Split toggle is now always visible — the user can flip both ways. The button label flips between "Pin preview to the right" and "Move preview back into a tab" to reflect what's about to happen. - reorderTabs preserves the Preview tab in the underlying `tabs` array even though it's filtered out of the drag set in split mode. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(raw_apps): VS Code-style "Preview" header on the right pane In split mode, the right pane now shows a small "Preview" tab-styled header anchored at its top-left — making the layout read like a real VS Code editor split, where each group has its own tab bar. - Header appears only when `splitWithPreview && activeTabKind !== 'preview'` (i.e. when the right pane is meaningfully separate from the left's content). In single mode with preview active, the right pane is the only thing visible and the main tab bar already labels it. - The header uses the same styling as an active tab: `bg-surface` on a `bg-surface-secondary` strip, h-8, text-xs, no border. - An X button next to the label toggles split off — equivalent to closing the editor in VS Code's split view (preview goes back to living as a tab in the main bar). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(raw_apps): VS Code-style symmetric tab bars per pane Restructure the editor area so each pane is a self-contained "editor group" with its own tab bar at the top. The Splitpanes is now the topmost element — the divider runs floor-to-ceiling, splitting both the tab bars and the content. Layout (left pane = source / runnable, right pane = preview): - Left pane top: DraggableTabs (file/runnable tabs, Preview tab when split is off) + Split-toggle in the trailing slot. - Right pane top: a custom preview header — "Preview" label styled like an active tab on the left + the preview-affecting toolbar (bundler, inspector, rebuild) on the right. - Each pane independently sized via Splitpanes; iframes + the runnable panel stay mounted and toggled via `display` so state survives every transition. Trade-off: in single-mode with Preview active (paneA=0), the left tab bar is hidden along with the left pane. To switch back to a file tab the user uses the sidebar — which is exactly the discoverability surface tabs were meant to complement, not replace. Button placement by semantic ownership: - Layout control (Split toggle) — left side, with the editor. - Preview-affecting controls (bundler, inspector, rebuild) — right side, with the preview. No close-X on the right; the Split toggle on the left is the canonical way to flip layouts. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(raw_apps): keep tab bar visible when Preview is active in single mode The "VS Code-style" restructure put the tab bar inside the left Pane. When activeTabKind became 'preview' in single mode, the left pane collapsed to width 0 and the entire tab bar disappeared with it — leaving the user with no way to switch back to a file tab except via the sidebar. Move the main tab bar back above the inner Splitpanes (full width, always visible). The preview pseudo-header stays inside the right pane, carrying the bundler / inspector / rebuild toolbar. The splitter only goes through the content area below the tab bar, which is acceptable given how much friction the disappearing-tabs edge case caused. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(raw_apps): per-pane tab bars with mirrored single-mode lists Replace the single tab bar above the inner Splitpanes with one DraggableTabs per pane. Splitter now goes floor-to-ceiling through tabs AND content in split mode. In single mode both bars mirror the full tab list, so the visible pane always carries every tab — fixes the bug where activating Preview hid the tab strip. Clicking Preview while in split mode is a no-op (Preview is permanently visible in the right pane). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(raw_apps): polish tab strip and sync editor font to text-xs * feat(raw_apps): move logs overlay onto the preview pane * refactor(splitpanes): extract pixel-aware minSize helper * fix(raw_apps): tab hydration loads correct file; closeTab in split mode * fix(raw_apps): lazy-mount UI Builder iframe + add dev:ui-builder script * feat(raw_apps): default split view, blue preview tab, fix dnd ghosting * fix(raw_apps): remove 1px splitter sliver beside preview in single view * fix(raw_apps): tab scrollbar on hover, fix thumb height + resize staleness * refactor(raw_apps): don't persist tab/split layout in localStorage * refactor(raw_apps): derive pane sizes + binding setter instead of effects * style(raw_apps): trim verbose comments * feat(raw_apps): accept appendLogs delta from the UI Builder iframe * fix(raw_apps): exit inspect mode on Escape * fix(raw_apps): Escape clears lingering inspector selection after pick * style(raw_apps): accent-selected styling for active tab, bg-surface strip * fix(raw_apps): address PR review nits (drop debug log, timer/reorder/pane-setter, dev script restore) * fix(raw_apps): clear inspector overlay on the preview iframe, not the source * style(raw_apps): neutral tab look (surface-tertiary/text-emphasis selected, text-hint idle) * chore(raw_apps): bump bundled ui_builder to 61b6fdd --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(raw_apps): bump bundled ui_builder to b4f6219 (#9314) * skip workspaced-route duplicate checks on cloud (#9305) * fix(settings): skip workspaced-route duplicate checks on cloud The pre-write validation hooks for `app_workspaced_route` and `http_route_workspaced_route` query the DB for cross-workspace duplicates and fail the save when any are found. On cloud both `custom_path_exists` (apps) and `route_path_key_exists` (HTTP triggers) already scope lookups by `workspace_id` regardless of these settings, so duplicates across workspaces are expected and the validation has no runtime meaning. The result was that any cloud super-admin attempting to save instance settings with these toggles set to false received `Duplicate HTTP route paths detected` even though the setting has no effect on cloud routing. Fixes WIN-1983 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(error): render JsonErr as readable text and return 400 `Error::JsonErr` previously rendered through `#[error("Error: {0:#?}")]`, leaking Rust's `Debug` output (`Object { "error": String(...), "details": Array [...] }`) into the HTTP response body, and was bucketed into the catch-all 500 branch in `IntoResponse`. The result was a 500 status with a wall of Rust debug syntax in the toast — confusing and user-hostile. - Bucket `JsonErr` into 400 (Bad Request): every current call site (workspaced-route duplicate checks, OAuth client errors, etc.) is a client/validation issue, not an internal server fault. - Add `format_json_err_message` which surfaces the `error` field as the headline, summarises `details` (with a `- key=value` per entry), and pretty-prints the rest as JSON for unknown shapes. The frontend toast now reads e.g. Duplicate HTTP route paths detected - route_path=a, workspace_id=admins, http_method=post - route_path=a, workspace_id=starter, http_method=post Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(toast): preserve newlines and escape HTML in multi-line errors The toast renders via `{@html processMessage(message)}`, so server-side error bodies that span multiple lines (e.g. the duplicate-route response from the settings endpoint) collapsed into a single line because HTML treats consecutive whitespace (including `\n`) as a single space. When the message contains a newline, escape HTML first (defends against injected markup in server error bodies) and convert `\n` to `<br />` so multi-line errors stay readable in the toast. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fixup: address CI review feedback - toast.ts: escape HTML unconditionally. The previous gate on `\n` left single-line server error bodies unsafe under {@html}, which cubic flagged as P0. The path regex below only inserts a `<span>` around a `u/...` or `f/...` capture that can't contain HTML metacharacters, so escaping the whole input is the simpler and correct fix. - error.rs: add unit tests pinning the rendered shape of `format_json_err_message` (error+details, error-only, truncation cap, non-object fallback to pretty JSON). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(service-accounts): allow choosing role at creation time (#9307) * [ee] feat(service-accounts): allow choosing role at creation time Previously, service accounts were hardcoded to operator and could not be used as the CLI sync user since they had no write access. They also only counted as 0.5 seat each. This change: - Extends `NewServiceAccount` to accept optional `is_admin` / `operator` (defaults to `operator=true` for backward compatibility). - Exposes a role picker in `AddUser.svelte` when creating a service account (Operator / Developer / Admin). - Lets admins update a service account's role from the user list (it used to be locked to "Operator" with a tooltip). - Updates the OpenAPI spec + regenerates the frontend client. A developer/admin service account counts as 1 seat under the existing seat-cap logic (operators stay at 0.5). Companion PR on windmill-ee-private updates the `INSERT INTO usr` to honour the chosen role. Fixes WIN-1985 * [ee] feat(service-accounts): wm_deployers opt-in for Dev role When creating a service account with role=Developer, surface a toggle "Add to wm_deployers" (recommended). Members of wm_deployers can deploy on behalf of other users — the typical setup when the service account is used as the CLI sync / CI deploy identity. - `NewServiceAccount` gains an optional `add_to_deployers` flag. - Frontend defaults the toggle to on but only shows it under Developer (admins have it implicitly; operators can't deploy). - Tooltip links to docs.windmill.dev "Run on behalf of". Companion EE PR updates the handler to INSERT into usr_to_group for wm_deployers when the flag is set. Refs WIN-1985 * chore: update ee-repo-ref to 974ed42067d9f63acb42332b671b8c01ffd4b625 This commit updates the EE repository reference after PR #589 was merged in windmill-ee-private. Previous ee-repo-ref: f7dbc3cc2ba21c396f4828881e3b9d9ab6f50c69 New ee-repo-ref: 974ed42067d9f63acb42332b671b8c01ffd4b625 Automated by sync-ee-ref workflow. * [ee] fix(service-accounts): unhardcode role in superadmin user list Two review issues from the merged #9307 / #589: 1. P1 — The global Users tab in #superadmin-settings still pinned every service account to "Operator". Now it shows the actual role (Admin / Operator / Developer), derived from the SA's usr row. - `list_users_as_super_admin`: replaced `true as operator_only` with the real `operator` value, and added `is_workspace_admin` from the row (NULL for password users since their admin status is per-workspace). - `global_whoami`: when the email belongs to a service account, look up its real `operator` / `is_admin` instead of pinning to operator. - `SuperadminSettingsInner.svelte`: drop the hardcoded "Operator" badge; render Admin / Operator / Developer using the new fields, matching the workspace-level view. 2. P2 — Regenerate the bundled `openapi-deref.{yaml,json}` so the `createServiceAccount` body (now exposing `is_admin`, `operator`, `add_to_deployers`) and the new `GlobalUserInfo.is_workspace_admin` field show up at runtime in `/api/openapi.{yaml,json}`. Bumps `ee-repo-ref.txt` to the EE follow-up that adds the offline seat-cap check on `create_service_account`. Refs WIN-1985 * chore: update ee-repo-ref to b7a6068c1f3dc845e012959268b2426f0de4d697 This commit updates the EE repository reference after PR #590 was merged in windmill-ee-private. Previous ee-repo-ref: 0b1307c21d1bfd6fb43a03c2ba39d2a8bf8e6470 New ee-repo-ref: b7a6068c1f3dc845e012959268b2426f0de4d697 Automated by sync-ee-ref workflow. --------- Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com> * fix(jobs): authorization bypass in only_result job updates (WIN-1980) (#9301) * fix(jobs): enforce anonymous-only guard on `only_result` job updates The `jobs_u/getupdate/{id}` and `jobs_u/getupdate_sse/{id}` endpoints accept `only_result=true`. In that branch, `get_job_update_data` queried the result solely by (workspace_id, job_id) and skipped the `created_by == "anonymous"` check that the non-only_result path and adjacent unauthenticated endpoints apply. An unauthenticated requester who learned a private job UUID could therefore retrieve that job's output. Hoist the guard to the top of `get_job_update_data` so both branches are covered. Fixes WIN-1980 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor: fold `created_by` check into existing only_result queries Avoids the extra `SELECT created_by` round-trip per call by joining `v2_job` once in the two queries that handled the unauth path and checking inline. Behavior is identical to the prior commit; the SSE polling loop now does one query per poll instead of two for unauthenticated callers. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor: cache anonymous_verified across SSE polls Replace the LEFT JOIN approach with an upfront `SELECT created_by` guarded by a new `&mut bool anonymous_verified` parameter that mirrors `early_return_suppressed`. The SSE polling loop now performs the auth check exactly once per stream rather than per poll, and the data SQL reverts to its original form so authenticated callers pay no extra cost. `created_by` cannot change after job creation, so caching the verification across polls is safe. Cost matrix: - Authed (any path): 0 extra queries - Unauthed one-shot: 1 extra query (unavoidable) - Unauthed SSE: 1 extra query at stream start, 0 per poll Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor: scope anonymous check to only_result branch The non-only_result branch already enforces the `created_by` check via its main query, so a top-level hoisted check duplicated work for unauthenticated default-path callers. Move the check inside the `if only_result.unwrap_or(false)` block — exactly where the bypass lives — and leave the non-only_result path untouched. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(raw_apps): surface UI Builder build errors over the preview pane (#9316) * feat(raw_apps): surface UI Builder build errors over the preview pane Companion to the matching change in the UI Builder repo (see linked PR), which stops rendering the build-error overlay over the VS Code editor iframe and instead emits a `buildError` postMessage on every build (message: undefined on success to clear). Listen for that message on the existing window message handler (already source-gated by the UI Builder iframe), store it in a `buildError` $state, and surface it in two places: * A red banner over the preview iframe, sibling to the existing logs overlay (`top-12 left-2 right-2 z-20` so it clears the tab bar) — failures appear right where the user looks for the rendered output. * The Preview tab's icon and label tint red (`text-red-600 dark:text-red-400`, matching the existing error convention in raw_apps) — important in single-tab mode where the preview pane is collapsed to 0px and the banner would be hidden. Done by mapping `leftPaneTabs` / `rightPaneTabs` through a small `tintPreviewOnError` helper so the source-of-truth `tabs` array is untouched (DnD, ordering, fallback selection keep using the original previewTab object). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(raw_apps): use Alert component for the build-error banner Replace the hand-rolled red div with the shared `Alert` component (`type="error"`, `title="Build failed"`). The error text stays in a `<pre>` child so multi-line bundler output keeps its formatting, with `max-h-60` so a long error never takes over the whole preview pane. The absolute-positioned wrapper (`top-12 left-2 right-2 z-20`) and the `role="alert"` move to that wrapper so the Alert component itself stays unstyled at the call site. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(raw_apps): solid bg-surface backing behind build-error Alert The Alert's error background is semi-transparent in dark mode (`bg-red-900/40` in `common/alert/model.ts`), so the preview iframe shows through when the banner is laid over it. Add a `::before` pseudo on the Alert root with `bg-surface` (matched `rounded-md`, `-z-10` so it sits behind the red bg) to give it a solid plate. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(raw_apps): isolate banner stacking context, DRY tab tint chain Two small follow-ups from review: * Add `isolate` to the build-error banner wrapper so the `before:-z-10` pseudo's stacking context is pinned locally — it works today because `position: absolute` + `z-20` creates one, but `isolate` makes the dependency self-documenting and survives a future refactor that removes the explicit `z-20`. * Extract `tintTabs = (ts) => ts.map(tintPreviewOnError)` so the two `$derived` blocks for leftPaneTabs / rightPaneTabs read identically. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(raw_apps): trim build-error overlay comments Per review feedback. Keep only the load-bearing facts (bg-surface backs the Alert's translucent red, isolate pins the pseudo stacking, the `message: undefined` clear convention) and drop the prose context that duplicated what the code already shows. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(raw_apps): bump bundled ui_builder to 00c9834 Brings in the postMessage emission from windmill-labs/windmill-code-ui-builder#9 (merged) so this PR's host listener actually receives `buildError` events. SHA verified against the R2 artifact. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(main): release 1.709.0 (#9312) * chore(main): release 1.709.0 * Apply automatic changes --------- Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com> * add cli-sync workspace snapshot/load scripts (#9322) * feat(fixtures): add cli-sync workspace snapshot/load scripts * fix(fixtures): address review nits (env var password, mktemp, dead refs) * fix(fixtures): address CI review (SIGPIPE, JSON escaping, doc/code drift) * feat(queue): stochastic admission + EE availability of workspace fairness algorithm (#9321) * refactor: unify AI provider credentials (#9317) * refactor: use provider credentials for worker builders * refactor: resolve api proxy credentials directly * fix: lazy load frontend eval modes * fix(websocket-trigger): honor HTTPS_PROXY/HTTP_PROXY/NO_PROXY (#9324) * feat(websocket-trigger): honor HTTPS_PROXY/HTTP_PROXY/NO_PROXY (WIN-1988) `tokio_tungstenite::connect_async` opens a raw TCP socket and ignores the standard outbound-proxy env vars, so deployments behind a forward HTTP proxy can't reach the WebSocket endpoint and Test Connection times out after 30s. Add a small `proxy` module that resolves the right proxy URL for the target host (HTTPS_PROXY for wss://, HTTP_PROXY for ws://, NO_PROXY exclusions, ALL_PROXY fallback, lowercase variants), opens an HTTP CONNECT tunnel when one applies, and hands the resulting TcpStream to `client_async_tls_with_config` for the TLS + WS handshake. Direct connect remains the default when no proxy env is set. Unit tests cover NO_PROXY matching, proxy URL parsing (including IPv6 literals and basic-auth userinfo), and the CONNECT handshake itself against an in-process fake proxy (success, basic-auth header, 407 rejection). Fixes WIN-1988 * refactor(websocket-trigger): reduce blast radius and reuse existing logic Follow-up to the proxy support change. Three things: 1. Skip the new code path entirely when no proxy is configured. `connect_async_with_proxy` now checks the env-var snapshots up front and delegates straight to `tokio_tungstenite::connect_async` if neither `HTTP_PROXY` nor `HTTPS_PROXY` is set. Same fall-through applies when proxy env is set but `NO_PROXY` excludes the host or the proxy URL doesn't parse. Non-proxied deployments now exercise exactly the previous code path. 2. Move the `NO_PROXY` / `HTTP_PROXY` / `HTTPS_PROXY` env-var snapshots from `windmill-worker::worker` into `windmill-common`. The worker's `PROXY_ENVS` static now reads from there, and the websocket trigger reads from the same source — one place reads the env, one source of truth for both call sites. 3. Replace the hand-rolled proxy-URL parser with `url::Url::parse` (already a workspace dep, used across the codebase). Half the LoC and handles edge cases (userinfo percent-encoding, IPv6 literals, path/query stripping) via the well-tested crate instead of by hand. All 13 proxy unit tests still pass. `cargo check` is clean. * fix(websocket-trigger): unbreak EE build + trim proxy tests - Re-export `NO_PROXY` / `HTTP_PROXY` / `HTTPS_PROXY` from `windmill-worker::worker` (via `pub use windmill_common::...`) so the EE `otel_tracing_proxy_ee` module's `use crate::{HTTPS_PROXY, ...}` resolves like it did before. Fixes the `check_ee_full` / `cargo_test` CI failures from the previous commit. - Trim the proxy tests to one un-ignored canary (`http_connect_tunnel_sends_well_formed_request_and_unwraps_stream`) that exercises the actual on-wire CONNECT handshake plus byte-perfect tunnel passthrough. The NO_PROXY-matching, URL-parsing, and edge-case tunnel tests are kept under `#[ignore]` for manual debugging (`cargo test -- --ignored`) since they're either delegated to `url::Url::parse` or trivial string matching — low ROI on every CI run. * chore(main): release 1.710.0 (#9323) * chore(main): release 1.710.0 * Apply automatic changes --------- Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com> * fix: improve workspace fairness * chore(main): release 1.710.1 (#9327) * chore(main): release 1.710.1 * Apply automatic changes --------- Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com> * prevent windows backend tests from running out of disk space (#9325) * ignore flaky fairness regression tests in CI (#9328) `fairness_ignores_zombie_running_rows` and `fairness_ignores_concurrency_suspended_rows` panic intermittently in CI (both Linux and Windows runs). Mark them `#[ignore]` until the underlying flakiness is resolved. * feat(cli): add object-storage commands and flow test-step (#9326) * feat(cli): add object-storage commands and flow test-step * docs(cli): clarify flow test-step doesn't recurse into aiagent tools * fix(cli): correct failure step id in docs, handle bare flow.yaml path * refactor(cli): fold flow test-step into flow preview --step (#9330) * fix(queue): duration-weighted workspace fairness signal (#9329) * fix(queue): bump EE ref to include worker_ping fairness signal The current ee-repo-ref.txt pointed to 31cda7c (an unrelated merge commit on the asset-graph-view-ee branch) instead of ddc9e80, which contains the workspace-fairness fix that switches the active-share signal from v2_job_queue.running=true to worker_ping. As a result cloud was still computing overload off the legacy signal, so a workspace with many in-flight/suspended flows (lancom01-prod, with 799 suspended flows × 3 v2_job_queue bookkeeping rows each = 2397 running-true rows) was flagged as 95% of cluster activity despite consuming zero worker slots. Bumping to ddc9e80 picks up the worker_ping-based signal, which naturally excludes (a) suspended jobs (no worker pinging them), (b) zombie running-rows from dead workers, and (c) flow/flownode orchestration rows that never run on a worker in the first place. * test(queue): seed v2_job rows + realistic durations for fairness helpers The new duration-weighted fairness algorithm joins v2_job_queue and v2_job_completed to v2_job for the `kind` filter (excluding flow bookkeeping) and reads `duration_ms` for the completed contribution. Update the test helpers to mirror that schema: * `insert_completed` now inserts a matching v2_job row (kind=script) and writes `duration_ms = 1000` with a 1-second [started_at, completed_at] interval, so each completed row contributes ~1 worker-second when fully inside the refresh window. * `insert_queued` likewise pre-inserts v2_job, sets `started_at` to NOW() - 1s when running=true (so running rows contribute ~1 worker-second by the time the refresh runs), and seeds v2_job_runtime.ping so the running side accrues real-time worker seconds (the algorithm bounds end-of-interval by ping). The zombie/suspended insert helpers are intentionally left without v2_job rows — the new algorithm's INNER JOIN excludes them, so they still correctly contribute zero worker-seconds. * chore(queue): bump EE ref to duration-weighted fairness algorithm Companion to windmill-ee-private#<TBD>: switch the EE workspace fairness aggregation from a count-based UNION (worker_ping snapshot + v2_job_completed count) to a worker-seconds aggregation sourced directly from v2_job_queue and v2_job_completed, with kind/suspend filters mirroring handle_zombie_jobs and per-row defenses against zombie inflation on both halves. * chore(queue): bump EE ref for fairness perf fix (inline window_start) * chore(queue): bump EE ref for fairness perf rewrite (driver-side flip) * update ee ref * feat(hub-publish): add backend proxy routes for hub publishing New workspaced router /api/w/:ws/hub/* forwarding to the Hub: - POST /publish_draft → POST {HUB}/workspaces (slug/name/summary/readme) - POST /scripts → POST {HUB}/scripts/add (workspace_slug + content) - POST /flows | /apps | /raw_apps → corresponding hub endpoints - POST /scripts/:ask_id/recording, /flows/:flow_id/recording → recording uploads Auth uses HUB_DEV_TOKEN env var (dev shortcut). All bodies are serde-typed; the helper forward_to_hub centralises the HTTP call. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(deploy-to-hub): wire frontend to backend hub proxy Replaces the mocked deploy flow with real backend calls: - confirmBundle() POSTs /hub/publish_draft with sanitized slug, name, summary and readme. - deployAll() pushes selectedItems one by one via pushItem(), fetching the live content (Script/Flow/AppService + raw_apps get_data) before forwarding to /hub/{scripts,flows,apps,raw_apps}. - saveRecording() builds the replay-shaped payload expected by the Hub (initial_job + events with type: 'CompletedJob') and POSTs to /hub/{scripts,flows}/{hub_id}/recording. - Adds bundleSummary state + TextInput in the drawer. Hub item ids (ask_id / flow_id) returned by the create calls are cached client-side to wire later recording uploads. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(hub-publish): add /resources proxy route Forward workspace resource stubs (path + type) to the hub's /workspaces/{slug}/resources endpoint. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(deploy-to-hub): auto-detect resource dependencies from selection Derive resource dependencies from the $res:/res:// references in the selected scripts/flows/apps instead of a manual resource list, sync them as empty stubs, and show them read-only (chip per type, hover for path + which items use it). Aborts item publish if dependency sync fails to avoid broken fork references. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(deploy-to-hub): add project-bundle closure + path-rewrite logic Pure, unit-tested module (projectBundle.ts) backing the "project = folder" Hub bundle: - extractScriptRefs / extractFlowRefs / extractAppRefs: structural detection of $res: references (code, static step inputs, script-by-path), hub refs classified separately. - classifyPath / buildPathMap: relocate external u/.. and f/other/.. paths under f/<slug>/, with deterministic _2/_3 collision suffixes. - rewriteContent / rewriteFlowValue / rewriteAppValue: rewrite every ref to its relocated path, leaving hub/.. untouched. - buildProjectBundle: walk the transitive closure of a seed selection (scripts pulled in recursively, resources pulled as stubs), returning the rewritten items + resource stubs + unresolved list. 14 vitest cases cover classification, extraction, collision suffixing, partial-match safety, deep-clone, and the closure orchestrator. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(deploy-to-hub): publish as relocated project bundle + resource drawer - deployAll now builds a self-contained project bundle (buildProjectBundle), pushing resource types, empty resource stubs at relocated f/<slug>/ paths, and the rewritten items — so a fork's references resolve inside the project. - Resource-dependency detection is unified on the same bundle: the UI list (dependencyTypes) is derived from the bundle preview, guaranteeing what's shown matches what's pushed. Removes the duplicate in-component detection (extractResRefs/refsForItem/resolveResourceSet/typeForResource). - Input-type deps (schema format: resource-<type>) are synced as types and conventional f/<slug>/<type> stubs alongside hardcoded ones. - Replaces the hardcoded-path warning/fix/block machinery with a read-only "Resource dependencies" drawer: per-type usages tagged input vs hardcoded path, with an info popover explaining the portability tradeoff. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(deploy-to-hub): gate hub publish endpoints + harden trigger detection - Add ApiAuthed + require_admin to all hub publish handlers; previously any workspace-authenticated session could trigger Hub-side writes attributed to the URL workspace via the shared HUB_DEV_TOKEN. - Track per-kind trigger fetch failures (triggerLoadErrors) so an EE-gated or transiently failing trigger service no longer silently maps to "0 triggers"; UI surfaces an amber badge listing the missing kinds and a toast warns the operator before publish. - Add workspaceLoadSeq cancellation so the parallel loadWorkspace + loadTriggers stop bleeding stale data when the workspace switches mid load. - Drop the silent effectiveSlug fallback to sanitizeSlug(hubName) when the Hub response can't be parsed; abort the publish instead so items don't land under a slug the Hub never locked. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(deploy-to-hub): thin /triggers proxy to forward trigger bulk-sync to Hub Mirrors the existing /scripts, /flows, /apps thin proxies. Forwards { triggers, workspace_slug } to Hub's POST /workspaces/[slug]/triggers bulk-replace endpoint, with the same require_admin + HUB_DEV_TOKEN guardrails. Lets the frontend push trigger stubs in a single round-trip after the items they reference have landed on the Hub. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(deploy-to-hub): push trigger stubs as the final bundle step After scripts/flows/apps land on the Hub, pushTriggers() builds a relocation map for the trigger paths, strips operational metadata (workspace_id, edited_by/at, enabled, last_*/captured_*, capture data, error_handler_path/args, permissioned_as*) from each config, resolves script_ask_id / flow_id via the hubItemIds map produced by step 3, and POSTs the whole set to /api/w/:wsp/hub/triggers. Triggers whose runnable didn't publish are skipped with a warning rather than emitted as broken stubs. Also drops the per-kind trigger-load error surfacing: feature-gated services (Kafka, NATS, ...) 404 on instances that don't enable them, and the banner was lighting up on every load for nothing. Errors are swallowed silently again, matching the pre-review behaviour. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(hub_publish): rename Hub-facing fields and URLs from workspace to project Matches the windmillhub rename: every body now carries `project_slug` instead of `workspace_slug`, the draft creation forwards to `/projects`, and the resource_types/resources/triggers proxies hit `/projects/{slug}/...`. `HubWorkspaceBody` becomes `HubProjectBody`. The instance-side `Path(workspace)` extractor and the `workspace` URL parameter stay because that's still the source tenant's identifier. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ui(deploy-to-hub): user-facing rename from "workspace" to "project" The Hub-deploy surface now talks about *projects* (the bundle published to the Hub) instead of *workspaces* (which still means the source tenant). Tab is "Publish project", header copy mentions "project", the Hub URL in the breadcrumb points to /projects/<slug>, payload field is `project_slug`. Internal state names (`workspaceItems`, `workspaceStore`, `WorkspaceService`, …) stay — they refer to the instance workspace the items are read from, which has not been renamed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ui(deploy-to-hub): open-in-tab affordance on each dependency and trigger Adds a small ExternalLink icon at the far right of every row in the Resource dependencies drawer (script / flow / app / raw_app) and the Triggers drawer (per trigger kind, opens the matching list page — /routes, /schedules, /websocket_triggers, /kafka_triggers, …). Both buttons open in a new tab scoped to the current $workspaceStore. Sized to sit after the role badge so the dominant signal (input vs hardcoded path, script vs flow) stays read first. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(deploy-to-hub): proxy raw app embed to the Hub Add POST /w/{workspace}/hub/raw_apps/{id}/embed forwarding to the Hub so a shared (public) raw app's external_embed_url can be set/cleared. null is forwarded (not skipped) so unpublish clears the embed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(deploy-to-hub): bundle raw apps, share live iframe, folder-scoped bundles - Detect modern raw apps (app table, raw_app=true) and push them to the Hub as raw apps: fetch source files + runnables + the compiled bundle (via the latest-version bundle secret) and shape them into the raw payload RawAppView expects. Fail loudly when no compiled bundle exists. - Capture the Hub id for raw apps and wire "Share as iframe"/"Unpublish" for them (post-bundle, like recordings); re-sync the embed on re-bundle for already-public apps. Factor the publish/unpublish flow into setAppShared + pushRawAppEmbed helpers. - Scope bundles to a single required f/<folder>/ (Select instead of MultiSelect) so relocated paths stay predictable. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(hub): send item path when publishing a project to the hub Include each item's newPath in the script/flow/app/raw_app publish payloads so the hub can store the relocated Windmill path. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(hub): accept path on publish and proxy project export Add an optional path field to the publish bodies and a GET /projects/{slug}/export route that proxies the hub export (admin-only, authenticated with HUB_DEV_TOKEN). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(projects): add project install page New /projects/install page pulls a hub project's export and re-creates it in the selected workspace. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(deploy-to-hub): let user pick target folder on project import Add a FolderPicker to the project install page (defaulting to the project slug, with create-new-folder support) and retarget every `f/<slug>/` prefix in the bundle — item paths, $res:/script refs, schedule runnable paths — to the chosen folder in one pass. Ensures the target folder exists before creating items. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Fix wording * fix(hub-publish): bind Hub publish/export to the trusted workspace via source_id Hub publish endpoints ignored the {workspace} path and addressed the Hub project purely by client-supplied project_slug, forwarding with an instance-wide HUB_DEV_TOKEN. Any workspace admin could mutate or export another workspace's Hub project by passing its slug. Stamp the server-trusted workspace from the path onto every forwarded request as source_id (body for mutations, query param for export) so the Hub can enforce that the targeted project belongs to the calling workspace. Requires the matching Hub-side source_id ownership check. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(deploy-to-hub): reset draft/publish state on workspace switch The workspace-switch effect only reset load-derived state, so phase, draftItems, recordings, hub/bundle metadata, hubVersion, deploymentStatus, effectiveSlug and hubItemIds survived a switch — a draft built in one workspace could publish its items/slug under the next workspace's auth. Reset the full publish session on switch. Also drop explanatory comments. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(deploy-to-hub): pull sub-flows referenced by type: flow steps into the bundle extractFlowRefs only emitted refs for type: script steps, so a flow calling an external sub-flow by path was never followed and the published project was silently incomplete. Add a 'flow' RefKind, emit it for type: flow steps, recurse on it in buildProjectBundle, and rewrite its path on relocation. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(deploy-to-hub): fall back to slug when import folder is whitespace-only (folderName || slug).trim() let a whitespace-only folder bypass the slug fallback and trim to an empty target, producing invalid f//... paths and a failed import. Trim first, then fall back. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(hub-publish): validate project slug before interpolating into Hub path slug/project_slug are caller-controlled and were interpolated straight into the Hub request path; a crafted value (e.g. ../../admin) could reach an unintended Hub endpoint after URL normalization. Validate against the frontend charset (lowercase alphanumerics + hyphens, 3-50 chars) in the four handlers that put the slug in the path. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(deploy-to-hub): use ?tab query param to link to the Apps settings tab The "Edit in Workspace settings → Apps" link set window.location.hash, but the settings page derives the active tab from ?tab=..., so the link was a dead affordance. Navigate with goto('?tab=default_app') instead. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(deploy-to-hub): point Open in Hub link at the project slug, not the workspace hubSlug was derived from $workspaceStore, so the Open in Hub link and badge used the workspace id instead of the published project slug — navigating to the wrong (or nonexistent) Hub project. Derive hubSlug from the actual project slug (effectiveSlug, falling back to sanitizeSlug(hubName)). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Sync hub to instance * feat(deploy-to-hub): rehydrate project state, wire review flow, bundle trigger resources - Rehydrate the publish panel from the Hub by source_id on load (phase, slug, metadata, items, hub ids, recordings) so refresh no longer loses the draft. - Map Hub project status to the draft/under_review/live phase; submitForReview now persists to the Hub instead of a local stub; drop the unused v{n} version display (status is the source of truth). - Send source_path (original workspace path) per item for recording round-trip. - Detect resources referenced by triggers, add them to the bundle closure (extraResourcePaths) so they appear in dependencies, get stubbed/relocated, and rewrite the trigger config path via the full bundle pathMap (no leaked private path); show trigger usages in the dependency drawer. - Review fixes: Array.isArray guards on trigger topic/subject lists; snapshot relevantTriggers in deployAll to avoid a mid-deploy folder-switch race; index-key the usage list. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(deploy-to-hub): keep item summary on rehydrated draft, drop placeholder diff button Rehydrated draft items now carry their summary (from the Hub) so step 2 shows the summary like step 1 instead of falling back to the path. Remove the "Diff vs submitted" button: it only toasted add/remove counts with no view, which read as broken. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(deploy-to-hub): New draft returns to the folder-picker step instead of erroring In the live phase the folder picker is hidden, so startNewDraft's selectedFolder guard always failed with "Pick a folder..." and the user had no way to pick one. Now New draft goes back to step 1 (predeploy) with the project's folder pre-selected (inferred from the item paths) so the user can re-bundle. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(hub_publish): return 500 not 400 when HUB_DEV_TOKEN missing * fix(deploy-to-hub): keep internal subfolder paths identity-mapped when bundling * fix(projects-install): never overwrite existing resources; isolate invalid raw app json * fix(deploy-to-hub): route raw_app to apps_raw/get and guard openRecord schema race * refactor(hub_publish): extract hub_token helper, drop duplicated env lookup * refactor(projects-install): route raw-app and unsupported-trigger failures through record() * fix(deploy-to-hub): refresh review status from Hub and use configured hub base url * fix(hub_publish): return 400 not 500 when HUB_DEV_TOKEN is unset Missing config is a client/config error, not a server fault. Restores the BadRequest class lost when hub_token() was extracted. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(hub_publish): scope Hub projects per folder via workspace:folder source key * feat(deploy-to-hub): publish per-folder projects from the Folders page * ui(deploy-to-hub): move phase CTA to the top-right header * Fable review * feat(hub_publish): forward the caller's token to the Hub instead of HUB_DEV_TOKEN * style(windmill-api): cargo fmt fallout in build.rs and lib.rs * Nit fixes * Nit fix * fix: structural project-ref rewrite and deploy-to-hub state fixes Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix: deterministic draft phase fallback when post-deploy rehydrate fails Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(frontend): skip EE-only native trigger calls on CE to avoid console 404s Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(deploy-to-hub): fork all trigger kinds on project install The project install (fork) flow recreated only schedule triggers and rejected every other kind with "not supported yet". Recreate all trigger kinds instead, imported disabled (enabled: false → mode disabled). Kafka, NATS, SQS, GCP and Azure require an Enterprise license, so they are gated behind enterpriseLicense and reported as "requires Enterprise" on CE rather than firing backend calls that 404. http, websocket, postgres, mqtt and email are recreated on CE. The kind-specific config (with retargeted resource paths) is spread into the create body; explicit path/script_path/ is_flow/summary/enabled win over it. Also carry the schedule summary through. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(deploy-to-hub): list CE trigger kinds without an Enterprise license loadTriggers wrapped http, websocket, postgres, mqtt and email list calls in eeList, so on CE (no enterpriseLicense) they resolved to [] and never made it into deploy state — those triggers silently disappeared from the Hub publish set. Only Kafka, NATS, SQS, GCP and Azure are EE; switch the CE kinds back to safeList so they are always listed and published. Mirrors the EE gating used on the project install (fork) side. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(deploy-to-hub): reload triggers when EE license hydrates late loadTriggers captures enterpriseLicense at call time and the main reload $effect only depends on workspace/folder, guarded by lastLoadedKey. When the license store hydrates asynchronously after loadTriggers already ran, the EE trigger kinds (kafka/nats/sqs/gcp/azure) stay empty until the workspace or folder changes. Add a dedicated $effect that re-fetches triggers on the license false→true transition, mirroring the sidebar's license-race handling. prevHadLicense is seeded from the current value so a license already present at mount doesn't trigger a redundant reload. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(deploy-to-hub): token loadTriggers so a late EE reload can't be clobbered The license-late reload calls loadTriggers with the same workspaceLoadSeq as the original license-less load, so the workspace guard alone lets both assign workspaceTriggers. If the earlier (EE-empty) request resolves last, it overwrites the newer license-aware result and the EE trigger kinds disappear again. Add a per-invocation triggerLoadSeq token and only let the latest load assign (and toggle triggersLoading), so a slow earlier request is discarded. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(deploy-to-hub): use mode 'disabled' for forked non-schedule triggers Non-schedule triggers expose `mode` (TriggerMode), not the deprecated `enabled` flag, in their create body. `enabled: false` happens to still map to disabled today via the backend's legacy BaseTriggerData field, but relying on a deprecated path is fragile. Set `mode: 'disabled'` explicitly so imported http/websocket/postgres/mqtt/native triggers stay disabled. Schedules keep `enabled: false` (NewSchedule uses the enabled flag). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(deploy-to-hub): snapshot workspace + key bundle by kind:path Address three P1 review findings: - Project install (fork) read the reactive `workspace` ($derived) across many sequential awaits, so a workspace switch mid-import could create the folder in one workspace and later items in another. Snapshot the target workspace once at the top of install(). - DeployToHub.deployAll re-read $workspaceStore after confirmBundle had already created the Hub draft bound to a specific workspace's source_id, so a switch during draft creation could publish items to a different workspace. Pass the workspace captured by confirmBundle into deployAll instead. - buildProjectBundle keyed its fetched/queued maps by bare path, silently dropping one of two distinct-kind items at the same path (script vs flow). Key by `${kind}:${path}` and derive item paths from the fetched values, keeping path relocation separate. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * style(deploy-to-hub): condense comments Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(deploy-to-hub): surface backend error body on failed project import record() only showed `e.message`, which for API errors is the generic status text ("Bad Request"). Prefer the ApiError `.body` (plain-text reason for Windmill 4xx) so a failed import reports the actual cause — e.g. a path or route_path collision — instead of a bare "Bad Request". Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(deploy-to-hub): close mid-request workspace-switch races Two follow-ups to the workspace snapshotting: - confirmBundle captured `workspace` but read selectedItems / relevantTriggers / hubSlug only inside deployAll, after the publish_draft await. A workspace switch during that request resets those to the new workspace, so deployAll would push the new workspace's items into the old workspace's Hub draft. Capture workspaceLoadSeq before the request and abort (with a toast) if it changed before publishing. - install() snapshotted `workspace` but still read the reactive `data` after the createFolder await; load() can replace `data` on a workspace switch, so retarget() could run against a different export than `folder` was derived from. Snapshot `data` up-front and use it throughout install(). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(deploy-to-hub): guard stale load response and mid-publish status writes - install load() assigned `data`/`folderName` unconditionally, so a slow /export for an old ?hub= could overwrite a newer project after navigation. Add a load token + captured slug/workspace and only assign if still current. - deployAll wrote deploymentStatus/hubItemIds incrementally and only checked the workspace at the very end. Bail at the top of the per-item loop when the active workspace changed, so a mid-publish switch can't keep writing the old workspace's item statuses and Hub IDs into the new workspace's live view. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(hub-projects): generate and apply datatable migrations on project publish/install (#9977) * feat: add datatable_migrations table * feat: add route to run datatable migrations * feat: sync datatable migrations as .up.sql/.down.sql files * feat: add datatable migrate up/down commands and post-push run prompt * feat: add datatable migrate new command to scaffold migrations * feat: add datatable migrations management UI * feat: prompt to create migration on DDL in datatable SQL editors * feat: support running a single specific datatable migration * feat: view migration content, run single migration, fix stacked modal * feat: per-row revert button with out-of-order warning * fix: avoid migrations list flicker on refresh after an action * feat: generate initial datatable migration via pg_dump * fix: surface datatable migration API error details in toasts * fix: revert created migration if create-and-run fails to run * fix: include postgres error detail in migration run/rollback failures * feat: sync datatable migrations as files via the workspace export * refactor: move datatable migrations to migrations/datatable/ path * fix: drop redundant datatable_migration label in sync output * fix: exclude datatable migration sql files from script metadata generation * feat: run datatable migrations as user-permissioned labeled jobs * feat: reject invalid datatable migrations on sync push * feat: datatable migrate up/down default to all datatables, --datatable to target one * fix: surface postgres error detail when datatable migrations fail to run * chore: regenerate CLI docs for datatable migrate commands * feat: default new datatable migration to a BEGIN/END transaction template * fix: validate datatable migration name and datatable at the API boundary * fix: ensure detected DDL ends with semicolon when wrapped in transaction * fix: re-prompt instead of stripping DDL when new-migration modal is cancelled * feat: refresh datatable schema after running a migration from the SQL REPL * feat: record db manager DDL on data tables as migrations * feat: make datatable migrations opt-in per data table * fix: make migration view editor read-only so its code can scroll * fix: don't re-prompt DDL guard when creating a migration without running * feat: generate down migrations for db manager DDL (postgres) * fix: correct down migration for db manager alters (no double-wrap, serial) * feat: explain migrations purpose with a tooltip in the migrations modal * compare paeg * feat: add datatable_migration kind to workspace diff pipeline * chore: point ee-repo-ref at datatable_migration git-sync companion * fix: harden datatable migration version allocation and initial-migration bookkeeping, add tests * feat: deploy and run datatable migrations on workspace merge Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Refactor + handle datatable setting delete/rename * refactor: move datatable migration rename/delete cascade into module Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(windmill-utils-internal): bump to 1.7.1 for datatable migration deploy provider methods Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(db-manager): add Migrations button to top bar, make Refresh icon-only Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * BEGIN/END placeholder in down migration * feat: autofocus migration name input and flag it red when empty Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(datatable-migrations): allow non-admins to create/run/revert migrations, gate only opt in/out Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * border nits * refresh db manager schema on migrations * BEGIN/END scaffold in CLI * feat(cli): push local datatable migrations before running on migrate up Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat: flag invalid migration name with red border, not just empty Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor: drop random slug from auto-generated migration names Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat: offer revert-and-delete when deleting an installed migration Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat: record fork merge as a migration when target datatable opts in * nit * clone migrations on fork * windmill-utils-internal * fix(datatable-migrations): serialize run/rollback with a per-db advisory lock Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(db-manager): fail closed when migrations-status check errors on DDL apply Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs: fix generate_initial migration ordering comment to match code * chore(datatable-migrations): remove unused update_datatable_migrations endpoint Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix: run DDL migration guard on the script editor Test button Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * split * ee-repo-ref * chore(frontend): sync package-lock with package.json (@emnapi deps) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(datatable-migrations): never resolve instance credentials into migration job args datatable_database_arg eagerly resolved instance data-table credentials (including the shared instance-wide Postgres password) and passed them as the migration job's plaintext `database` arg, landing in v2_job.args. Since the run route has no admin gate, a non-admin could run a migration and read args.database to recover the password, granting cross-workspace psql access to all instance data-table DBs. Pass a `datatable://<name>` reference for both resource-backed and instance data tables instead; the pg executor already resolves it to real credentials server-side at run time, so nothing sensitive is ever stored in the job args. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * nit * fix: handle dollar-quoting and comments when splitting SQL statements * feat: deploy datatable migrations on merge with explicit opt-in error * fix(frontend): sync package-lock with npm 11 peer-dep resolution npm ci failed with 'Missing: @emnapi/core@1.11.2 / @emnapi/runtime@1.11.2 from lock file'. @napi-rs/wasm-runtime declares @emnapi/core|runtime ^1.7.1 as peerDependencies while @rolldown/binding-wasm32-wasi pins them to exactly 1.10.0. Newer npm (bundled with node 24 in CI) installs the peer deps at the highest match (1.11.2) alongside rolldown's nested 1.10.0, so the ideal tree needs both versions; the committed lock only had 1.10.0. Regenerate the lock with npm 11.18 so it carries both 1.11.2 (top-level, for the peer deps) and 1.10.0 (nested, for rolldown's pin). Verified npm ci passes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * nit npm publish * fix: fail closed on migrations-status error in fork schema merge * nit CI emnapi/core version * prevent initial_datatable_migration if migrations already exist * fix(datatable-migrations): validate persisted data table names as path segments edit_datatable_config only validated rename segments, not the actual settings.datatables keys, so a data table could be saved directly under a name like '..' or one containing '/'. Since new tables default to migrations_enabled = true, generate_initial_datatable_migration would then insert a migration row and the sync export would build migrations/datatable/<name>/... paths from that name, producing malformed or directory-escaping export paths. Validate every persisted data table name in edit_datatable_config (alongside the existing rename checks) and add validate_datatable_path_segment to generate_initial_datatable_migration for defense in depth. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix: scope datatable _wm_migrations by data table and cascade renames/deletes Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(system_prompts): resolve nested local command groups in CLI docs generator The CLI docs generator anchored on the first `new Command()` in a file and never resolved locally-defined command groups passed as `.command("name", localCmd)`. For datatable this flattened the nested `migrate` group: it emitted `datatable new/up/down` plus a bare `datatable migrate`, and mislabeled the datatable command with the migrate group's description. jobs was broken the same way (its description was pull's, and pull/push rendered empty). Anchor block extraction on the `export default`ed command, recurse into locally-defined `const x = new Command()` groups mounted as subcommands, and render nested sub-subcommands. Regenerated docs now show `datatable migrate new/up/down` and `jobs pull/push` with their real options. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor: drop unreleased _wm_migrations legacy-upgrade handling Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix: return datatable migration SQL from getItemValue for the diff drawer Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(frontend): use windmill-utils-internal 1.8.2 for migration diff drawer Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * nit * nit * fix: handle datatable migration renames on push and dedupe timestamps * fix: reject rewriting an already-applied datatable migration on upsert Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(frontend): add missing @emnapi/core and @emnapi/runtime lockfile entries Resolves npm ci EUSAGE failure: the optional cpu:wasm32 @rolldown/binding-wasm32-wasi declares deps on @emnapi/core@1.11.2 and @emnapi/runtime@1.11.2 that had no resolved lockfile entries. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(cli): datatable migrate up/down default to main datatable, not all Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix: fail closed when applied status unreadable on datatable migration rewrite Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix: surface full error detail in Database Manager DDL/query errors * "See migration" button in the toast * feat: add Enter shortcut to Create-a-migration in the DDL guard * fix(frontend): warn before running a newly-created datatable migration out of order The row-level Run action warns when earlier migrations are still pending, but the create-and-run paths ran a just-created migration with `only` directly, applying it ahead of older pending migrations without that confirmation. Reuse the same "Run migration out of order" confirmation across all create-and-run paths via a shared helper (datatableMigrationUtils): - NewDataTableMigrationModal "Create and run" (and the DDL guard path) - DatatableSchemaDiff fork→parent merge - dbOps schema ops (DB manager create/alter/drop) — the pure factory throws a MigrationRunCancelled sentinel on decline, which DBTableEditor treats as a silent cancel Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix: keep renamed datatable migrations visible in compare view * fix: record per-migration deployment on datatable migrations disable * fix(cli): run deployed datatable migrations after workspace merge The merge command upserted datatable_migration definitions into the target workspace and reported the item as successfully deployed, but never ran the migrations. For forked datatables backed by separate databases, this left the target schema unchanged until someone manually ran `wmill datatable migrate up`, while the CLI reported a successful merge. Collect the datatable migrations deployed (not deleted) into the target and, after the deploy loop, offer to run them via the existing offerToRunNewMigrations helper — the same post-deploy run prompt the push/sync path uses (interactive only; `--yes`/non-TTY skip the mutating run, matching push behavior). Export parseDatatableMigrationDeployPath so the merge path can parse the deployed items. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(backend): serialize datatable migration edits/deletes with the run lock A migration run snapshots a migration's code_up from datatable_migrations and only records its version in the data table's _wm_migrations after the job succeeds. upsert_datatable_migration checked _wm_migrations before allowing an edit but took no lock, so a concurrent edit could read "not applied yet", rewrite code_up/code_down, and then the in-flight run would record the version for the old SQL — leaving _wm_migrations pointing at SQL that was never applied (migrate up then skips it; rollback runs a down that doesn't match). Serialize definition rewrites and deletes with the same per-database advisory lock the run/rollback paths use: - Factor the connect+advisory-lock into lock_datatable_migration_runs and the applied-versions read into read_applied_versions_on_client. - run_datatable_migrations now snapshots the definitions AFTER taking the lock, so code_up can't change between snapshot and version-record. - upsert (when changing an existing def) and delete take the lock across the applied-check and the write; delete now rejects deleting an already-applied migration (would orphan its _wm_migrations record), symmetric with upsert. Both fail closed if the data table database is unreachable. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(frontend): stack the out-of-order migration confirm above the DB editor preview Creating a table on a migrations-enabled data table opened the DB table editor's "Confirm running the following" preview modal, whose confirm triggers applyDdl, which then asks for out-of-order confirmation. Both are ConfirmationModals with a hardcoded z-[9999]; the out-of-order one lives in DBManagerContent (mounted before the editor), so it rendered behind the still-open preview modal. Add an optional zIndexClass prop to ConfirmationModal (default z-[9999], backward-compatible) and give the DB-manager out-of-order confirm z-[10000] so it stacks on top. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(hub-projects): generate and apply datatable migrations for projects Detect datatable assets in a project's scripts/flows/raw apps when publishing to the Hub, generate a best-effort CREATE TABLE migration per data table from the source workspace's live schema, and let the publisher edit/toggle them in the bundle drawer. On import, offer to run the shipped migrations: recorded (datatable_migrations + _wm_migrations) when the target data table opted into migrations, otherwise as a one-off preview job. Missing target data tables are surfaced and skipped. - backend: POST /hub/migrations proxy forwarding to the Hub - frontend publish: projectMigrations.ts detection + generation, new "Data table migrations" section in DeployToHub - frontend import: run/skip modal + missing-datatable confirmation - extract pure SQL-gen from DatatableSchemaDiff.svelte into datatableSchemaSql.ts so plain .ts modules can import it Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(hub-projects): close datatable migration table set over foreign keys Pull a referenced table's FK targets into the generated migration transitively, so it creates every table it references (ordered by FK dependency), and drop any FK whose target still isn't in the set so the generated SQL always runs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(hub-projects): show Data table dependencies in the publish view Detect data table usage off the predeploy bundle preview and surface it as a "Data table dependencies" summary right after "Resource dependencies", mirroring how resource types and triggers are shown. The editable migration itself stays in the bundle drawer. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(hub-projects): explain un-generated migrations with SQL comments When a table can't be found in the schema, a data table is referenced as a whole, or the schema can't be loaded, write a `--` comment describing the problem into the migration instead of leaving it blank. Partial migrations keep the CREATE TABLEs that did generate and comment the rest; comment-only migrations stay disabled. The bundle drawer now always shows the SQL box so those comments are visible and editable. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(hub-projects): review/edit migrations on import + rollback down migration Replace the plain "run migrations?" confirmation with a review drawer that previews each runnable migration, lets the user edit the SQL and toggle which to run, before the import proceeds. When recording an imported migration, also record a down migration (DROP TABLE of the created tables, in reverse order) derived from the up SQL, so it can be rolled back; the derived rollback is previewed in both the publish bundle drawer and the import review drawer. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * nit * feat(hub-projects): editable Up/Down Monaco editor for migrations Replace the plain textarea with a Monaco SQL editor split into Up/Down tabs. The down migration is now generated once as best-effort (DROP TABLE in reverse creation order) and is fully editable — no longer parsed back out of the up SQL. The down is threaded through publish → Hub → import (new project_migration.sql_down) and recorded as code_down when an imported migration is applied. - projectMigrations: GeneratedMigration.sql_down generated from the table set - MigrationSqlEditor.svelte: shared Up/Down tabbed Monaco editor (re-keyed on regeneration since Monaco ignores external code changes) - DeployToHub + install review drawer use it; sql_down pushed/applied - backend: PublishMigrationBody carries sql_down Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(hub-projects): generate CREATE TABLE IF NOT EXISTS for project migrations The FK closure pulls a referenced table's parents into the same transaction (e.g. `orders` drags in `customers`); those shared parents often already exist in the target, so a plain CREATE TABLE aborted the whole migration on the first collision. Emit CREATE TABLE IF NOT EXISTS for project migrations (via a new opt-in flag on generateMigrationSql, leaving the schema-diff behavior unchanged) so a pre-existing parent is skipped instead of failing. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(hub-projects): key FK ordering by schema-qualified table name orderByFkDependency keyed its dependency graph by bare table name (and resolved FK targets with .split('.').pop()), so two same-named tables in different schemas collapsed and one was dropped from the ordered set and never created. Key by schema.table like the rest of the pipeline, resolving FK targets through resolveTable. Also let resolveTable fall back to the bare table name when a schema-qualified ref's schema doesn't match. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(hub-projects): comment out generated down-migration DROP statements The generated down migration listed DROP TABLE for every table in the FK closure, including shared parent tables that may have pre-existed in the target — a rollback could drop a table the project never created (data loss). Emit all DROP statements commented out with a note, so nothing is dropped by default; the publisher uncomments the tables this migration actually owns. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(hub-projects): disable Import button during migration review planMigrations awaits the review / missing-datatable modals before setting installing = true, so the Import button stayed enabled during review and a second click launched a concurrent install() (second review drawer, duplicated item creation). Track a planningMigrations flag, disable the button on it, and early-return install() if already installing or planning. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(hub-projects): toast when migration generation fails regenerateMigrations cleared the drafts on error, showing "No data table usage detected" — indistinguishable from a genuine schema-load failure. Add a toast on the catch so the publisher can tell the two apart. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(hub-projects): honor cancel on the missing-data-table warning planMigrations awaited missingDatatableModal.ask() but ignored its boolean, so cancelling the "some data tables are missing" warning still proceeded with the import — the cancel affordance did nothing. Show the warning first and abort the whole import when the user cancels (planMigrations returns null; install() early-returns), so they can create the data table(s) and re-run. Confirming still imports without the missing migrations. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(hub-projects): detect data tables from low-code app DB-table config Low-code apps don't carry a persisted asset list, but the DB-table component declares its data table and table explicitly: a `oneOf` `type` config with `selected === 'datatable'` holding `datatable://<name>` and the table. Walk the app value for those configs so an app that reads a data table is picked up by the Data table dependencies detection. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Revert "feat(hub-projects): detect data tables from low-code app DB-table config" This reverts commit9c43ebd512. * fix(hub-projects): detect data tables from full-code apps' declaration Full-code (raw) apps explicitly declare the data tables/tables they use in value.data.tables (refs like main/customers or main/schema:table), which the "Data table dependencies" detection missed — it only looked at inline-script assets. Read the declaration via extractDataConfig/parseDataTableRef. The bundler previously dropped value.data (kept only files + runnables); include it so detection sees it and the imported app keeps its declaration, and pass it through on import. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(hub-projects): recompute app policy on project import Apps imported from a Hub project were created with an empty triggerables_v2 policy, so running any inline component script failed at runtime with "Path rawscript/<sha> forbidden by policy". The policy is computed client-side on deploy and stored verbatim by the backend, and import skipped that step; retargeting also rewrites inline-script content (changing its sha), so a copied policy would not match either. Recompute the policy from the retargeted value at import, mirroring the deploy path: updatePolicy for grid apps, updateRawAppPolicy for raw apps, defaulting execution_mode to publisher. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * nit fix --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(hub-projects): retarget plain trigger resource paths on import Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(hub-projects): reset migration drafts on workspace/folder switch Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(hub-projects): bundle http auth resources, pin drafts during deploy Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(hub-projects): make generated data table migrations idempotent Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Reapply "feat(hub-projects): detect data tables from low-code app DB-table config" This reverts commit112844deea. * fix(hub-projects): create all tables before FK constraints in migrations Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(hub-projects): reset install state when the hub slug or workspace changes Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Revert "Reapply "feat(hub-projects): detect data tables from low-code app DB-table config"" This reverts commit14abefb4f6. * fix: dedupe args state duplicated by main merge in AssetGraphDetailsPane Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(deploy-to-hub): extract session class keyed by workspace+folder All DeployToHub state and async operations move into DeployToHubSession (deployToHubSession.svelte.ts), an immutable-(workspace, folder) state class. A workspace/folder change replaces the instance and remounts the UI via {#key} instead of manually resetting ~20 state vars, and in-flight async work writes to the discarded object instead of racing the new scope. The workspace-scoped seq counters (workspaceLoadSeq/triggerLoadSeq for lifecycle, migrationsSeq) collapse into a dispose flag plus intra-session tokens only. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WqqWYQR46tcunvPVRfidZS * refactor(triggers): single shared module for all-kind workspace trigger listing TRIGGER_KINDS (badge/route/note/resourceField/eeOnly + list call), listAllWorkspaceTriggers, triggerResourcePath, stripTriggerConfig and triggerDetails move to $lib/components/triggers/workspaceTriggersList.ts, so EE-license gating per trigger kind is declared once instead of being re-decided at each call site. DeployToHubSession consumes it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WqqWYQR46tcunvPVRfidZS * refactor(hub-publish): route every endpoint through one validation choke point HubPublishCtx (a FromRequestParts extractor) is now the only way a handler reaches the Hub: it performs the admin check, resolves and validates the workspace:folder source key, and carries the forwarded token — a new endpoint cannot skip any of it. Project slugs become a ProjectSlug newtype whose only constructor is validating deserialization (body field or path segment), so every slug that reaches a Hub URL or payload is valid by construction; the previously unvalidated slugs in publish_draft/scripts/flows/apps/raw_apps/ embed/recording bodies are now checked too. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018DoHaGJdACgE7RvknRDAb6 * refactor(hub-projects): shared bundle format module + per-kind project installer The Hub export format (types + retargetProjectExport/buildRetargetMap) moves into projectBundle.ts so publish and install share one definition, with unit tests for retargeting. projectInstall.ts owns the import: one importer per item kind with per-item error capture, and trigger creation goes through createWorkspaceTriggerDisabled in the shared trigger module, which encodes the per-kind disable semantics (schedules use enabled:false, everything else mode:'disabled') and EE gating once. The install page shrinks to orchestration and UI. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018DoHaGJdACgE7RvknRDAb6 * fix(hub-projects): AMQP kind, trigger handler bundling, import containment Review-round fixes: register the AMQP trigger kind (CE) in the shared registry so it lists/bundles/imports like every other kind; stop stripping error_handler_path/args from trigger configs and bundle + relocate handler runnables (including schedules' script|flow-prefixed on_* refs) with the project; resolve full schedule rows on listing (listSchedules is slim) and spread the exported config on import so cron_version, retry, handlers and no_flow_overlap survive; refuse per-item any export path that escapes the selected f/<folder>/ target; and gate the install page's results/done writes on the load sequence so a stale import can't mark a newly loaded project as imported. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018DoHaGJdACgE7RvknRDAb6 * fix(hub-projects): schedule config hygiene and complete handler bundling Strip email/is_draft/paused_until from exported trigger configs (the full schedule row carries owner and runtime state that must not reach the Hub); bundle and relocate dynamic_skip handler scripts (schedule creation refuses a missing one, so an unrelocated path breaks the import); exclude and report a schedule whose detail fetch fails instead of silently exporting the slim row with default behavior; and seed migration detection with the same handler-augmented item set as deployment so data tables used only by bundled handlers get their migrations. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018DoHaGJdACgE7RvknRDAb6 * fix(deploy-to-hub): single guarded publish path, gated on trigger load publishBundle() owns draft creation + deployment under one synchronously-set deploying flag, so a double-click can't start two interleaved publishes, and it refuses to run while triggers are still loading — snapshotting an incomplete relevantTriggers list would permanently omit triggers, their handlers and handler-only migrations from the draft. The bundle CTAs disable while trigger discovery is in flight. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018DoHaGJdACgE7RvknRDAb6 * fix(hub-projects): block publish on failed trigger discovery, strict schema-qualified table resolution listAllWorkspaceTriggers now distinguishes a feature-gated 404 (kind not compiled into the instance — legitimately empty) from a real listing or detail-fetch failure: failures are surfaced, recorded per kind, and the session blocks publishing with a visible retry until discovery completes cleanly, so an incomplete trigger snapshot can't be bundled silently. resolveTable no longer falls back to a same-named table in another schema when a qualified ref misses — that generated a migration for an unrelated table; the miss now produces the existing commented warning instead. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018DoHaGJdACgE7RvknRDAb6 * docs(openapi): document the 15 hub publish proxy routes All /w/{workspace}/hub endpoints (draft/items/recordings/resource types/resources/triggers/migrations/export/submit/by-source) enter the public API contract with their body schemas derived from the serde structs, a shared HubProjectSlug schema encoding the slug validation, and passthrough text responses matching the proxy behavior. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018DoHaGJdACgE7RvknRDAb6 * fix(hub-projects): bundle $res refs nested in trigger configs, nullable trigger payload fields Trigger dependency collection now scans the full stripped config for $res:/res:// tokens (schedule args, on_*_extra_args, error_handler_args — e.g. the built-in Slack handler's channel resource) in addition to the kind-specific resource field, so those resources enter the bundle path map, get relocated by rewriteTriggerConfig, export a typed stub, and show up in the dependency pane. PublishTriggerBody's summary/description/ script_ask_id/flow_id become nullable in the OpenAPI contract, matching what the publisher actually sends and the Rust Options accept. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018DoHaGJdACgE7RvknRDAb6 * fix(hub-projects): flow preprocessor/env refs, no cloud provisioning on import, config containment Flow extraction and rewriting now cover preprocessor_module (walked like any other module) and flow_env $res: values, so those dependencies are bundled and relocated instead of keeping source-workspace paths. GCP/Azure triggers are refused at import with an actionable message — their create endpoints manage cloud subscriptions before storing the trigger, even disabled, so auto-creating them from an import could mutate external infrastructure. The import containment guard now also validates everything a trigger config binds to (kind resource field, handler runnables incl. hub/ refs, nested $res: tokens), closing the path where a crafted export binds a trigger to assets outside the chosen folder. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018DoHaGJdACgE7RvknRDAb6 * refactor(hub-projects): per-kind config allowlists from a full trigger-field audit Every trigger kind's boundary-crossing config is now an explicit per-kind allowlist (configFields in TRIGGER_KINDS), derived from a field-by-field audit of every create type: portableTriggerConfig replaces the blocklist and is applied on export AND import, so an upstream field addition is dropped until consciously admitted (no more email-style leaks) and a crafted export can't inject fields like permissioned_as into create calls. The audit also surfaced unbundled websocket runnables — $script:/$flow: URLs and initial-message runnable_result paths are now collected and relocated — and drops GCP/Azure provisioned identities (subscription ids, delivery_config with the source instance's endpoint) from exports. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018DoHaGJdACgE7RvknRDAb6 * fix(hub-projects): bundle $res refs nested in JSON flow_env values The worker resolves $res: references inside nested JSON flow_env values (transform_json walks the full value), so extraction and rewriting now scan the env's full serialization instead of only top-level strings. Also: the install-page Enterprise note includes GCP/Azure, the trigger-discovery Retry button binds to the loading state so clicks can't stack requests, and extractTriggerConfigResourceRefs no longer splits rewriteTriggerConfig from its doc comment. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018DoHaGJdACgE7RvknRDAb6 * fix(hub-projects): scope $script:/$flow: relocation to the websocket url field The runnable-url form is only meaningful in that one field; remapping it on every nested config string could corrupt a literal payload that happens to look like one (e.g. a websocket initial raw_message). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018DoHaGJdACgE7RvknRDAb6 * fix(hub-projects): nested static-transform refs, shared flow walk for migrations, top-level-only url remap Static input transforms accept arbitrary JSON and the worker resolves $res: refs nested inside them — extraction and rewriting now scan the full serialization, preserving the value's type. projectMigrations reuses projectBundle's allFlowModules instead of carrying its own module walk, so the preprocessor module (and any future module class) can't diverge between bundling and migration detection. The websocket $script:/$flow: url remap applies only at the config's top level, leaving nested url keys in args or handler payloads untouched. Schedule tag stays excluded by design (a source instance's worker-group name; a foreign tag queues jobs forever) — now documented in the allowlist contract. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018DoHaGJdACgE7RvknRDAb6 * fix(hub-projects): remap prefixed runnable refs only in their known config fields script/<path> and flow/<path> forms are now rewritten only in the top-level schedule handler fields (on_failure/on_recovery/on_success), joining the url field treatment — shape-based remapping on arbitrary strings could rewrite a literal payload that merely looked like a handler ref. Bare-path exact matches and $res: tokens remain position-independent. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018DoHaGJdACgE7RvknRDAb6 * fix(hub-projects): abort stale-session imports after review, walk failure-module descendants Confirming a migration review whose project/workspace was switched away from now aborts with a toast before any write — previously the writes went to the old workspace with all feedback suppressed by the session guard. And allFlowModules puts the failure module in the root list so its nested children (loops/branches inside a failure handler) are expanded like every other module, for both bundling and migration detection. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018DoHaGJdACgE7RvknRDAb6 * fix(hub-projects): preserve app share state on Hub draft rehydration `rehydrateFromHub()` rebuilt `draftItems` from the Hub project payload, which carries only draft membership, so it dropped each app's `published`/`publicUrl` and app-table origin. Outside `predeploy` the UI reads `draftItems` exclusively, so reopening a draft showed a still-public app as unshared and removed its Unpublish control. Merge the live workspace-item state onto matching drafts after both `#loadWorkspace` and `rehydrateFromHub` (they race). Also gate the Share-as-iframe action on `canShareAsIframe`: legacy raw apps live only in the `raw_app` table, but that flow drives `AppService` (the `app` table) and fails with "App not found" for them, so the action is now hidden for legacy entries. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(hub-projects): stale-identity import guard, live publish state on drafts, iframe action gating The install session check now also compares the live slug and workspace to the captured ones — loadSeq only advances when a new load starts, so navigating away (workspace or ?hub becoming empty) previously left the stale migration review able to import into the captured workspace. Draft items are decorated with the live workspace item's shared-iframe fields (published/publicUrl/appTable) so a public app still shows as public after reopening a draft, settling reactively regardless of load order. The share-as-iframe action is offered only for apps and app-table raw apps — legacy raw_app entries have no AppService representation and the action could only fail. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018DoHaGJdACgE7RvknRDAb6 * refactor(deploy-to-hub): drive share-state merge from one reactive derived The rebase left two parallel fixes for the same rehydration gap: an imperative mergeShareState call after each racing load, and a read-time derived. Keep the pure, tested mergeShareState as the single implementation and invoke it from the derived — no load-completion call sites to maintain, and the merge settles whichever load finishes last. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018DoHaGJdACgE7RvknRDAb6 * fix(hub-projects): block publish on unresolved refs; contain imported item refs Address two Codex findings: - Publish continued after `buildProjectBundle` reported unresolved references (a selected root or transitive runnable that failed to fetch, or a resource with no resolvable type), shipping a project whose items silently vanished or still pointed at the publisher's private source-workspace path. `#deployAll` now aborts before any Hub write when the bundle doesn't close, and the bundle drawer surfaces the unresolved list and disables "Create bundle". - `installProject` validated only each item's own path, so a crafted or incomplete export could place a script/flow/app inside the target folder while its `$res:`/script/flow reference stayed bound to an existing `u/...` or other `f/...` asset. Extract each item's live references and reject any that escape `f/<folder>/` (hub/ script refs allowed), mirroring the existing trigger-config containment check. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(hub-projects): contain imported $var refs; dedupe unresolved list Follow-up to the publish-blocker and import-containment fixes: - `$var:` references (flow static inputs, flow_env, app values, and trigger config fields such as SQS queue_url) were not caught by the containment check, which only recognized `$res:`/runnable refs. Retargeting leaves them unchanged, so an export with `$var:u/admin/token` imported an item that resolves a variable outside the target folder under the runnable's permissions. Scan each imported flow/app/trigger for `$var:` tokens and reject out-of-folder ones. Scripts are skipped: `$var:` is resolved in job args, not script source. - `buildProjectBundle` stored bare paths in `unresolved` while keying missing items by kind:path, so a script and flow sharing a missing path produced a duplicate string. The new keyed unresolved list in the bundle drawer then hit Svelte's duplicate-key runtime error instead of rendering the publish blocker. Dedupe `unresolved` at the source. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(hub-projects): contain $var/$jsonvar imports; retryable partial publish; iframe rollback Address four Codex findings: - `$jsonvar:` (secret JSON args) was not contained on import, and scanning the serialized flow/app for `$var:` tokens falsely rejected inline-code literals. The worker only substitutes a variable when an argument value *is* the reference (whole value, walking nested JSON), never a token embedded in code. Replace the token scan with a structural whole-value walk (`$var:`/`$jsonvar:`) and reject out-of-folder refs in flows, apps, and trigger config. Scripts carry no variable args, so they are skipped. - A partial publish (failed item/trigger/migration write) still transitioned to the submit-ready `draft` phase. Stay in the retryable `predeploy` state on any failure, keeping the failed items visible, so nothing incomplete can be submitted and re-publishing retries every idempotent write. - `#setAppShared` flipped a raw app public before checking its Hub item id or syncing the embed, so a missing id or a failed embed sync left the app publicly accessible while reporting failure. Validate the Hub target up front and roll the policy back if the embed sync fails. - `buildProjectBundle` could emit duplicate unresolved paths (a script and flow sharing a missing path), breaking the keyed publish-blocker render. Deduped. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(hub-projects): full-set trigger sync; count iframe re-sync + URL failures Address three Codex findings, two of them refinements of the incomplete-publish gate and iframe-rollback fixes: - `#pushTriggers` returned early on an empty set, so re-deploying a project after removing all its triggers left the previous Hub triggers intact. Always post the trigger list (an empty one clears them), mirroring the migrations full-set sync. - A raw app's post-deploy iframe re-sync failure only toasted; it now increments `failures`, so a public app left with a stale embed keeps the draft out of the submit-ready phase. - `#setAppShared` skipped the embed and still returned success when the public URL couldn't be resolved, leaving the app anonymous with no usable link. It now rolls the policy back and throws when a share has no resolvable URL, alongside the existing embed-failure rollback (factored into one helper). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(hub-projects): count URL-less iframe re-sync; evict failed preview caches Two Codex findings, both refinements of earlier fixes: - The post-deploy iframe re-sync skipped a published raw app whose public URL was missing (URL resolution had failed) without counting it, so the re-bundle left the app public with a cleared Hub embed yet the draft still became submit-ready. Treat a published raw app with no resolvable URL as an incomplete publish and count it like a push failure. - The bundle-preview dependency caches memoized promises that resolve to undefined after transient item/resource fetch failures, so fixing or retrying a dependency could never clear `bundlePreview.unresolved` and the Create bundle button stayed disabled until the session was recreated. Evict a cache entry once it resolves to undefined so a later rebuild re-fetches. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(hub-projects): keep Unpublish for a public app whose URL didn't resolve The iframe controls required both `published` and `publicUrl`, so an anonymous app whose public-URL lookup failed rendered as unshared with only a Share action and no way to unpublish. Branch the Public badge and Unpublish on `published` alone, gate the URL-dependent Open/Copy-iframe actions on `publicUrl`, and offer a Retry link that re-resolves the URL when it is missing. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(hub-projects): make $var/$jsonvar dependencies portable on import Variable references were neither retargeted nor materialized, so a published project that used a variable broke on import: a renamed-folder import rejected the containing item (the `$var:` kept the old folder prefix), and a same-folder import left the reference dangling (the target variable never existed). Treat variables like resource stubs, fully on the import side (their `$var:`/ `$jsonvar:` refs already travel inside the exported item values): - `buildRetargetMap` now also relocates the internal variable paths embedded in the export's flows/apps/triggers, and `rewriteContent` rewrites `$var:`/ `$jsonvar:` tokens (kind preserved) for any path in the map — so the publish map, which omits variables, is unaffected. - `installProject` creates an empty secret placeholder for each in-folder variable ref, conflict-safe via `existsVariable`, for the importer to fill. Values are never shipped. External refs stay rejected by containment. Custom resource-type definitions (the sibling finding) are intentionally left to the standardized official Hub resource types, so no schema import is needed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(hub-projects): relocate $var refs structurally, never in inline code Routing variable retargeting through `rewriteContent` also rewrote `$var:`/ `$jsonvar:` tokens embedded in script source, inline rawscript, and serialized app strings, so an inert literal sharing a real variable's path was silently altered on a renamed-folder import — contradicting the whole-string runtime-reference rule. Relocate variables with a structural walk (`rewriteVarRefsInValue`) that rewrites only whole-string `$var:`/`$jsonvar:` values (the sole form the worker resolves), applied to flow/app/trigger values in `retargetProjectExport`; `rewriteContent` is back to `$res:`-only. Inline code literals are left untouched. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(hub-projects): relocate $var refs into the slug at publish Import-side retargeting assumed exported `$var:`/`$jsonvar:` refs already began with the project slug, but `buildProjectBundle` never relocated them from the source folder. Publishing `f/source_folder/...` as slug `my-toolkit` therefore exported `$var:f/source_folder/key`; import (fromSlug=my-toolkit) left it unchanged and containment rejected the item. Collect each item's runtime variable refs, feed them through the same path map that relocates items/resources into `f/<slug>/`, and structurally rewrite the whole-value refs — symmetric with the import retarget. The export is now slug-relative whatever the source folder, and inline-code literals stay untouched. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(hub-projects): relocate trigger config $var refs at publish Item variable refs were relocated into the slug, but triggers publish through a separate path (`#pushTriggers` → `rewriteTriggerConfig`), which doesn't touch `$var:`/`$jsonvar:`. Publishing `f/source/...` under a different Hub slug left schedule args and other config refs pointing at `f/source/...`, and import containment then rejected the trigger. Collect each trigger config's whole-string variable refs (`#triggerVarPaths`), feed them through the bundle path map via a new `extraVarPaths` arg to `buildProjectBundle`, and structurally rewrite the config on publish. Symmetric with the item and import-side handling; the import retarget already relocated trigger vars. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(hub-projects): correct varContainmentViolation retargeting contract The comment claimed retargeting doesn't rewrite variable refs; it now relocates a project's own refs into the target folder, and containment rejects only those left outside it. Describe the current behavior. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: Ruben Fiszel <ruben@windmill.dev> Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com> Co-authored-by: hugocasa <hugo@casademont.ch> Co-authored-by: centdix <40307056+centdix@users.noreply.github.com> Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com> Co-authored-by: Aldrin Jenson <aldrinjenson@gmail.com> Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> Co-authored-by: centdix <centdix@users.noreply.github.com> Co-authored-by: Alexander Petric <alpetric@users.noreply.github.com> Co-authored-by: Diego Imbert <70353967+diegoimbert@users.noreply.github.com> Co-authored-by: Guilhem <guilhemlemouel@gmail.com> Co-authored-by: Diego Imbert <diego@windmill.dev>
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.






