* fix(ai): stop teaching nonexistent while-loop iter.value state-carrying Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ai): scope while-loop results guidance to cross-iteration reads only Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ai): drop unverified wmill state-helper fallback from while-loop guidance Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ai): document supported cross-iteration results state in while loops Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ai): rescope while-loop fast-path rule and add results-carrying example Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
10 KiB
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
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
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
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"
}
}