* feat: sandbox new low-code and raw apps by default Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: navigate the top page from plain links in a sandboxed low-code app Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: allow clipboard and fullscreen in sandboxed raw-app frames Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: leave non-http links to the browser in the sandboxed app frame Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: let a link's own click handlers run before relaying it out of the frame Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: read link attributes so SVG anchors relay correctly Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * refactor: tidy the app deploy drawer to the brand guidelines Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * refactor: show enabled frontend api scopes as badges, rename runs-as section Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: keep frontend api access collapsed by default, badges next to its title Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * refactor: lead the run-on-behalf-of section with the user picker Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs: explain publisher execution and WM_END_USER_EMAIL in the run-on-behalf-of tooltip Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs: shorten the run-on-behalf-of tooltip, mention viewer data access for raw apps Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs: tell the AI what breaks in a sandboxed raw app and that turning it off is a security risk Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat: warn in the deploy drawer when an app is not isolated Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * refactor: drop the sandbox toggle help line Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs: say when to turn sandbox isolation off in the toggle tooltip Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * refactor: show who a non-deployer's app runs as, read-only Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: show a new app runs as the deployer, gray read-only user line Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * refactor: read-only on-behalf-of display as an opt-in selector prop Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: show a folder's default run-as user on a new app instead of the deployer Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: deploy an app under a group folder default without a mismatched email Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * revert: leave a new app's run-as user unselected, as on main Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat: warn in the app deploy drawer when a deploy changes the run-as user Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: snapshot the deployed policy so later edits don't change the saved app Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
System Prompts
The single source of the AI guidance Windmill ships: what an agent needs to know about Windmill itself to write flows, scripts, apps, pipelines, resources, triggers and schedules. Two consumers are built from it:
- the chat (the frontend AI chat: flow, script, app and global modes), through the
$system_promptsalias toauto-generated/; - the CLI (
wmill init/wmill refresh prompts), as the skills embedded incli/src/guidance/skills.gen.ts, next to theAGENTS.wmill.mdtemplate incli/src/guidance/core.ts.
Guidance written in one consumer only is guidance the other never gets. Write it here.
Structure
system_prompts/
├── base/ # Hand-written guidance, one file per topic (flow-base.md, raw-app.md, …)
│ # plus CLI-only intros (flow-cli.md, script-cli.md, raw-app-cli.md, …)
├── languages/ # Hand-written per-language guidance (bun.md, python3.md, …)
├── generate.py # Builds everything under auto-generated/ and cli/src/guidance/skills.gen.ts
├── utils.py # Shared helpers, including the fence renderer
└── auto-generated/ # Generated — never edit
├── prompts.ts # One export per base/ and languages/ file (chat render), SDK docs, schema
├── index.ts # Chat helpers (getFlowPrompt, getScriptPrompt, …)
├── skills/ # One SKILL.md per CLI skill (cli render)
├── sdks/ # SDK reference extracted from the TypeScript and Python clients
└── cli/ # CLI command reference extracted from cli/src/commands/
How a topic is assembled
TOPICS in generate.py lists, for each topic, the parts both consumers concatenate in order:
a file under base/ or languages/, or a token filled per consumer ({lang}, {sdk},
{wac_sdk}, {openflow_schema}, {cli_commands}). The same entry produces the topic's chat
helper in index.ts and its CLI skill, so a part added there reaches both. A part tagged
('chat', …) or ('cli', …) goes to that consumer only, and cli_intro heads the CLI skill with
a CLI-only workflow file (flow-cli.md, script-cli.md, raw-app-cli.md).
| Topic | Chat helper | CLI skill | Shared parts |
|---|---|---|---|
| script | getScriptPrompt(lang) |
write-script-<lang> |
script-base.md, the language file, its SDK |
| flow | getFlowPrompt() |
write-flow |
flow-base.md, the OpenFlow schema |
| raw app | getRawAppPrompt(lang) |
raw-app |
raw-app.md (chat adds the SDK) |
| resources | getResourcePrompt() |
resources |
resources.md |
| workflow-as-code | getWorkflowAsCodePrompt(lang) |
write-workflow-as-code |
workflow-as-code.md, the WAC SDK |
| pipeline | getPipelinePrompt() |
write-pipeline |
pipeline-base.md |
| data tables | getDatatablePrompt() |
datatables |
datatable.md |
| workspace settings | getWorkspaceSettingsPrompt() |
workspace-settings |
workspace-settings.md |
| triggers, schedules, preview, CLI commands | — | same names | CLI only |
The app chat embeds RAW_APP_BASE directly (chat/app/core.ts).
Consumer fences
A sentence that only one consumer should see stays in the shared file, fenced:
A tool name outside that character set fails every run of the flow.
<!-- chat-only -->
The flow write tools refuse such a name.
<!-- /chat-only -->
<!-- cli-only -->
`wmill lint <flow folder>` reports it before anything runs.
<!-- /cli-only -->
render_for (utils.py) renders every file once per consumer: prompts.ts gets the chat render,
the skills get the cli render, and the fence lines reach neither. Fences sit on their own lines and
cannot nest; an unbalanced or misspelled fence fails generation (an HTML comment opening with cli
or chat, or ending in only, is read as a fence attempt).
Use a fence for a sentence or a section. When most of a topic differs per consumer, a CLI-only
file used as cli_intro reads better than a file that is mostly fences.
What goes where
- Here: anything true of Windmill regardless of who is asking — module shapes, data flow rules, what the editor accepts, how data tables, secrets or app access work.
- In the chat's TypeScript prompt builders (
frontend/src/lib/components/copilot/chat/*/core.ts): only tool plumbing (which tool to call, its arguments) and values known at run time (the user's name, this app's data-table policy, session capabilities). - In
cli/src/guidance/core.ts(AGENTS.wmill.md): project-level CLI workflow — which skill to use, deploying, debugging jobs.
A file in TOPICS names no chat tool, not even inside a chat-only fence: it reaches every chat
mode (flow mode's system prompt, global mode's get_instructions), each with its own tool names,
and global/sessionToolset.test.ts fails a prompt that names a tool its session lacks. Describe the
action instead ("the flow write tools refuse such a name") and name the tool in TypeScript. The one
exception is flow-chat-special-modules.md, which only flow mode reads.
Regenerating
After editing anything here, or when SDK methods, the OpenFlow schema or CLI commands change:
python system_prompts/generate.py
CI (.github/workflows/check-system-prompts.yml) runs system_prompts/check-freshness.sh, which
fails when the committed output is stale.
To also refresh the standalone skills in a Claude plugin checkout:
python system_prompts/generate.py --plugin-dir ~/windmill-claude-plugin
--plugin-dir accepts:
- the
windmill-claude-pluginrepo root - a plugin root such as
plugins/windmill - a direct
skills/directory
To regenerate the public docs repo (consumed by context7):
python system_prompts/generate.py --context7-dir ~/windmill-cli-docs
--context7-dir writes a fully-rendered snapshot (AGENTS.md,
cli-commands.md, skills/<name>/SKILL.md, README.md, manifest.json
with the Windmill version) with all template placeholders resolved —
suitable for ingestion by docs aggregators. In CI this runs from
.github/workflows/publish-cli-docs.yml on every release tag. The
generator refuses to wipe the target directory unless it's empty or has
a context7 marker (context7.json, manifest.json, or a
windmill-cli-docs git remote), so a typo can't delete unrelated files.