Files
Guilhem 2b4369d7cb fix: reject invalid AI agent tool names when the chat writes a flow (#10756)
* fix: reject invalid AI agent tool names when the chat writes a flow

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: address review nits on agent tool name validation

Share one AI-agent walk between the providerless-agent and invalid-tool-name
collectors, drop the unused validateToolName, and list every reserved id in the
tool naming rules.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: describe an agent tool's summary as the name the agent calls it by

The OpenFlow schema described `AgentTool.summary` as a short description of
the tool, which is the same schema the flow write tools hand the model, so it
pulled against the naming rules. Narrow those rules to flowmodule tools, since
websearch and mcp tool names are never regex-checked, and let `kind` take
either vocabulary its callers resolve.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: name-check only the agent tools whose summary the agent calls

An mcp tool exposes the MCP server's own tool names and a websearch tool's
summary is a plain label, so neither reaches the worker's name check. Both
default to an empty summary in the editor, which the chat then refused to
write back.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 10:49:01 +02:00

13 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 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:

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_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.

{
  "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 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 through value.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 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
  • stop_after_all_iters_if is for checks after the whole loop finishes, not the normal per-iteration break condition
  • flow_input.iter.value in a whileloopflow is just the iteration index (same number as flow_input.iter.index) — it never carries state, so flow_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's stop_after_if MUST 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 where results.<step_id> is null on every iteration and the loop never terminates (bodies with 2+ steps, or whose single step has its own stop_after_if, retry or similar, resolve results across 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 with stop_after_if on 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.

  • suspend belongs on the flow module object itself, as a sibling of id and value
  • Never put suspend inside value
  • Do NOT use type: identity for 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. Use results.<branchone_module_id> instead
  • Outside a branchall, do NOT reference ids of steps inside its branches. Use results.<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 summary made 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"
  }
}