- name
- write-flow
- description
- MUST use when creating flows.
# Windmill Flow CLI Guide
## Creating a Flow
**You — the AI agent — scaffold the flow yourself by running `wmill flow new <path>` with the right flags. Do NOT hand-create the folder + `flow.yaml`, and do NOT tell the user to "run `wmill flow new` and follow the prompts".**
`wmill flow new` creates the folder with the correct suffix (`__flow` or `.flow` depending on the workspace's `nonDottedPaths` setting), writes a minimal `flow.yaml` shell, and prints Claude-specific next-step hints. Scaffolding by hand skips all of that and often picks the wrong suffix.
### Step 1 — Gather path + summary by asking the user
You need two things:
1. **path** — the windmill path, e.g. `f/folder/my_flow` or `u/username/my_flow`.
2. **summary** — a short description of the flow.
If the user's request didn't supply both, ask for both in a single round-trip. Use whichever interactive question facility your runtime provides — a structured multi-choice tool if available, otherwise plain chat — and provide one or two example values for each (with an "Other" / free-form fallback). Do not guess paths or summaries.
### Step 2 — Run the command yourself
```bash
wmill flow new f/folder/my_flow --summary "Short description"
```
Add `--description "..."` when the user provided a longer explanation worth preserving separately from the summary.
### Step 3 — Fill in `flow.yaml`
Open the generated `flow.yaml` (under the folder the command just created) and replace the empty `value.modules` + `schema` with the real flow definition.
For rawscript modules, use `!inline path/to/script.ts` for the content key. Inline script files should NOT include `.inline_script.` in their names (e.g. use `a.ts`, not `a.inline_script.ts`).
Once the flow has real content, **offer** to open the visual preview as a one-sentence next step (e.g. "Want me to open the visual preview?"). Don't auto-open — opening the dev page has side effects (browser window, possibly a `launch.json` entry) and the user should consent.
### Anti-patterns to avoid
- ❌ Hand-creating the `__flow` folder + `flow.yaml` instead of running `wmill flow new`. You'll miss the suffix-setting resolution, the default shape, and the Claude hints.
- ❌ Telling the user to "run `wmill flow new <path>`" — you can and should run it yourself.
- ❌ Inventing a path/summary instead of asking the user.
## CLI Commands — running, previewing, deploying
After writing, act on the user's intent instead of just listing commands. Run `wmill flow preview` yourself when it fits (see "After writing — offer to run, don't wait passively" below). `wmill generate-metadata` regenerates local lock/hash files (not a deploy) but re-resolves deps — offer it and run on agreement, unless the project's `AGENTS.md` opts into running metadata automatically. Only *name* `wmill sync push` (the deploy) so the user can approve it. The options:
- `wmill flow preview <flow_path>` — **default when iterating on a local flow.** Runs the local `flow.yaml` against local inline scripts without deploying. Add `--remote` to use deployed workspace scripts for PathScript steps instead of local files. Add `--step <step_id>` to run only one module in isolation (see "Single-step vs whole-flow preview" below).
- `wmill flow run <path>` — runs the flow **already deployed** in the workspace. Use only when the user explicitly wants to test the deployed version, not local edits.
- `wmill generate-metadata` — regenerate stale local `.lock` files for the flow and its inline scripts and refresh their content hashes in `wmill-lock.yaml`. Writes local files only (not a deploy). Run it after editing inline scripts whose imports or arguments changed, so `wmill-lock.yaml` doesn't drift and add noise to git-sync/CI. By default it scans **scripts, flows, and apps** across the workspace but only regenerates stale ones; pass the flow's folder as an argument (or run from that subdirectory) to limit the scope to the flow you edited. Note a flow (or script) that imports a changed shared script is pulled in too — run `wmill generate-metadata --dry-run` to see exactly what is stale and why (`content changed` vs `depends on <path>`) before applying.
- Deploy local changes to the workspace — via `git push` or `wmill sync push` depending on how the repo is wired (see the **Deploying** section in `AGENTS.wmill.md`). Only suggest/run a deploy when the user explicitly asks to deploy/publish/push — not when they say "run", "try", or "test".
### Preview vs run — choose by intent, not habit
If the user says "run the flow", "try it", "test it", "does it work" while there are **local edits to a `flow.yaml`**, use `flow preview`. Do NOT push the flow to then `flow run` it — pushing is a deploy, and deploying just to test overwrites the workspace version with untested changes.
Only use `flow run` when:
- The user explicitly says "run the deployed version" / "run what's on the server".
- There is no local `flow.yaml` being edited (you're just invoking an existing flow).
Only use `sync push` when:
- The user explicitly asks to deploy, publish, push, or ship.
- The preview has already validated the change and the user wants it in the workspace.
### Single-step vs whole-flow preview
Use `flow preview <flow_path> --step <step_id>` when the user is iterating on one module and the flow's upstream steps aren't part of what they're trying to validate. It runs only that step's runnable (rawscript: the inline script; script: the PathScript, locally if available; flow: the subflow by path) and is much faster than running the whole flow when previous steps are slow or expensive. The step id is resolved by walking nested branchone/branchall/forloopflow/whileloopflow modules and includes the special `preprocessor` and `failure` modules.
Use `flow preview <flow_path>` (no `--step`) when steps depend on each other's outputs, when the user is validating the overall control flow, or when `--step` doesn't apply (branchone, branchall, forloopflow, whileloopflow, identity, and AI agent steps cannot themselves be tested in isolation — for branchone/branchall/forloopflow/whileloopflow, the *contained* steps can, by passing the inner step's id).
### After writing — offer to run, don't wait passively
This is about **programmatic execution** (`wmill flow preview -d '<args>'`), which actually runs the flow and has side effects. Visual preview (the `preview` skill) is offered separately — see "Visual preview" below.
If the user hasn't already told you to run/test the flow, offer it as a one-sentence next step (e.g. "Want me to run `wmill flow preview` with sample args?"). Do not present a multi-option menu.
If the user already asked to test/run/try the flow in their original request, skip the offer and just execute `wmill flow preview <path> -d '<args>'` directly — pick plausible args from the flow's input schema.
An input typed as a resource (`format: resource-<type>` in the schema) takes the bare string `"$res:<path>"` as its whole value — `-d '{"db": "$res:f/databases/postgres_prod"}'`, not `{"db": {"$res": "..."}}` and not a plain path. Same for a variable, with `"$var:<path>"`. See the `resources` skill.
`wmill flow preview` is safe to run yourself (it does not deploy). `wmill generate-metadata` does not deploy either (it only writes local lock/hash files) but re-resolves deps — offer it and run on agreement, unless the project's `AGENTS.md` opts into automatic metadata. After running it, check the regenerated `.lock` diff and tell the user which inline-script dependency versions changed, so they can catch an unwanted bump before deploying. Only `wmill sync push` deploys; run it only when the user explicitly asks.
### Visual preview
To open the flow visually in the dev page (graph + live reload), use the `preview` skill. Always **offer** it as a one-sentence next step (e.g. "Want me to open the visual preview?") rather than opening it automatically — opening the dev page has side effects (browser window, possibly a `launch.json` entry under MCP-preview branches) the user should consent to. If the user already asked to see/preview/visualize the flow in their original request, skip the offer and just invoke the skill.
# Windmill Flow Building Guide
## OpenFlow Schema
The OpenFlow schema (openflow.openapi.yaml) is the source of truth for flow structure. Refer to OPENFLOW_SCHEMA for the complete type definitions.
## Reserved Module IDs
- `failure` - Reserved for failure handler module
- `preprocessor` - Reserved for preprocessor module
- `Input` - Reserved for flow input reference
## Hard Structural Rules
These are strict Windmill schema rules. Follow them exactly.
- `value.modules` is only for normal sequential steps
- `value.preprocessor_module` and `value.failure_module` are special top-level fields inside `value`, not entries in `value.modules`
- If a flow needs a preprocessor, create `value.preprocessor_module` with `id: preprocessor`
- If a flow needs a failure handler, create `value.failure_module` with `id: failure`
- Do NOT create regular modules inside `value.modules` named `preprocessor` or `failure`
- `preprocessor_module` and `failure_module` only support `script` or `rawscript`
- `preprocessor_module` runs before normal modules and cannot reference `results.*`
- `failure_module` can use the `error` object with `error.message`, `error.step_id`, `error.name`, and `error.stack`
Correct shape:
```yaml
value:
preprocessor_module:
id: preprocessor
value:
type: rawscript
...
failure_module:
id: failure
value:
type: rawscript
...
modules:
- id: process_event
value:
type: rawscript
...
```
Incorrect shape:
```yaml
value:
modules:
- id: preprocessor
...
- id: process_event
...
- id: failure
...
```
## Module ID Rules
- Must be unique across the entire flow
- Use underscores, not spaces (e.g., `fetch_data` not `fetch data`)
- Use descriptive names that reflect the step's purpose
## AI Agent Modules
An `aiagent` module runs an LLM that can call tools. Each entry of `value.tools` is a module-shaped
object with an extra `value.tool_type`: `flowmodule` for a script/flow tool, `mcp` for an MCP server
tool, `websearch` for web search.
```json
{
"id": "support_agent",
"summary": "AI agent for customer support",
"value": {
"type": "aiagent",
"input_transforms": {
"provider": {
"type": "static",
"value": { "kind": "openai", "resource": "$res:f/ai_providers/openai", "model": "gpt-4o" }
},
"output_type": { "type": "static", "value": "text" },
"user_message": { "type": "javascript", "expr": "flow_input.query" },
"system_prompt": { "type": "static", "value": "You are a helpful assistant." }
},
"tools": [
{
"id": "search_docs",
"summary": "search_documentation",
"description": "Search the product documentation. Use it whenever the user asks how a feature works.",
"value": {
"tool_type": "flowmodule",
"type": "rawscript",
"language": "bun",
"content": "export async function main(query: string) { return ['doc1', 'doc2']; }",
"input_transforms": { "query": { "type": "static", "value": "" } }
}
}
]
}
}
```
- `provider` is an object, not a bare resource string: `{ "kind": <provider kind>,
"resource": "$res:<path>", "model": <model id> }`. Required unless the module links to a saved
agent through `value.agent`. Static is right for a flow run from a form; a chat flow wires its
fields to flow inputs instead — see below
### Chat-Mode Flows
A flow with `value.chat_input_enabled: true` is run from a chat instead of a form: the composer
sends one message per turn and renders the conversation. It needs a required `user_message` string
input, read by the agent. Any other flow input the composer does not edit itself is asked for
under Configure inputs.
**A static `provider` gives a chat that cannot change its model.** Feed it from flow inputs
instead, either way round: one input carrying the whole object (`"expr": "flow_input.model_config"`)
makes every field editable, or wire it field by field to fix some and expose others. A field the
chat can write becomes a control in the composer — a provider picker, a model list, a thinking
control — and a field left static is fixed, with no control drawn for it. `kind` is the one
exception: the composer writes it only together with `resource`, since a provider is picked as a
pair, so a `kind` input wired on its own stays askable under Configure inputs and nothing the run
needs becomes unreachable.
```json
{
"id": "chat_agent",
"value": {
"type": "aiagent",
"input_transforms": {
"provider": {
"type": "javascript",
"expr": "({ kind: 'anthropic', resource: '$res:f/ai/claude', model: flow_input.model, reasoning_effort: flow_input.thinking })"
},
"user_message": { "type": "javascript", "expr": "flow_input.user_message" },
"user_attachments": { "type": "javascript", "expr": "flow_input.files" },
"memory": { "type": "static", "value": { "kind": "window", "context_length": 10 } },
"streaming": { "type": "static", "value": true },
"output_type": { "type": "static", "value": "text" }
},
"tools": []
}
}
```
- Wiring field by field means one object literal whose values are literals or bare `flow_input.x`
references. A spread, a call or a computed key leaves the composer unable to tell which input
feeds which field, so it offers no control at all — a bare `flow_input.x` for the whole object
is read instead as that one input carrying every field
- `memory` is what lets the agent see earlier turns; without it every message starts from nothing
- `streaming` on makes the answer and its thinking appear token by token instead of all at once
- `user_attachments` points at a flow input typed as an array of s3 objects
(`{ "type": "array", "items": { "type": "object", "resourceType": "s3object" } }`), so files
sent with a message reach the agent
- Running one needs a `memory_id` **query parameter** — not a flow argument — naming the
conversation the turn belongs to: a fresh UUID starts one, reusing a UUID continues it. The chat
supplies it itself; a run driven any other way has to pass it or the server refuses the job
### Tool Naming Rules
These rules cover `flowmodule` tools, the ones the agent calls by name. A `websearch` tool's
`summary` is a plain label (`Web Search`), and an `mcp` tool exposes the MCP server's own tool
names, so neither is name-checked at all — leave those summaries as they are.
- A flowmodule tool's `summary` is the **name the agent calls it by**, not a human label. Put the
human-readable explanation in `description`
- `summary` must match `^[a-zA-Z0-9_]+$`: letters, numbers and underscores only. No spaces, dashes,
dots or accents — `search_documentation`, never `Search documentation`
- Always set `summary`. It must be unique among that agent's tools, and must not be one of the
reserved ids (`do`, `bg`, `ctx`, `state`, `if`, `else`, `for`, `delete`, `while`, `new`, `in`,
`failure`, `preprocessor`, `as`, `Input`, `Result`, `Trigger`)
- A tool name outside that character set is rejected: flow write tools refuse it, and a flow that
reaches the worker with one fails every run with `Invalid tool name`
- Tool `id` follows the same rules as any module ID — unique across the flow, underscores not spaces
- `description` is optional free text telling the agent when and how to call the tool. Set it
whenever the name alone does not make that obvious; it overrides the description derived from the
underlying script
## Common Mistakes to Avoid
- Missing `input_transforms` - Rawscript parameters won't receive values without them
- Referencing future steps - `results.step_id` only works for steps that execute before the current one
- Duplicate module IDs - Each module ID must be unique in the flow
- AI agent flowmodule tool names with spaces - `summary` is the tool name and only accepts letters, numbers and underscores
## Data Flow Between Steps
- `flow_input.property` - Access flow input parameters
- `results.step_id` - Access output from a previous step only when that step result is in scope
- `results.step_id.property` - Access specific property from a previous step output only when that step result is in scope
- `flow_input.iter.value` - Current iteration value inside a `forloopflow`; in a `whileloopflow` it is just the iteration index (a plain number, same as `flow_input.iter.index`)
- `flow_input.iter.index` - Current loop index when inside a loop (`forloopflow` or `whileloopflow`)
## Loop Structure Rules
- For `whileloopflow`, break the loop with a module-level `stop_after_if`: on the loop module itself, or on an inner step (required when that step carries state via its own `results` — see below)
- `stop_after_if` is always a sibling of `id` and `value` on a flow module — never a direct key of the loop's `value` object
View on GitHub