Windmill Flow CLI Guide
SkillDev toolsGuides your agent to create flows in Windmill the required way.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Windmill Flow CLI Guide skill
About this capability
MUST use when creating flows.
What this skill tells your AI
The instructions your AI receives, as published by windmill-labs/windmill in system_prompts/auto-generated/skills/write-flow/SKILL.md and read by ahel’s review.
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:
- path — the windmill path, e.g.
f/folder/my_floworu/username/my_flow. - 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
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
__flowfolder +flow.yamlinstead of runningwmill 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 localflow.yamlagainst local inline scripts without deploying. Add--remoteto 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.lockfiles for the flow and its inline scripts and refresh their content hashes inwmill-lock.yaml. Writes local files only (not a deploy). Run it after editing inline scripts whose imports or arguments changed, sowmill-lock.yamldoesn'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 — runwmill generate-metadata --dry-runto see exactly what is stale and why (content changedvsdepends on <path>) before applying.- Deploy local changes to the workspace — via
git pushorwmill sync pushdepending on how the repo is wired (see the Deploying section inAGENTS.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.yamlbeing 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 modulepreprocessor- Reserved for preprocessor moduleInput- Reserved for flow input reference
Hard Structural Rules
These are strict Windmill schema rules. Follow them exactly.
value.modulesis only for normal sequential stepsvalue.preprocessor_moduleandvalue.failure_moduleare special top-level fields insidevalue, not entries invalue.modules- If a flow needs a preprocessor, create
value.preprocessor_modulewithid: preprocessor - If a flow needs a failure handler, create
value.failure_modulewithid: failure - Do NOT create regular modules inside
value.modulesnamedpreprocessororfailure preprocessor_moduleandfailure_moduleonly supportscriptorrawscriptpreprocessor_moduleruns before normal modules and cannot referenceresults.*failure_modulecan use theerrorobject witherror.message,error.step_id,error.name, anderror.stack
Correct shape:
value:
preprocessor_module:
id: preprocessor
value:
type: rawscript
...
failure_module:
id: failure
value:
type: rawscript
...
modules:
- id: process_event
value:
type: rawscript
...
Incorrect shape:
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_datanotfetch 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.
{
"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": "" } }
}
}
]
}
}
provideris a static object, not a bare resource string:{ "kind": <provider kind>, "resource": "$res:<path>", "model": <model id> }. Required unless the module links to a saved agent throughvalue.agent
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
summaryis the name the agent calls it by, not a human label. Put the human-readable explanation indescription summarymust match^[a-zA-Z0-9_]+$: letters, numbers and underscores only. No spaces, dashes, dots or accents —search_documentation, neverSearch 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
idfollows the same rules as any module ID — unique across the flow, underscores not spaces descriptionis 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_idonly 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 -
summaryis the tool name and only accepts letters, numbers and underscores
Data Flow Between Steps
flow_input.property- Access flow input parametersresults.step_id- Access output from a previous step only when that step result is in scoperesults.step_id.property- Access specific property from a previous step output only when that step result is in scopeflow_input.iter.value- Current iteration value inside aforloopflow; in awhileloopflowit is just the iteration index (a plain number, same asflow_input.iter.index)flow_input.iter.index- Current loop index when inside a loop (forloopfloworwhileloopflow)
Loop Structure Rules
- For
whileloopflow, break the loop with a module-levelstop_after_if: on the loop module itself, or on an inner step (required when that step carries state via its ownresults— see below) stop_after_ifis always a sibling ofidandvalueon a flow module — never a direct key of the loop'svalueobjectstop_after_all_iters_ifis for checks after the whole loop finishes, not the normal per-iteration break conditionflow_input.iter.valuein awhileloopflowis just the iteration index (same number asflow_input.iter.index) — it never carries state, soflow_input.iter.value.<field>is always undefined and a loop whose stop condition depends on it never terminates- To carry state across iterations, a step reads its own previous-iteration result via
results.<its_own_id>with a first-iteration fallback (e.g.results.b ?? flow_input.start) — but then the loop'sstop_after_ifMUST sit on that inner step, not on the loop module: a body that is exactly one plain step with the stop condition on the loop module runs on a fast path whereresults.<step_id>is null on every iteration and the loop never terminates (bodies with 2+ steps, or whose single step has its ownstop_after_if, retry or similar, resolveresultsacross iterations regardless of stop placement) - For state that is just a counter, derive it from the index instead (e.g.
flow_input.iter.index + 1) — that works in every configuration, including withstop_after_ifon the loop module - If the user asks for a final scalar/object after a loop, add a normal step after the loop that extracts the final value from the loop result instead of returning the whole loop result array
Correct whileloopflow shape:
- id: loop_until_done
stop_after_if:
expr: result.done === true
skip_if_stopped: false
value:
type: whileloopflow
skip_failures: false
modules:
- id: advance_state
value:
type: rawscript
input_transforms:
count:
type: javascript
expr: flow_input.iter.index + 1
- id: return_final_state
value:
type: rawscript
input_transforms:
final_state:
type: javascript
expr: results.loop_until_done[results.loop_until_done.length - 1]
Correct whileloopflow shape carrying state via results (stop condition on the inner step):
- id: loop_until_done
value:
type: whileloopflow
skip_failures: false
modules:
- id: advance_state
stop_after_if:
expr: result.done === true
skip_if_stopped: false
value:
type: rawscript
input_transforms:
state:
type: javascript
expr: results.advance_state ?? flow_input.initial_state
Incorrect whileloopflow patterns:
- id: loop_until_done
value:
type: whileloopflow
stop_after_if:
expr: result.done === true
input_transforms:
state:
type: javascript
# iter.value is a number (the iteration index); there is no previous-iteration state
expr: flow_input.iter.value.count
input_transforms:
final_state:
type: javascript
expr: results.loop_until_done
Approval / Suspend Structure
An approval step is a normal script step (type: rawscript or type: script) that is turned into an approval by adding a module-level suspend. Its script calls wmill.getResumeUrls(approver) to generate the secret resume/cancel URLs and returns them so they can be sent to the approver(s) (Slack, email, etc.) or approved from the run page.
suspendbelongs on the flow module object itself, as a sibling ofidandvalue- Never put
suspendinsidevalue - Do NOT use
type: identityfor an approval step. An identity step suspends but never produces the resume URLs, so approvers have no link to act on — it is not a functional approval.
Correct shape:
- id: request_approval
suspend:
required_events: 1
resume_form:
schema:
type: object
properties:
comment:
type: string
required: [comment]
value:
type: rawscript
language: bun
input_transforms:
approver:
type: static
value: ''
content: |
import * as wmill from "windmill-client"
export async function main(approver?: string) {
const urls = await wmill.getResumeUrls(approver)
// send urls.resume / urls.cancel to the approver(s), e.g. via Slack or email
return urls
}
Incorrect shape (suspend misplaced inside value):
- id: request_approval
value:
type: rawscript
suspend:
required_events: 1
Incorrect shape (identity has no resume URLs — not a real approval):
- id: request_approval
suspend:
required_events: 1
value:
type: identity
Branch Result Scope Rules
- Inside a branch, you may reference earlier outer steps and earlier steps in the same branch
- Outside a
branchone, do NOT reference ids of steps that only exist inside its branches or default branch. Useresults.<branchone_module_id>instead - Outside a
branchall, do NOT reference ids of steps inside its branches. Useresults.<branchall_module_id>instead - If downstream steps need a stable shape after a branch, make each branch return the same fields
- When needed, add a normalization step immediately after the branch and consume
results.<branch_module_id>there
Correct after branchone:
- id: route_order
value:
type: branchone
...
- id: send_confirmation
value:
input_transforms:
routed:
type: javascript
expr: results.route_order
Incorrect after branchone:
expr: results.create_shipment
expr: results.create_backorder
Correct after branchall:
- id: enrich_parallel
value:
type: branchall
parallel: true
...
- id: combine_data
value:
input_transforms:
enrichments:
type: javascript
expr: results.enrich_parallel
Input Transforms
Every rawscript module needs input_transforms to map function parameters to values:
Static transform (fixed value): {"param_name": {"type": "static", "value": "fixed_string"}}
JavaScript transform (dynamic expression): {"param_name": {"type": "javascript", "expr": "results.previous_step.data"}}
Resource References
- For flow inputs: Use type
"object"with format"resource-{type}"(e.g.,"resource-postgresql") - For step inputs: Use static value
"$res:path/to/resource"
Final Structural Self-Check
Before finalizing a flow, verify:
- any preprocessor is in
value.preprocessor_module - any failure handler is in
value.failure_module - any approval step has module-level
suspend - no downstream step references inner branch step ids from outside the branch
- every AI agent flowmodule tool has a unique
summarymade only of letters, numbers and underscores
S3 Object Operations
Windmill provides built-in support for S3-compatible storage operations.
To accept an S3 object as flow input:
{
"type": "object",
"properties": {
"file": {
"type": "object",
"format": "resource-s3_object",
"description": "File to process"
}
}
}
Using Resources in Flows
On Windmill, credentials and configuration are stored in resources. Resource types define the format of the resource.
As Flow Input
In the flow schema, set the property type to "object" with format "resource-{type}":
{
"type": "object",
"properties": {
"database": {
"type": "object",
"format": "resource-postgresql",
"description": "Database connection"
}
}
}
As Step Input (Static Reference)
Reference a specific resource using $res: prefix:
{
"database": {
"type": "static",
"value": "$res:f/folder/my_database"
}
}
OpenFlow Schema
Shortened here. Read the whole file on GitHub.
Signals
- GitHub stars
- 18k
- Forks
- 1k
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
write-flow- Source
- github.com/windmill-labs/windmill