Files
windmill/system_prompts
08319a004c feat: allow early return on steps inside branches (#11733)
* feat: allow early return on steps inside a branch to one

The "Early return for sync webhooks" flow setting could only point at a
top-level step. It can now also point at a step inside a branch to one,
in any branch including the default one, and through nested branches to
one.

When the branch is taken, the sync call returns that step's result as
soon as it completes while the rest of the flow keeps running. When it
is not taken, the call returns the flow's final result. The sync wait
loops already resolved nested steps and fell back to the final result,
so they are unchanged.

Worker: check_if_early_return_or_last_in_early_return_parent now
accepts a job whose step id matches early_return at any depth, as long
as every job between it and the root is a branch to one. A loop
iteration or a subflow reusing the same id does not take the stream.
The existing case, the last step of a branch to one that is itself the
early-return step, is unchanged.

Frontend: the native <select> is replaced by EarlyReturnPicker, a
searchable list in flow order where each nested step shows its path
(e.g. `a › Branch 1`). Branch to all and loops inside a branch to one,
and the steps inside them, are not offered: the results lookup cannot
resolve them when nested. Picking a nested step shows a hint that it
only runs if its branch is taken.

* fix: stream the last step when the early return branch is not taken

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: keep the stream off the last step once the early return step ran

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor: list early return steps by id only

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor: use the standard select for the early return step

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: match the early return step by its flow scope, not its path

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: keep branch to all and loop steps off the early return stream

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat: allow early return on steps inside a branch to all

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: resolve a nested branch to all or loop as the early return step

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test: pin which step streams for an early return inside branches

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: return an empty nested loop or branch to all as its early return list

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* revert: read an empty container's early return status from the root

An empty nested loop or branch to all falls back to the flow's final result again,
so an empty top-level loop that failed keeps its real status.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: stefan <stefan.radovanovic@ip-paris.fr>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 23:19:29 +02:00
..

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_prompts alias to auto-generated/;
  • the CLI (wmill init / wmill refresh prompts), as the skills embedded in cli/src/guidance/skills.gen.ts, next to the AGENTS.wmill.md template in cli/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-plugin repo 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.