// Auto-generated by generate.py - DO NOT EDIT

export interface SkillMetadata {
  name: string;
  description: string;
  languageKey?: string;
}

export const SKILLS: SkillMetadata[] = [
  { name: "write-script-ansible", description: "MUST use when writing Ansible playbooks.", languageKey: "ansible" },
  { name: "write-script-bash", description: "MUST use when writing Bash scripts.", languageKey: "bash" },
  { name: "write-script-bigquery", description: "MUST use when writing BigQuery queries.", languageKey: "bigquery" },
  { name: "write-script-bun", description: "MUST use when writing TypeScript scripts. Bun is the default and preferred TypeScript runtime — pick it for TypeScript unless the script specifically needs Deno.", languageKey: "bun" },
  { name: "write-script-bunnative", description: "MUST use when writing Bun Native scripts. The script must start with //native to run on the native worker.", languageKey: "bunnative" },
  { name: "write-script-csharp", description: "MUST use when writing C# scripts.", languageKey: "csharp" },
  { name: "write-script-deno", description: "Use ONLY when a TypeScript script specifically requires the Deno runtime (Deno stdlib or deno.land URL imports). For all other TypeScript, use write-script-bun instead.", languageKey: "deno" },
  { name: "write-script-duckdb", description: "MUST use when writing DuckDB queries.", languageKey: "duckdb" },
  { name: "write-script-go", description: "MUST use when writing Go scripts.", languageKey: "go" },
  { name: "write-script-graphql", description: "MUST use when writing GraphQL queries.", languageKey: "graphql" },
  { name: "write-script-java", description: "MUST use when writing Java scripts.", languageKey: "java" },
  { name: "write-script-mssql", description: "MUST use when writing MS SQL Server queries.", languageKey: "mssql" },
  { name: "write-script-mysql", description: "MUST use when writing MySQL queries.", languageKey: "mysql" },
  { name: "write-script-php", description: "MUST use when writing PHP scripts.", languageKey: "php" },
  { name: "write-script-postgresql", description: "MUST use when writing PostgreSQL queries.", languageKey: "postgresql" },
  { name: "write-script-powershell", description: "MUST use when writing PowerShell scripts.", languageKey: "powershell" },
  { name: "write-script-python3", description: "MUST use when writing Python scripts.", languageKey: "python3" },
  { name: "write-script-rlang", description: "MUST use when writing R scripts.", languageKey: "rlang" },
  { name: "write-script-rust", description: "MUST use when writing Rust scripts.", languageKey: "rust" },
  { name: "write-script-snowflake", description: "MUST use when writing Snowflake queries.", languageKey: "snowflake" },
  { name: "write-flow", description: "MUST use when creating flows." },
  { name: "raw-app", description: "MUST use when creating raw apps." },
  { name: "triggers", description: "MUST use when configuring triggers." },
  { name: "schedules", description: "MUST use when configuring schedules." },
  { name: "resources", description: "MUST use when managing resources." },
  { name: "write-workflow-as-code", description: "MUST use when writing or modifying Windmill Workflow-as-Code scripts using workflow, task, step, sleep, approvals, taskScript, taskFlow, task_script, or task_flow." },
  { name: "cli-commands", description: "MUST use when using the CLI, including debugging job failures and inspecting run history via `wmill job`." },
  { name: "preview", description: "MUST use when opening the Windmill dev page / visual preview of a flow, script, or app. Triggers on words like preview, open, navigate to, visualize, see the flow/app/script, and after writing a flow/script/app for visual verification." },
];

// Skill content for each skill (loaded inline for bundling)
export const SKILL_CONTENT: Record<string, string> = {
  "write-script-ansible": `---
name: write-script-ansible
description: MUST use when writing Ansible playbooks.
---

## CLI Commands

Place scripts in a folder.

After writing, tell the user which command fits what they want to do:

- \`wmill script preview <script_path>\` — **default when iterating on a local script.** Runs the local file without deploying.
- \`wmill script run <path>\` — runs the script **already deployed** in the workspace. Use only when the user explicitly wants to test the deployed version, not local edits.
- \`wmill generate-metadata\` — regenerate the local \`.script.yaml\` (input schema) and \`.lock\` (resolved dependencies) for scripts you changed, and refresh their content hashes in \`wmill-lock.yaml\`. Local files only — **not** a deploy. See "Keep metadata in sync" below.
- 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 script", "try it", "test it", "does it work" while there are **local edits to the script file**, use \`script preview\`. Do NOT push the script to then \`script run\` it — pushing is a deploy, and deploying just to test overwrites the workspace version with untested changes.

Only use \`script run\` when:
- The user explicitly says "run the deployed version" / "run what's on the server".
- There is no local script being edited (you're just invoking an existing script).

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.

### Keep metadata in sync after editing

\`wmill-lock.yaml\` tracks a content hash for each item. Editing a script's content — most importantly **adding or removing an import** or **changing \`main\`'s arguments** — invalidates that hash and leaves the \`.lock\`, the \`.script.yaml\` input schema, and the hash row out of date. Run \`wmill generate-metadata\` (scoped to what you touched) after such edits so the resolved lock, the auto-generated args UI (driven by \`.script.yaml\`), and \`wmill-lock.yaml\` all match the code. Leaving them stale produces spurious diffs in git-sync and CI.

This only writes local files (it is **not** a deploy), but it re-resolves dependencies, so it can bump unpinned versions (the same as deploying from the UI; expected, not a bug). So by default offer it and run it once the user agrees, rather than running it silently after every edit — unless the project's \`AGENTS.md\` opts into running metadata automatically (see the "Keeping metadata in sync" preference there). Either way YOU run the command, not the user. After running it, diff the regenerated \`.lock\` / \`.script.lock\` files and tell the user which dependency versions changed (e.g. \`requests 2.31.0 → 2.32.0\`), so they can catch an unwanted bump before deploying — even under \`Metadata: auto\`, since it's information, not a confirmation gate. Pin versions in code to keep them fixed.

With no path argument, \`generate-metadata\` regenerates only the items whose content hash drifted — not everything. Imports propagate: editing a script that others import marks every importer stale too, so a one-line change to a shared module can regenerate many locks (by design — their locks must reflect the imported code). If it touches more than you expect, run \`wmill generate-metadata --dry-run\` — it lists each stale item with a reason (\`content changed\` or \`depends on <path>\`) without changing anything — then narrow with a path argument (\`wmill generate-metadata f/foo\`) or \`--strict-folder-boundaries\`.

If the on-disk \`.lock\` and \`.script.yaml\` are already correct and only \`wmill-lock.yaml\` needs its hashes refreshed (hash drift, or bootstrapping missing entries), use \`wmill generate-metadata rehash\` — it re-records hashes from disk with no backend round-trip and no dependency changes.

### After writing — offer to test, don't wait passively

If the user hasn't already told you to run/test/preview the script, offer it as a one-sentence next step (e.g. "Want me to run \`wmill script preview\` with sample args?"). Do not present a multi-option menu.

If the user already asked to test/run/try the script in their original request, skip the offer and just execute \`wmill script preview <path> -d '<args>'\` directly — pick plausible args from the script's declared parameters. The shape varies by language: \`main(...)\` for code languages, the SQL dialect's own placeholder syntax (\`$1\` for PostgreSQL, \`?\` for MySQL/Snowflake, \`@P1\` for MSSQL, \`@name\` for BigQuery, etc.), positional \`$1\`, \`$2\`, … for Bash, \`param(...)\` for PowerShell.

\`wmill script preview\` does not deploy, but it still executes script code and may cause side effects; run it yourself when the user asked to test/preview (or after confirming that execution is intended). \`wmill generate-metadata\` does not deploy either — it only writes local files (locks, schemas, hashes) — but offer it before running (or run automatically if the project's \`AGENTS.md\` opts in), per "Keep metadata in sync" above. Deploying to the workspace (\`git push\` or \`wmill sync push\` depending on how the repo is wired — see the **Deploying** section) is the only step that mutates remote state — do it only when the user explicitly asks to deploy/publish/push.

For a **visual** open-the-script-in-the-dev-page preview (rather than \`script preview\`'s run-and-print-result), use the \`preview\` skill.

Use \`wmill resource-type list --schema\` to discover available resource types.

# Ansible

Windmill runs Ansible playbooks with \`ansible-playbook\`. A script is a single YAML
document made of two parts separated by a \`---\` line: a Windmill **header** and one or
more standard Ansible **plays**.

## Structure

\`\`\`yaml
---
# Windmill header: configures inventories, file resources, arguments and dependencies
extra_vars:
  world_qualifier:
    type: string
dependencies:
  galaxy:
    collections:
      - name: community.general
  python:
    - jmespath
---
# Standard Ansible plays
- name: Echo
  hosts: 127.0.0.1
  connection: local
  tasks:
    - name: Print debug message
      debug:
        msg: "Hello, {{ world_qualifier }} world!"
\`\`\`

## Header

The header is **not** standard Ansible — it is parsed by Windmill to build the script's
inputs and runtime environment. Supported keys:

- \`extra_vars\`: defines the script arguments. Each entry is passed to the playbook via
  \`--extra-vars\` and becomes a Jinja variable usable as \`{{ name }}\` in the plays. Give
  each argument a \`type\` (\`string\`, \`number\`, \`boolean\`, \`object\`, ...) so Windmill can
  generate the input form.
- \`inventory\`: lists inventories. Use \`resource_type: ansible_inventory\` (optionally
  pinned with \`resource: u/user/your_resource\`) or \`resource_type: dynamic_inventory\`.
- \`files\`: writes Windmill resources/variables to files before the run, e.g.
  \`- resource: u/user/template\` with \`target: ./config.j2\`, or
  \`- variable: u/user/ssh_key\` with \`target: ./ssh_key\` and \`mode: '0600'\`.
- \`dependencies\`: \`galaxy\` collections/roles (installed with \`ansible-galaxy\`) and
  \`python\` pip packages available to the playbook.
- \`options\`: extra \`ansible-playbook\` flags such as \`- verbosity: vvv\`.
- \`vault_password\`: a Windmill variable path to use as the Ansible Vault password.

## Arguments

Reference header \`extra_vars\` directly as Jinja variables in the plays:

\`\`\`yaml
extra_vars:
  name:
    type: string
  count:
    type: number
---
- hosts: localhost
  tasks:
    - debug:
        msg: "{{ name }} x {{ count }}"
\`\`\`

## Environment variables

Windmill contextual variables are available as environment variables and read with the
\`env\` lookup:

\`\`\`yaml
- debug:
    msg: "Running in workspace {{ lookup('env', 'WM_WORKSPACE') }}"
\`\`\`

## Output

To return a result, write JSON to a \`result.json\` file in the job directory:

\`\`\`yaml
- hosts: localhost
  tasks:
    - name: Write result
      copy:
        content: "{{ { 'ok': true, 'value': 42 } | to_json }}"
        dest: result.json
\`\`\`
`,
  "write-script-bash": `---
name: write-script-bash
description: MUST use when writing Bash scripts.
---

## CLI Commands

Place scripts in a folder.

After writing, tell the user which command fits what they want to do:

- \`wmill script preview <script_path>\` — **default when iterating on a local script.** Runs the local file without deploying.
- \`wmill script run <path>\` — runs the script **already deployed** in the workspace. Use only when the user explicitly wants to test the deployed version, not local edits.
- \`wmill generate-metadata\` — regenerate the local \`.script.yaml\` (input schema) and \`.lock\` (resolved dependencies) for scripts you changed, and refresh their content hashes in \`wmill-lock.yaml\`. Local files only — **not** a deploy. See "Keep metadata in sync" below.
- 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 script", "try it", "test it", "does it work" while there are **local edits to the script file**, use \`script preview\`. Do NOT push the script to then \`script run\` it — pushing is a deploy, and deploying just to test overwrites the workspace version with untested changes.

Only use \`script run\` when:
- The user explicitly says "run the deployed version" / "run what's on the server".
- There is no local script being edited (you're just invoking an existing script).

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.

### Keep metadata in sync after editing

\`wmill-lock.yaml\` tracks a content hash for each item. Editing a script's content — most importantly **adding or removing an import** or **changing \`main\`'s arguments** — invalidates that hash and leaves the \`.lock\`, the \`.script.yaml\` input schema, and the hash row out of date. Run \`wmill generate-metadata\` (scoped to what you touched) after such edits so the resolved lock, the auto-generated args UI (driven by \`.script.yaml\`), and \`wmill-lock.yaml\` all match the code. Leaving them stale produces spurious diffs in git-sync and CI.

This only writes local files (it is **not** a deploy), but it re-resolves dependencies, so it can bump unpinned versions (the same as deploying from the UI; expected, not a bug). So by default offer it and run it once the user agrees, rather than running it silently after every edit — unless the project's \`AGENTS.md\` opts into running metadata automatically (see the "Keeping metadata in sync" preference there). Either way YOU run the command, not the user. After running it, diff the regenerated \`.lock\` / \`.script.lock\` files and tell the user which dependency versions changed (e.g. \`requests 2.31.0 → 2.32.0\`), so they can catch an unwanted bump before deploying — even under \`Metadata: auto\`, since it's information, not a confirmation gate. Pin versions in code to keep them fixed.

With no path argument, \`generate-metadata\` regenerates only the items whose content hash drifted — not everything. Imports propagate: editing a script that others import marks every importer stale too, so a one-line change to a shared module can regenerate many locks (by design — their locks must reflect the imported code). If it touches more than you expect, run \`wmill generate-metadata --dry-run\` — it lists each stale item with a reason (\`content changed\` or \`depends on <path>\`) without changing anything — then narrow with a path argument (\`wmill generate-metadata f/foo\`) or \`--strict-folder-boundaries\`.

If the on-disk \`.lock\` and \`.script.yaml\` are already correct and only \`wmill-lock.yaml\` needs its hashes refreshed (hash drift, or bootstrapping missing entries), use \`wmill generate-metadata rehash\` — it re-records hashes from disk with no backend round-trip and no dependency changes.

### After writing — offer to test, don't wait passively

If the user hasn't already told you to run/test/preview the script, offer it as a one-sentence next step (e.g. "Want me to run \`wmill script preview\` with sample args?"). Do not present a multi-option menu.

If the user already asked to test/run/try the script in their original request, skip the offer and just execute \`wmill script preview <path> -d '<args>'\` directly — pick plausible args from the script's declared parameters. The shape varies by language: \`main(...)\` for code languages, the SQL dialect's own placeholder syntax (\`$1\` for PostgreSQL, \`?\` for MySQL/Snowflake, \`@P1\` for MSSQL, \`@name\` for BigQuery, etc.), positional \`$1\`, \`$2\`, … for Bash, \`param(...)\` for PowerShell.

\`wmill script preview\` does not deploy, but it still executes script code and may cause side effects; run it yourself when the user asked to test/preview (or after confirming that execution is intended). \`wmill generate-metadata\` does not deploy either — it only writes local files (locks, schemas, hashes) — but offer it before running (or run automatically if the project's \`AGENTS.md\` opts in), per "Keep metadata in sync" above. Deploying to the workspace (\`git push\` or \`wmill sync push\` depending on how the repo is wired — see the **Deploying** section) is the only step that mutates remote state — do it only when the user explicitly asks to deploy/publish/push.

For a **visual** open-the-script-in-the-dev-page preview (rather than \`script preview\`'s run-and-print-result), use the \`preview\` skill.

Use \`wmill resource-type list --schema\` to discover available resource types.

# Bash

## Structure

Do not include \`#!/bin/bash\`. Arguments are obtained as positional parameters:

\`\`\`bash
# Get arguments
var1="$1"
var2="$2"

echo "Processing $var1 and $var2"

# Return JSON by echoing to stdout
echo "{\\"result\\": \\"$var1\\", \\"count\\": $var2}"
\`\`\`

**Important:**
- Do not include shebang (\`#!/bin/bash\`)
- Arguments are always strings
- Access with \`$1\`, \`$2\`, etc.

## Output

The script output is captured as the result. For structured data, output valid JSON:

\`\`\`bash
name="$1"
count="$2"

# Output JSON result
cat << EOF
{
  "name": "$name",
  "count": $count,
  "timestamp": "$(date -Iseconds)"
}
EOF
\`\`\`

## Environment Variables

Environment variables set in Windmill are available:

\`\`\`bash
# Access environment variable
echo "Workspace: $WM_WORKSPACE"
echo "Job ID: $WM_JOB_ID"
\`\`\`
`,
  "write-script-bigquery": `---
name: write-script-bigquery
description: MUST use when writing BigQuery queries.
---

## CLI Commands

Place scripts in a folder.

After writing, tell the user which command fits what they want to do:

- \`wmill script preview <script_path>\` — **default when iterating on a local script.** Runs the local file without deploying.
- \`wmill script run <path>\` — runs the script **already deployed** in the workspace. Use only when the user explicitly wants to test the deployed version, not local edits.
- \`wmill generate-metadata\` — regenerate the local \`.script.yaml\` (input schema) and \`.lock\` (resolved dependencies) for scripts you changed, and refresh their content hashes in \`wmill-lock.yaml\`. Local files only — **not** a deploy. See "Keep metadata in sync" below.
- 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 script", "try it", "test it", "does it work" while there are **local edits to the script file**, use \`script preview\`. Do NOT push the script to then \`script run\` it — pushing is a deploy, and deploying just to test overwrites the workspace version with untested changes.

Only use \`script run\` when:
- The user explicitly says "run the deployed version" / "run what's on the server".
- There is no local script being edited (you're just invoking an existing script).

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.

### Keep metadata in sync after editing

\`wmill-lock.yaml\` tracks a content hash for each item. Editing a script's content — most importantly **adding or removing an import** or **changing \`main\`'s arguments** — invalidates that hash and leaves the \`.lock\`, the \`.script.yaml\` input schema, and the hash row out of date. Run \`wmill generate-metadata\` (scoped to what you touched) after such edits so the resolved lock, the auto-generated args UI (driven by \`.script.yaml\`), and \`wmill-lock.yaml\` all match the code. Leaving them stale produces spurious diffs in git-sync and CI.

This only writes local files (it is **not** a deploy), but it re-resolves dependencies, so it can bump unpinned versions (the same as deploying from the UI; expected, not a bug). So by default offer it and run it once the user agrees, rather than running it silently after every edit — unless the project's \`AGENTS.md\` opts into running metadata automatically (see the "Keeping metadata in sync" preference there). Either way YOU run the command, not the user. After running it, diff the regenerated \`.lock\` / \`.script.lock\` files and tell the user which dependency versions changed (e.g. \`requests 2.31.0 → 2.32.0\`), so they can catch an unwanted bump before deploying — even under \`Metadata: auto\`, since it's information, not a confirmation gate. Pin versions in code to keep them fixed.

With no path argument, \`generate-metadata\` regenerates only the items whose content hash drifted — not everything. Imports propagate: editing a script that others import marks every importer stale too, so a one-line change to a shared module can regenerate many locks (by design — their locks must reflect the imported code). If it touches more than you expect, run \`wmill generate-metadata --dry-run\` — it lists each stale item with a reason (\`content changed\` or \`depends on <path>\`) without changing anything — then narrow with a path argument (\`wmill generate-metadata f/foo\`) or \`--strict-folder-boundaries\`.

If the on-disk \`.lock\` and \`.script.yaml\` are already correct and only \`wmill-lock.yaml\` needs its hashes refreshed (hash drift, or bootstrapping missing entries), use \`wmill generate-metadata rehash\` — it re-records hashes from disk with no backend round-trip and no dependency changes.

### After writing — offer to test, don't wait passively

If the user hasn't already told you to run/test/preview the script, offer it as a one-sentence next step (e.g. "Want me to run \`wmill script preview\` with sample args?"). Do not present a multi-option menu.

If the user already asked to test/run/try the script in their original request, skip the offer and just execute \`wmill script preview <path> -d '<args>'\` directly — pick plausible args from the script's declared parameters. The shape varies by language: \`main(...)\` for code languages, the SQL dialect's own placeholder syntax (\`$1\` for PostgreSQL, \`?\` for MySQL/Snowflake, \`@P1\` for MSSQL, \`@name\` for BigQuery, etc.), positional \`$1\`, \`$2\`, … for Bash, \`param(...)\` for PowerShell.

\`wmill script preview\` does not deploy, but it still executes script code and may cause side effects; run it yourself when the user asked to test/preview (or after confirming that execution is intended). \`wmill generate-metadata\` does not deploy either — it only writes local files (locks, schemas, hashes) — but offer it before running (or run automatically if the project's \`AGENTS.md\` opts in), per "Keep metadata in sync" above. Deploying to the workspace (\`git push\` or \`wmill sync push\` depending on how the repo is wired — see the **Deploying** section) is the only step that mutates remote state — do it only when the user explicitly asks to deploy/publish/push.

For a **visual** open-the-script-in-the-dev-page preview (rather than \`script preview\`'s run-and-print-result), use the \`preview\` skill.

Use \`wmill resource-type list --schema\` to discover available resource types.

# BigQuery

Arguments use \`@name\` syntax.

Name the parameters by adding comments before the statement:

\`\`\`sql
-- @name1 (string)
-- @name2 (int64) = 0
SELECT * FROM users WHERE name = @name1 AND age > @name2;
\`\`\`

## Receiving an S3Object as a script parameter

Declare the arg with type \`(s3object)\`. Windmill renders an S3 file picker for
it, downloads the file, and binds it as a \`STRING\` JSON parameter — Parquet/CSV
files are decoded server-side into a JSON array of records, JSON/JSONL pass
through. Consume with \`JSON_EXTRACT_ARRAY\` / \`JSON_VALUE\`:

\`\`\`sql
-- @file (s3object)
SELECT
  CAST(JSON_VALUE(row, '$.id') AS INT64) AS id,
  JSON_VALUE(row, '$.name') AS name
FROM UNNEST(JSON_EXTRACT_ARRAY(@file)) AS row;
\`\`\`

## Streaming query results to S3

Add a \`-- s3\` directive at the top of the script to stream the result set to S3
instead of returning rows. Windmill writes the file and returns its \`S3Object\`
as the script result.

\`\`\`sql
-- s3 prefix=exports/users format=parquet
SELECT id, name FROM users;
\`\`\`

All keys are optional: \`prefix\` (object key prefix), \`storage\` (named storage —
omit to use the workspace default), \`format\` (\`json\` (default), \`parquet\`, or
\`csv\`). Use this for large result sets — rows stream directly to S3 instead of
being buffered, bypassing the 10000-row return cap.
`,
  "write-script-bun": `---
name: write-script-bun
description: MUST use when writing TypeScript scripts. Bun is the default and preferred TypeScript runtime — pick it for TypeScript unless the script specifically needs Deno.
---

## CLI Commands

Place scripts in a folder.

After writing, tell the user which command fits what they want to do:

- \`wmill script preview <script_path>\` — **default when iterating on a local script.** Runs the local file without deploying.
- \`wmill script run <path>\` — runs the script **already deployed** in the workspace. Use only when the user explicitly wants to test the deployed version, not local edits.
- \`wmill generate-metadata\` — regenerate the local \`.script.yaml\` (input schema) and \`.lock\` (resolved dependencies) for scripts you changed, and refresh their content hashes in \`wmill-lock.yaml\`. Local files only — **not** a deploy. See "Keep metadata in sync" below.
- 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 script", "try it", "test it", "does it work" while there are **local edits to the script file**, use \`script preview\`. Do NOT push the script to then \`script run\` it — pushing is a deploy, and deploying just to test overwrites the workspace version with untested changes.

Only use \`script run\` when:
- The user explicitly says "run the deployed version" / "run what's on the server".
- There is no local script being edited (you're just invoking an existing script).

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.

### Keep metadata in sync after editing

\`wmill-lock.yaml\` tracks a content hash for each item. Editing a script's content — most importantly **adding or removing an import** or **changing \`main\`'s arguments** — invalidates that hash and leaves the \`.lock\`, the \`.script.yaml\` input schema, and the hash row out of date. Run \`wmill generate-metadata\` (scoped to what you touched) after such edits so the resolved lock, the auto-generated args UI (driven by \`.script.yaml\`), and \`wmill-lock.yaml\` all match the code. Leaving them stale produces spurious diffs in git-sync and CI.

This only writes local files (it is **not** a deploy), but it re-resolves dependencies, so it can bump unpinned versions (the same as deploying from the UI; expected, not a bug). So by default offer it and run it once the user agrees, rather than running it silently after every edit — unless the project's \`AGENTS.md\` opts into running metadata automatically (see the "Keeping metadata in sync" preference there). Either way YOU run the command, not the user. After running it, diff the regenerated \`.lock\` / \`.script.lock\` files and tell the user which dependency versions changed (e.g. \`requests 2.31.0 → 2.32.0\`), so they can catch an unwanted bump before deploying — even under \`Metadata: auto\`, since it's information, not a confirmation gate. Pin versions in code to keep them fixed.

With no path argument, \`generate-metadata\` regenerates only the items whose content hash drifted — not everything. Imports propagate: editing a script that others import marks every importer stale too, so a one-line change to a shared module can regenerate many locks (by design — their locks must reflect the imported code). If it touches more than you expect, run \`wmill generate-metadata --dry-run\` — it lists each stale item with a reason (\`content changed\` or \`depends on <path>\`) without changing anything — then narrow with a path argument (\`wmill generate-metadata f/foo\`) or \`--strict-folder-boundaries\`.

If the on-disk \`.lock\` and \`.script.yaml\` are already correct and only \`wmill-lock.yaml\` needs its hashes refreshed (hash drift, or bootstrapping missing entries), use \`wmill generate-metadata rehash\` — it re-records hashes from disk with no backend round-trip and no dependency changes.

### After writing — offer to test, don't wait passively

If the user hasn't already told you to run/test/preview the script, offer it as a one-sentence next step (e.g. "Want me to run \`wmill script preview\` with sample args?"). Do not present a multi-option menu.

If the user already asked to test/run/try the script in their original request, skip the offer and just execute \`wmill script preview <path> -d '<args>'\` directly — pick plausible args from the script's declared parameters. The shape varies by language: \`main(...)\` for code languages, the SQL dialect's own placeholder syntax (\`$1\` for PostgreSQL, \`?\` for MySQL/Snowflake, \`@P1\` for MSSQL, \`@name\` for BigQuery, etc.), positional \`$1\`, \`$2\`, … for Bash, \`param(...)\` for PowerShell.

\`wmill script preview\` does not deploy, but it still executes script code and may cause side effects; run it yourself when the user asked to test/preview (or after confirming that execution is intended). \`wmill generate-metadata\` does not deploy either — it only writes local files (locks, schemas, hashes) — but offer it before running (or run automatically if the project's \`AGENTS.md\` opts in), per "Keep metadata in sync" above. Deploying to the workspace (\`git push\` or \`wmill sync push\` depending on how the repo is wired — see the **Deploying** section) is the only step that mutates remote state — do it only when the user explicitly asks to deploy/publish/push.

For a **visual** open-the-script-in-the-dev-page preview (rather than \`script preview\`'s run-and-print-result), use the \`preview\` skill.

Use \`wmill resource-type list --schema\` to discover available resource types.

# TypeScript (Bun)

Bun runtime with full npm ecosystem and fastest execution. **Bun is the default and preferred TypeScript runtime** — choose it for any TypeScript script unless there is a major reason to use Deno for that specific use-case.

## Structure

Export a single **async** function called \`main\`:

\`\`\`typescript
export async function main(param1: string, param2: number) {
  // Your code here
  return { result: param1, count: param2 };
}
\`\`\`

Do not call the main function. Libraries are installed automatically.

## Resource Types

On Windmill, credentials and configuration are stored in resources and passed as parameters to main.

Use the \`RT\` namespace for resource types:

\`\`\`typescript
export async function main(stripe: RT.Stripe) {
  // stripe contains API key and config from the resource
}
\`\`\`

Only use resource types if you need them to satisfy the instructions. Always use the RT namespace.

Before using a resource type, check the \`rt.d.ts\` file in the project root to see all available resource types and their fields. This file is generated by \`wmill resource-type generate-namespace\`.

## Imports

\`\`\`typescript
import Stripe from "stripe";
import { someFunction } from "some-package";
\`\`\`

## Prefer \`//native\` when the runtime allows it

If a script only needs \`fetch\` and the JavaScript standard library — including when it uses \`windmill-client\` — prefer making it a **native** script: add \`//native\` as the first line and write it with the \`write-script-bunnative\` skill. Native scripts run on a lightweight V8 isolate, start faster, and parallelize heavily. \`windmill-client\` works on the native worker (its calls go over \`fetch\`), so needing the Windmill client is **not** a reason to avoid \`//native\`. Use the regular \`bun\` language only when the code (or a dependency) needs Node/Bun runtime APIs — \`node:*\` modules, the filesystem, child processes, or native addons.

## Windmill Client

Import the windmill client for platform interactions:

\`\`\`typescript
import * as wmill from "windmill-client";
\`\`\`

**Prefer \`windmill-client\` over raw \`fetch\` for anything that talks to Windmill** — reading resources/variables/states, running scripts and flows, S3 object operations, etc. It handles auth, the workspace, and the base URL for you, so you don't hand-roll URLs or tokens. Reserve \`fetch\` for calling *external* HTTP APIs that aren't Windmill.

The full \`windmill-client\` API reference (every exported function and its signature) is included in this skill below — consult it for the exact method to use instead of guessing or falling back to \`fetch\`.

## Preprocessor Scripts

For preprocessor scripts, the function should be named \`preprocessor\` and receives an \`event\` parameter:

\`\`\`typescript
type Event = {
  kind:
    | "webhook"
    | "http"
    | "websocket"
    | "kafka"
    | "email"
    | "nats"
    | "postgres"
    | "sqs"
    | "mqtt"
    | "gcp";
  body: any;
  headers: Record<string, string>;
  query: Record<string, string>;
};

export async function preprocessor(event: Event) {
  return {
    param1: event.body.field1,
    param2: event.query.id,
  };
}
\`\`\`

## S3 Object Operations

Windmill provides built-in support for S3-compatible storage operations. The \`wmill.S3Object\` type covers both the \`s3://storage/key\` URI form (\`s3:///key\` for the workspace default storage) and the \`{ s3, storage? }\` record form — always use it instead of redefining your own.

### Receiving an S3Object as a script parameter

\`\`\`typescript
import * as wmill from "windmill-client";

export async function main(file: wmill.S3Object) {
  const content = await wmill.loadS3File(file);
  // ...
}
\`\`\`

### S3 operations

\`\`\`typescript
import * as wmill from "windmill-client";

// Load file content from S3
const content: Uint8Array = await wmill.loadS3File(s3object);

// Load file as stream
const blob: Blob = await wmill.loadS3FileStream(s3object);

// Write file to S3
const result: wmill.S3Object = await wmill.writeS3File(
  s3object, // Target path (or undefined to auto-generate)
  fileContent, // string or Blob
  s3ResourcePath // Optional: specific S3 resource to use
);
\`\`\`


# TypeScript SDK (windmill-client)

Import: import * as wmill from 'windmill-client'

workerHasInternalServer(): boolean

/**
 * Initialize the Windmill client with authentication token and base URL
 * @param token - Authentication token (defaults to WM_TOKEN env variable)
 * @param baseUrl - API base URL (defaults to BASE_INTERNAL_URL or BASE_URL env variable)
 */
setClient(token?: string, baseUrl?: string): void

/**
 * Create a client configuration from env variables
 * @returns client configuration
 */
getWorkspace(): string

/**
 * Get a resource value by path
 * @param path path of the resource,  default to internal state path
 * @param undefinedIfEmpty if the resource does not exist, return undefined instead of throwing an error
 * @returns resource value
 */
async getResource(path?: string, undefinedIfEmpty?: boolean): Promise<any>

/**
 * Get the true root job id
 * @param jobId job id to get the root job id from (default to current job)
 * @returns root job id
 */
async getRootJobId(jobId?: string): Promise<string>

/**
 * @deprecated Use runScriptByPath or runScriptByHash instead
 */
async runScript(path: string | null = null, hash_: string | null = null, args: Record<string, any> | null = null, verbose: boolean = false, tag: string | null = null): Promise<any>

/**
 * Run a script synchronously by its path and wait for the result
 * @param path - Script path in Windmill
 * @param args - Arguments to pass to the script
 * @param verbose - Enable verbose logging
 * @param tag - Override the worker tag the job runs on
 * @returns Script execution result
 */
async runScriptByPath(path: string, args: Record<string, any> | null = null, verbose: boolean = false, tag: string | null = null): Promise<any>

/**
 * Run a script synchronously by its hash and wait for the result
 * @param hash_ - Script hash in Windmill
 * @param args - Arguments to pass to the script
 * @param verbose - Enable verbose logging
 * @param tag - Override the worker tag the job runs on
 * @returns Script execution result
 */
async runScriptByHash(hash_: string, args: Record<string, any> | null = null, verbose: boolean = false, tag: string | null = null): Promise<any>

/**
 * Append a text to the result stream
 * @param text text to append to the result stream
 */
appendToResultStream(text: string): void

/**
 * Stream to the result stream
 * @param stream stream to stream to the result stream
 */
async streamResult(stream: AsyncIterable<string>): Promise<void>

/**
 * Run a flow synchronously by its path and wait for the result
 * @param path - Flow path in Windmill
 * @param args - Arguments to pass to the flow
 * @param verbose - Enable verbose logging
 * @param tag - Override the worker tag the job runs on
 * @returns Flow execution result
 */
async runFlow(path: string | null = null, args: Record<string, any> | null = null, verbose: boolean = false, tag: string | null = null): Promise<any>

/**
 * Wait for a job to complete and return its result
 * @param jobId - ID of the job to wait for
 * @param verbose - Enable verbose logging
 * @returns Job result when completed
 */
async waitJob(jobId: string, verbose: boolean = false): Promise<any>

/**
 * Get the result of a completed job
 * @param jobId - ID of the completed job
 * @returns Job result
 */
async getResult(jobId: string): Promise<any>

/**
 * Get the result of a job if completed, or its current status
 * @param jobId - ID of the job
 * @returns Object with started, completed, success, and result properties
 */
async getResultMaybe(jobId: string): Promise<any>

/**
 * @deprecated Use runScriptByPathAsync or runScriptByHashAsync instead
 */
async runScriptAsync(path: string | null, hash_: string | null, args: Record<string, any> | null, scheduledInSeconds: number | null = null, tag: string | null = null): Promise<string>

/**
 * Run a script asynchronously by its path
 * @param path - Script path in Windmill
 * @param args - Arguments to pass to the script
 * @param scheduledInSeconds - Schedule execution for a future time (in seconds)
 * @param tag - Override the worker tag the job runs on
 * @returns Job ID of the created job
 */
async runScriptByPathAsync(path: string, args: Record<string, any> | null = null, scheduledInSeconds: number | null = null, tag: string | null = null): Promise<string>

/**
 * Run a script asynchronously by its hash
 * @param hash_ - Script hash in Windmill
 * @param args - Arguments to pass to the script
 * @param scheduledInSeconds - Schedule execution for a future time (in seconds)
 * @param tag - Override the worker tag the job runs on
 * @returns Job ID of the created job
 */
async runScriptByHashAsync(hash_: string, args: Record<string, any> | null = null, scheduledInSeconds: number | null = null, tag: string | null = null): Promise<string>

/**
 * Run a flow asynchronously by its path
 * @param path - Flow path in Windmill
 * @param args - Arguments to pass to the flow
 * @param scheduledInSeconds - Schedule execution for a future time (in seconds)
 * @param doNotTrackInParent - If false, tracks state in parent job (only use when fully awaiting the job)
 * @param tag - Override the worker tag the job runs on
 * @returns Job ID of the created job
 */
async runFlowAsync(path: string | null, args: Record<string, any> | null, scheduledInSeconds: number | null = null, // can only be set to false if this the job will be fully await and not concurrent with any other job // as otherwise the child flow and its own child will store their state in the parent job which will // lead to incorrectness and failures doNotTrackInParent: boolean = true, tag: string | null = null): Promise<string>

/**
 * Resolve a resource value in case the default value was picked because the input payload was undefined
 * @param obj resource value or path of the resource under the format \`$res:path\`
 * @returns resource value
 */
async resolveDefaultResource(obj: any): Promise<any>

/**
 * Get the state file path from environment variables
 * @returns State path string
 */
getStatePath(): string

/**
 * Set a resource value by path
 * @param path path of the resource to set, default to state path
 * @param value new value of the resource to set
 * @param initializeToTypeIfNotExist if the resource does not exist, initialize it with this type
 */
async setResource(value: any, path?: string, initializeToTypeIfNotExist?: string): Promise<void>

/**
 * Set the state
 * @param state state to set
 * @deprecated use setState instead
 */
async setInternalState(state: any): Promise<void>

/**
 * Set the state
 * @param state state to set
 * @param path Optional state resource path override. Defaults to \`getStatePath()\`.
 */
async setState(state: any, path?: string): Promise<void>

/**
 * Set the progress
 * Progress cannot go back and limited to 0% to 99% range
 * @param percent Progress to set in %
 * @param jobId? Job to set progress for
 */
async setProgress(percent: number, jobId?: any): Promise<void>

/**
 * Get the progress
 * @param jobId? Job to get progress from
 * @returns Optional clamped between 0 and 100 progress value
 */
async getProgress(jobId?: any): Promise<number | null>

/**
 * Set a flow user state
 * @param key key of the state
 * @param value value of the state
 */
async setFlowUserState(key: string, value: any, errorIfNotPossible?: boolean): Promise<void>

/**
 * Get a flow user state
 * @param path path of the variable
 */
async getFlowUserState(key: string, errorIfNotPossible?: boolean): Promise<any>

/**
 * Get the internal state
 * @deprecated use getState instead
 */
async getInternalState(): Promise<any>

/**
 * Get the state shared across executions
 * @param path Optional state resource path override. Defaults to \`getStatePath()\`.
 */
async getState(path?: string): Promise<any>

/**
 * Get a variable by path
 * @param path path of the variable
 * @returns variable value
 */
async getVariable(path: string): Promise<string>

/**
 * Set a variable by path, create if not exist
 * @param path path of the variable
 * @param value value of the variable
 * @param isSecretIfNotExist if the variable does not exist, create it as secret or not (default: false)
 * @param descriptionIfNotExist if the variable does not exist, create it with this description (default: "")
 */
async setVariable(path: string, value: string, isSecretIfNotExist?: boolean, descriptionIfNotExist?: string): Promise<void>

/**
 * Build a PostgreSQL connection URL from a database resource
 * @param path - Path to the database resource
 * @returns PostgreSQL connection URL string
 */
async databaseUrlFromResource(path: string): Promise<string>

async polarsConnectionSettings(s3_resource_path: string | undefined): Promise<any>

async duckdbConnectionSettings(s3_resource_path: string | undefined): Promise<any>

/**
 * Get S3 client settings from a resource or workspace default
 * @param s3_resource_path - Path to S3 resource (uses workspace default if undefined)
 * @param workspace - Workspace to read from (defaults to the \`WM_WORKSPACE\` env var)
 * @returns S3 client configuration settings
 */
async denoS3LightClientSettings(s3_resource_path: string | undefined, workspace: string | undefined = undefined): Promise<DenoS3LightClientSettings>

/**
 * Load the content of a file stored in S3. If the s3ResourcePath is undefined, it will default to the workspace S3 resource.
 * 
 * \`\`\`typescript
 * let fileContent = await wmill.loadS3FileContent(inputFile)
 * // if the file is a raw text file, it can be decoded and printed directly:
 * const text = new TextDecoder().decode(fileContentStream)
 * console.log(text);
 * \`\`\`
 * 
 * @param workspace - Workspace to read from (defaults to the \`WM_WORKSPACE\` env var)
 */
async loadS3File(s3object: S3Object, s3ResourcePath: string | undefined = undefined, workspace: string | undefined = undefined): Promise<Uint8Array | undefined>

/**
 * Load the content of a file stored in S3 as a stream. If the s3ResourcePath is undefined, it will default to the workspace S3 resource.
 * 
 * \`\`\`typescript
 * let fileContentBlob = await wmill.loadS3FileStream(inputFile)
 * // if the content is plain text, the blob can be read directly:
 * console.log(await fileContentBlob.text());
 * \`\`\`
 * 
 * @param workspace - Workspace to read from (defaults to the \`WM_WORKSPACE\` env var)
 */
async loadS3FileStream(s3object: S3Object, s3ResourcePath: string | undefined = undefined, workspace: string | undefined = undefined): Promise<Blob | undefined>

/**
 * Persist a file to the S3 bucket. If the s3ResourcePath is undefined, it will default to the workspace S3 resource.
 * 
 * \`\`\`typescript
 * const s3object = await writeS3File(s3Object, "Hello Windmill!")
 * const fileContentAsUtf8Str = (await s3object.toArray()).toString('utf-8')
 * console.log(fileContentAsUtf8Str)
 * \`\`\`
 * 
 * @param workspace - Workspace to write to (defaults to the \`WM_WORKSPACE\` env var)
 */
async writeS3File(s3object: S3Object | undefined, fileContent: string | Blob, s3ResourcePath: string | undefined = undefined, contentType: string | undefined = undefined, contentDisposition: string | undefined = undefined, workspace: string | undefined = undefined): Promise<S3Object>

/**
 * Permanently delete a file from S3 by key.
 * 
 * \`\`\`typescript
 * await wmill.deleteS3File({ s3: "path/to/file.txt" })
 * \`\`\`
 * 
 * @param s3object - S3 object identifying the file to delete (must have \`s3\` set)
 * @param workspace - Workspace to delete from (defaults to the \`WM_WORKSPACE\` env var)
 */
async deleteS3File(s3object: S3Object, workspace: string | undefined = undefined): Promise<void>

/**
 * Sign S3 objects to be used by anonymous users in public apps
 * @param s3objects s3 objects to sign
 * @returns signed s3 objects
 */
async signS3Objects(s3objects: S3Object[]): Promise<S3Object[]>

/**
 * Sign S3 object to be used by anonymous users in public apps
 * @param s3object s3 object to sign
 * @returns signed s3 object
 */
async signS3Object(s3object: S3Object): Promise<S3Object>

/**
 * Generate a presigned public URL for an array of S3 objects.
 * If an S3 object is not signed yet, it will be signed first.
 * @param s3Objects s3 objects to sign
 * @returns list of signed public URLs
 */
async getPresignedS3PublicUrls(s3Objects: S3Object[], { baseUrl }: { baseUrl?: string } = {}): Promise<string[]>

/**
 * Generate a presigned public URL for an S3 object. If the S3 object is not signed yet, it will be signed first.
 * @param s3Object s3 object to sign
 * @returns signed public URL
 */
async getPresignedS3PublicUrl(s3Objects: S3Object, { baseUrl }: { baseUrl?: string } = {}): Promise<string>

/**
 * Get URLs needed for resuming a flow after this step
 * @param approver approver name
 * @param flowLevel if true, generate resume URLs for the parent flow instead of the specific step.
 *                  This allows pre-approvals that can be consumed by any later suspend step in the same flow.
 * @returns approval page UI URL, resume and cancel API URLs for resuming the flow
 */
async getResumeUrls(approver?: string, flowLevel?: boolean): Promise<{
  approvalPage: string;
  resume: string;
  cancel: string;
}>

/**
 * @deprecated use getResumeUrls instead
 */
getResumeEndpoints(approver?: string): Promise<{
  approvalPage: string;
  resume: string;
  cancel: string;
}>

/**
 * Get an OIDC jwt token for auth to external services (e.g: Vault, AWS) (ee only)
 * @param audience audience of the token
 * @param expiresIn Optional number of seconds until the token expires
 * @returns jwt token
 */
async getIdToken(audience: string, expiresIn?: number): Promise<string>

/**
 * Convert a base64-encoded string to Uint8Array
 * @param data - Base64-encoded string
 * @returns Decoded Uint8Array
 */
base64ToUint8Array(data: string): Uint8Array

/**
 * Convert a Uint8Array to base64-encoded string
 * @param arrayBuffer - Uint8Array to encode
 * @returns Base64-encoded string
 */
uint8ArrayToBase64(arrayBuffer: Uint8Array): string

/**
 * Get email from workspace username
 * This method is particularly useful for apps that require the email address of the viewer.
 * Indeed, in the viewer context, WM_USERNAME is set to the username of the viewer but WM_EMAIL is set to the email of the creator of the app.
 * @param username
 * @returns email address
 */
async usernameToEmail(username: string): Promise<string>

/**
 * Sends an interactive approval request via Slack, allowing optional customization of the message, approver, and form fields.
 * 
 * **[Enterprise Edition Only]** To include form fields in the Slack approval request, go to **Advanced -> Suspend -> Form**
 * and define a form. Learn more at [Windmill Documentation](https://www.windmill.dev/docs/flows/flow_approval#form).
 * 
 * @param {Object} options - The configuration options for the Slack approval request.
 * @param {string} options.slackResourcePath - The path to the Slack resource in Windmill.
 * @param {string} options.channelId - The Slack channel ID where the approval request will be sent.
 * @param {string} [options.message] - Optional custom message to include in the Slack approval request.
 * @param {string} [options.approver] - Optional user ID or name of the approver for the request.
 * @param {DefaultArgs} [options.defaultArgsJson] - Optional object defining or overriding the default arguments to a form field.
 * @param {Enums} [options.dynamicEnumsJson] - Optional object overriding the enum default values of an enum form field.
 * @param {string} [options.resumeButtonText] - Optional text for the resume button.
 * @param {string} [options.cancelButtonText] - Optional text for the cancel button.
 * 
 * @returns {Promise<void>} Resolves when the Slack approval request is successfully sent.
 * 
 * @throws {Error} If the function is not called within a flow or flow preview.
 * @throws {Error} If the \`JobService.getSlackApprovalPayload\` call fails.
 * 
 * **Usage Example:**
 * \`\`\`typescript
 * await requestInteractiveSlackApproval({
 *   slackResourcePath: "/u/alex/my_slack_resource",
 *   channelId: "admins-slack-channel",
 *   message: "Please approve this request",
 *   approver: "approver123",
 *   defaultArgsJson: { key1: "value1", key2: 42 },
 *   dynamicEnumsJson: { foo: ["choice1", "choice2"], bar: ["optionA", "optionB"] },
 *   resumeButtonText: "Resume",
 *   cancelButtonText: "Cancel",
 * });
 * \`\`\`
 * 
 * **Note:** This function requires execution within a Windmill flow or flow preview.
 */
async requestInteractiveSlackApproval({ slackResourcePath, channelId, message, approver, defaultArgsJson, dynamicEnumsJson, resumeButtonText, cancelButtonText, }: SlackApprovalOptions): Promise<void>

/**
 * Sends an interactive approval request via Teams, allowing optional customization of the message, approver, and form fields.
 * 
 * **[Enterprise Edition Only]** To include form fields in the Teams approval request, go to **Advanced -> Suspend -> Form**
 * and define a form. Learn more at [Windmill Documentation](https://www.windmill.dev/docs/flows/flow_approval#form).
 * 
 * @param {Object} options - The configuration options for the Teams approval request.
 * @param {string} options.teamName - The Teams team name where the approval request will be sent.
 * @param {string} options.channelName - The Teams channel name where the approval request will be sent.
 * @param {string} [options.message] - Optional custom message to include in the Teams approval request.
 * @param {string} [options.approver] - Optional user ID or name of the approver for the request.
 * @param {DefaultArgs} [options.defaultArgsJson] - Optional object defining or overriding the default arguments to a form field.
 * @param {Enums} [options.dynamicEnumsJson] - Optional object overriding the enum default values of an enum form field.
 * 
 * @returns {Promise<void>} Resolves when the Teams approval request is successfully sent.
 * 
 * @throws {Error} If the function is not called within a flow or flow preview.
 * @throws {Error} If the \`JobService.getTeamsApprovalPayload\` call fails.
 * 
 * **Usage Example:**
 * \`\`\`typescript
 * await requestInteractiveTeamsApproval({
 *   teamName: "admins-teams",
 *   channelName: "admins-teams-channel",
 *   message: "Please approve this request",
 *   approver: "approver123",
 *   defaultArgsJson: { key1: "value1", key2: 42 },
 *   dynamicEnumsJson: { foo: ["choice1", "choice2"], bar: ["optionA", "optionB"] },
 * });
 * \`\`\`
 * 
 * **Note:** This function requires execution within a Windmill flow or flow preview.
 */
async requestInteractiveTeamsApproval({ teamName, channelName, message, approver, defaultArgsJson, dynamicEnumsJson, }: TeamsApprovalOptions): Promise<void>

setWorkflowCtx(ctx: WorkflowCtx | null): void

async sleep(seconds: number): Promise<void>

async step<T>(name: string, fn: () => T | Promise<T>): Promise<T>

/**
 * Create a task that dispatches to a separate Windmill script.
 * 
 * @example
 * const extract = taskScript("f/data/extract");
 * // inside workflow: await extract({ url: "https://..." })
 */
taskScript(path: string, options?: TaskOptions): (...args: any[]) => PromiseLike<any>

/**
 * Create a task that dispatches to a separate Windmill flow.
 * 
 * @example
 * const pipeline = taskFlow("f/etl/pipeline");
 * // inside workflow: await pipeline({ input: data })
 */
taskFlow(path: string, options?: TaskOptions): (...args: any[]) => PromiseLike<any>

/**
 * Mark an async function as a workflow-as-code entry point.
 * 
 * The function must be **deterministic**: given the same inputs it must call
 * tasks in the same order on every replay. Branching on task results is fine
 * (results are replayed from checkpoint), but branching on external state
 * (current time, random values, external API calls) must use \`step()\` to
 * checkpoint the value so replays see the same result.
 */
workflow<T>(fn: (...args: any[]) => Promise<T>): void

/**
 * Suspend the workflow and wait for an external approval.
 * 
 * Use \`getResumeUrls()\` (wrapped in \`step()\`) to obtain resume/cancel/approvalPage
 * URLs before calling this function.
 * 
 * @example
 * const urls = await step("urls", () => getResumeUrls());
 * await step("notify", () => sendEmail(urls.approvalPage));
 * const { value, approver } = await waitForApproval({ timeout: 3600 });
 */
waitForApproval(options?: { timeout?: number; form?: object; selfApproval?: boolean; }): PromiseLike<{ value: any; approver: string; approved: boolean }>

/**
 * Process items in parallel with optional concurrency control.
 * 
 * Each item is processed by calling \`fn(item)\`, which should be a task().
 * Items are dispatched in batches of \`concurrency\` (default: all at once).
 * 
 * @example
 * const process = task(async (item: string) => { ... });
 * const results = await parallel(items, process, { concurrency: 5 });
 */
async parallel<T, R>(items: T[], fn: (item: T) => PromiseLike<R> | R, options?: { concurrency?: number },): Promise<R[]>

/**
 * Commit Kafka offsets for a trigger with auto_commit disabled.
 * @param triggerPath - Path to the Kafka trigger (from event.wm_trigger.trigger_path)
 * @param topic - Kafka topic name (from event.topic)
 * @param partition - Partition number (from event.partition)
 * @param offset - Message offset to commit (from event.offset)
 */
async commitKafkaOffsets(triggerPath: string, topic: string, partition: number, offset: number,): Promise<void>

/**
 * Parse an S3 object from URI string or record format
 * @param s3Object - S3 object as URI string (\`s3://storage/key\`, \`s3:///key\`
 *   for the default storage) or record. Any other string throws rather than
 *   falling back to an auto-generated key: an auto key is requested by
 *   omitting the object, and a fallback would silently misplace the upload
 *   on any typo.
 * @returns S3 object record with storage and s3 key
 */
parseS3Object(s3Object: S3Object): S3ObjectRecord

/**
 * Create a SQL template function for PostgreSQL/datatable queries
 * @param name - Database/datatable name (default: "main")
 * @returns SQL template function for building parameterized queries
 * @example
 * let sql = wmill.datatable()
 * let name = 'Robin'
 * let age = 21
 * await sql\`
 *   SELECT * FROM friends
 *     WHERE name = \${name} AND age = \${age}::int
 * \`.fetch()
 */
datatable(name: string = "main"): DatatableSqlTemplateFunction

/**
 * Create a SQL template function for DuckDB/ducklake queries
 * @param name - DuckDB database name, optionally with a schema as \`name:schema\` (default: "main")
 * @returns SQL template function for building parameterized queries
 * @example
 * let sql = wmill.ducklake()
 * let name = 'Robin'
 * let age = 21
 * await sql\`
 *   SELECT * FROM friends
 *     WHERE name = \${name} AND age = \${age}
 * \`.fetch()
 * @example
 * // Target a specific schema within the ducklake
 * let sql = wmill.ducklake("my_lake:analytics")
 */
ducklake(name: string = "main"): SqlTemplateFunction

/**
 * Idempotently materialize \`selectSql\` into a ducklake table for one
 * partition (or the whole table when \`partition\` is omitted) — the client-side
 * equivalent of the \`// materialize\` engine.
 * With \`uniqueKey\` it upserts the slice (delete-by-key + insert); otherwise it
 * replaces it (whole table → \`CREATE OR REPLACE\`; partition → delete + insert).
 * Safe to re-run for the same partition (backfill / failure-recovery).
 * 
 * Returns a lazy statement — call \`.execute()\` to run it:
 * \`await wmill.upsertPartition({ table, selectSql, partition }).execute()\`.
 */
upsertPartition(opts: DucklakeMaterializeOptions): SqlStatement<any>

/**
 * INSERT-only materialization (no dedup/replace) for append-only tables.
 * Re-running the same partition duplicates rows — use only for immutable
 * event-log sources.
 * 
 * Returns a lazy statement — call \`.execute()\` to run it:
 * \`await wmill.appendPartition({ table, selectSql, partition }).execute()\`.
 */
appendPartition(opts: Omit<DucklakeMaterializeOptions, "uniqueKey">,): SqlStatement<any>
`,
  "write-script-bunnative": `---
name: write-script-bunnative
description: MUST use when writing Bun Native scripts. The script must start with //native to run on the native worker.
---

## CLI Commands

Place scripts in a folder.

After writing, tell the user which command fits what they want to do:

- \`wmill script preview <script_path>\` — **default when iterating on a local script.** Runs the local file without deploying.
- \`wmill script run <path>\` — runs the script **already deployed** in the workspace. Use only when the user explicitly wants to test the deployed version, not local edits.
- \`wmill generate-metadata\` — regenerate the local \`.script.yaml\` (input schema) and \`.lock\` (resolved dependencies) for scripts you changed, and refresh their content hashes in \`wmill-lock.yaml\`. Local files only — **not** a deploy. See "Keep metadata in sync" below.
- 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 script", "try it", "test it", "does it work" while there are **local edits to the script file**, use \`script preview\`. Do NOT push the script to then \`script run\` it — pushing is a deploy, and deploying just to test overwrites the workspace version with untested changes.

Only use \`script run\` when:
- The user explicitly says "run the deployed version" / "run what's on the server".
- There is no local script being edited (you're just invoking an existing script).

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.

### Keep metadata in sync after editing

\`wmill-lock.yaml\` tracks a content hash for each item. Editing a script's content — most importantly **adding or removing an import** or **changing \`main\`'s arguments** — invalidates that hash and leaves the \`.lock\`, the \`.script.yaml\` input schema, and the hash row out of date. Run \`wmill generate-metadata\` (scoped to what you touched) after such edits so the resolved lock, the auto-generated args UI (driven by \`.script.yaml\`), and \`wmill-lock.yaml\` all match the code. Leaving them stale produces spurious diffs in git-sync and CI.

This only writes local files (it is **not** a deploy), but it re-resolves dependencies, so it can bump unpinned versions (the same as deploying from the UI; expected, not a bug). So by default offer it and run it once the user agrees, rather than running it silently after every edit — unless the project's \`AGENTS.md\` opts into running metadata automatically (see the "Keeping metadata in sync" preference there). Either way YOU run the command, not the user. After running it, diff the regenerated \`.lock\` / \`.script.lock\` files and tell the user which dependency versions changed (e.g. \`requests 2.31.0 → 2.32.0\`), so they can catch an unwanted bump before deploying — even under \`Metadata: auto\`, since it's information, not a confirmation gate. Pin versions in code to keep them fixed.

With no path argument, \`generate-metadata\` regenerates only the items whose content hash drifted — not everything. Imports propagate: editing a script that others import marks every importer stale too, so a one-line change to a shared module can regenerate many locks (by design — their locks must reflect the imported code). If it touches more than you expect, run \`wmill generate-metadata --dry-run\` — it lists each stale item with a reason (\`content changed\` or \`depends on <path>\`) without changing anything — then narrow with a path argument (\`wmill generate-metadata f/foo\`) or \`--strict-folder-boundaries\`.

If the on-disk \`.lock\` and \`.script.yaml\` are already correct and only \`wmill-lock.yaml\` needs its hashes refreshed (hash drift, or bootstrapping missing entries), use \`wmill generate-metadata rehash\` — it re-records hashes from disk with no backend round-trip and no dependency changes.

### After writing — offer to test, don't wait passively

If the user hasn't already told you to run/test/preview the script, offer it as a one-sentence next step (e.g. "Want me to run \`wmill script preview\` with sample args?"). Do not present a multi-option menu.

If the user already asked to test/run/try the script in their original request, skip the offer and just execute \`wmill script preview <path> -d '<args>'\` directly — pick plausible args from the script's declared parameters. The shape varies by language: \`main(...)\` for code languages, the SQL dialect's own placeholder syntax (\`$1\` for PostgreSQL, \`?\` for MySQL/Snowflake, \`@P1\` for MSSQL, \`@name\` for BigQuery, etc.), positional \`$1\`, \`$2\`, … for Bash, \`param(...)\` for PowerShell.

\`wmill script preview\` does not deploy, but it still executes script code and may cause side effects; run it yourself when the user asked to test/preview (or after confirming that execution is intended). \`wmill generate-metadata\` does not deploy either — it only writes local files (locks, schemas, hashes) — but offer it before running (or run automatically if the project's \`AGENTS.md\` opts in), per "Keep metadata in sync" above. Deploying to the workspace (\`git push\` or \`wmill sync push\` depending on how the repo is wired — see the **Deploying** section) is the only step that mutates remote state — do it only when the user explicitly asks to deploy/publish/push.

For a **visual** open-the-script-in-the-dev-page preview (rather than \`script preview\`'s run-and-print-result), use the \`preview\` skill.

Use \`wmill resource-type list --schema\` to discover available resource types.

# TypeScript (Bun Native)

Native TypeScript execution. Native scripts are Bun scripts that run on the native worker — a lightweight V8 isolate that exposes \`fetch\` and the JavaScript standard library — and can be heavily parallelized. Every script MUST start with \`//native\` on its first line so Windmill routes it to the native worker; without it the exact same script runs on the regular Bun worker. You may import npm packages and other Windmill scripts (e.g. \`./helper.ts\`) — imports are resolved and bundled just like a regular Bun script — as long as everything (your code and its dependencies) relies only on \`fetch\` and the standard library. Libraries that need Node/Bun runtime APIs (filesystem, \`node:*\` modules, child processes, native addons) will not work on the native worker; use the regular \`bun\` language for those.

## Structure

Export a single **async** function called \`main\`:

\`\`\`typescript
//native
export async function main(param1: string, param2: number) {
  // Your code here
  return { result: param1, count: param2 };
}
\`\`\`

Do not call the main function.

## Resource Types

On Windmill, credentials and configuration are stored in resources and passed as parameters to main.

Use the \`RT\` namespace for resource types:

\`\`\`typescript
//native
export async function main(stripe: RT.Stripe) {
  // stripe contains API key and config from the resource
}
\`\`\`

Only use resource types if you need them to satisfy the instructions. Always use the RT namespace.

Before using a resource type, check the \`rt.d.ts\` file in the project root to see all available resource types and their fields. This file is generated by \`wmill resource-type generate-namespace\`.

## Imports

**The constraint is the runtime, not the import list.** You may import npm packages and relative Windmill scripts; they are resolved and bundled exactly like a regular Bun script. But the native worker only provides \`fetch\` and the JavaScript standard library, so any imported code must work using only those. Anything requiring Node/Bun built-ins (\`node:fs\`, \`child_process\`, the \`Bun\` API, native modules) belongs in a regular \`bun\` script instead. Use the globally available \`fetch\` for HTTP:

\`\`\`typescript
//native
export async function main(url: string) {
  const response = await fetch(url);
  return await response.json();
}
\`\`\`

## Windmill Client

\`windmill-client\` works on the native worker (its calls go over \`fetch\`), so use it as the **preferred way to talk to Windmill** — reading resources/variables/states, running scripts and flows, and the S3 helpers below (\`loadS3File\`, \`loadS3FileStream\`, \`writeS3File\`, \`S3Object\`). It handles auth, the workspace, and the base URL for you. Reserve raw \`fetch\` for calling *external* HTTP APIs that aren't Windmill.

The full \`windmill-client\` API reference (every exported function and its signature) is included in this skill below — consult it for the exact method instead of hand-rolling a \`fetch\` against the Windmill API.

## Preprocessor Scripts

For preprocessor scripts, the function should be named \`preprocessor\` and receives an \`event\` parameter:

\`\`\`typescript
//native
type Event = {
  kind:
    | "webhook"
    | "http"
    | "websocket"
    | "kafka"
    | "email"
    | "nats"
    | "postgres"
    | "sqs"
    | "mqtt"
    | "gcp";
  body: any;
  headers: Record<string, string>;
  query: Record<string, string>;
};

export async function preprocessor(event: Event) {
  return {
    param1: event.body.field1,
    param2: event.query.id,
  };
}
\`\`\`

## S3 Object Operations

Windmill provides built-in support for S3-compatible storage operations. The \`wmill.S3Object\` type covers both the \`s3://storage/key\` URI form (\`s3:///key\` for the workspace default storage) and the \`{ s3, storage? }\` record form — always use it instead of redefining your own.

### Receiving an S3Object as a script parameter

\`\`\`typescript
//native
import * as wmill from "windmill-client";

export async function main(file: wmill.S3Object) {
  const content = await wmill.loadS3File(file);
  // ...
}
\`\`\`

### S3 operations

\`\`\`typescript
//native
import * as wmill from "windmill-client";

// Load file content from S3
const content: Uint8Array = await wmill.loadS3File(s3object);

// Load file as stream
const blob: Blob = await wmill.loadS3FileStream(s3object);

// Write file to S3
const result: wmill.S3Object = await wmill.writeS3File(
  s3object, // Target path (or undefined to auto-generate)
  fileContent, // string or Blob
  s3ResourcePath // Optional: specific S3 resource to use
);
\`\`\`


# TypeScript SDK (windmill-client)

Import: import * as wmill from 'windmill-client'

workerHasInternalServer(): boolean

/**
 * Initialize the Windmill client with authentication token and base URL
 * @param token - Authentication token (defaults to WM_TOKEN env variable)
 * @param baseUrl - API base URL (defaults to BASE_INTERNAL_URL or BASE_URL env variable)
 */
setClient(token?: string, baseUrl?: string): void

/**
 * Create a client configuration from env variables
 * @returns client configuration
 */
getWorkspace(): string

/**
 * Get a resource value by path
 * @param path path of the resource,  default to internal state path
 * @param undefinedIfEmpty if the resource does not exist, return undefined instead of throwing an error
 * @returns resource value
 */
async getResource(path?: string, undefinedIfEmpty?: boolean): Promise<any>

/**
 * Get the true root job id
 * @param jobId job id to get the root job id from (default to current job)
 * @returns root job id
 */
async getRootJobId(jobId?: string): Promise<string>

/**
 * @deprecated Use runScriptByPath or runScriptByHash instead
 */
async runScript(path: string | null = null, hash_: string | null = null, args: Record<string, any> | null = null, verbose: boolean = false, tag: string | null = null): Promise<any>

/**
 * Run a script synchronously by its path and wait for the result
 * @param path - Script path in Windmill
 * @param args - Arguments to pass to the script
 * @param verbose - Enable verbose logging
 * @param tag - Override the worker tag the job runs on
 * @returns Script execution result
 */
async runScriptByPath(path: string, args: Record<string, any> | null = null, verbose: boolean = false, tag: string | null = null): Promise<any>

/**
 * Run a script synchronously by its hash and wait for the result
 * @param hash_ - Script hash in Windmill
 * @param args - Arguments to pass to the script
 * @param verbose - Enable verbose logging
 * @param tag - Override the worker tag the job runs on
 * @returns Script execution result
 */
async runScriptByHash(hash_: string, args: Record<string, any> | null = null, verbose: boolean = false, tag: string | null = null): Promise<any>

/**
 * Append a text to the result stream
 * @param text text to append to the result stream
 */
appendToResultStream(text: string): void

/**
 * Stream to the result stream
 * @param stream stream to stream to the result stream
 */
async streamResult(stream: AsyncIterable<string>): Promise<void>

/**
 * Run a flow synchronously by its path and wait for the result
 * @param path - Flow path in Windmill
 * @param args - Arguments to pass to the flow
 * @param verbose - Enable verbose logging
 * @param tag - Override the worker tag the job runs on
 * @returns Flow execution result
 */
async runFlow(path: string | null = null, args: Record<string, any> | null = null, verbose: boolean = false, tag: string | null = null): Promise<any>

/**
 * Wait for a job to complete and return its result
 * @param jobId - ID of the job to wait for
 * @param verbose - Enable verbose logging
 * @returns Job result when completed
 */
async waitJob(jobId: string, verbose: boolean = false): Promise<any>

/**
 * Get the result of a completed job
 * @param jobId - ID of the completed job
 * @returns Job result
 */
async getResult(jobId: string): Promise<any>

/**
 * Get the result of a job if completed, or its current status
 * @param jobId - ID of the job
 * @returns Object with started, completed, success, and result properties
 */
async getResultMaybe(jobId: string): Promise<any>

/**
 * @deprecated Use runScriptByPathAsync or runScriptByHashAsync instead
 */
async runScriptAsync(path: string | null, hash_: string | null, args: Record<string, any> | null, scheduledInSeconds: number | null = null, tag: string | null = null): Promise<string>

/**
 * Run a script asynchronously by its path
 * @param path - Script path in Windmill
 * @param args - Arguments to pass to the script
 * @param scheduledInSeconds - Schedule execution for a future time (in seconds)
 * @param tag - Override the worker tag the job runs on
 * @returns Job ID of the created job
 */
async runScriptByPathAsync(path: string, args: Record<string, any> | null = null, scheduledInSeconds: number | null = null, tag: string | null = null): Promise<string>

/**
 * Run a script asynchronously by its hash
 * @param hash_ - Script hash in Windmill
 * @param args - Arguments to pass to the script
 * @param scheduledInSeconds - Schedule execution for a future time (in seconds)
 * @param tag - Override the worker tag the job runs on
 * @returns Job ID of the created job
 */
async runScriptByHashAsync(hash_: string, args: Record<string, any> | null = null, scheduledInSeconds: number | null = null, tag: string | null = null): Promise<string>

/**
 * Run a flow asynchronously by its path
 * @param path - Flow path in Windmill
 * @param args - Arguments to pass to the flow
 * @param scheduledInSeconds - Schedule execution for a future time (in seconds)
 * @param doNotTrackInParent - If false, tracks state in parent job (only use when fully awaiting the job)
 * @param tag - Override the worker tag the job runs on
 * @returns Job ID of the created job
 */
async runFlowAsync(path: string | null, args: Record<string, any> | null, scheduledInSeconds: number | null = null, // can only be set to false if this the job will be fully await and not concurrent with any other job // as otherwise the child flow and its own child will store their state in the parent job which will // lead to incorrectness and failures doNotTrackInParent: boolean = true, tag: string | null = null): Promise<string>

/**
 * Resolve a resource value in case the default value was picked because the input payload was undefined
 * @param obj resource value or path of the resource under the format \`$res:path\`
 * @returns resource value
 */
async resolveDefaultResource(obj: any): Promise<any>

/**
 * Get the state file path from environment variables
 * @returns State path string
 */
getStatePath(): string

/**
 * Set a resource value by path
 * @param path path of the resource to set, default to state path
 * @param value new value of the resource to set
 * @param initializeToTypeIfNotExist if the resource does not exist, initialize it with this type
 */
async setResource(value: any, path?: string, initializeToTypeIfNotExist?: string): Promise<void>

/**
 * Set the state
 * @param state state to set
 * @deprecated use setState instead
 */
async setInternalState(state: any): Promise<void>

/**
 * Set the state
 * @param state state to set
 * @param path Optional state resource path override. Defaults to \`getStatePath()\`.
 */
async setState(state: any, path?: string): Promise<void>

/**
 * Set the progress
 * Progress cannot go back and limited to 0% to 99% range
 * @param percent Progress to set in %
 * @param jobId? Job to set progress for
 */
async setProgress(percent: number, jobId?: any): Promise<void>

/**
 * Get the progress
 * @param jobId? Job to get progress from
 * @returns Optional clamped between 0 and 100 progress value
 */
async getProgress(jobId?: any): Promise<number | null>

/**
 * Set a flow user state
 * @param key key of the state
 * @param value value of the state
 */
async setFlowUserState(key: string, value: any, errorIfNotPossible?: boolean): Promise<void>

/**
 * Get a flow user state
 * @param path path of the variable
 */
async getFlowUserState(key: string, errorIfNotPossible?: boolean): Promise<any>

/**
 * Get the internal state
 * @deprecated use getState instead
 */
async getInternalState(): Promise<any>

/**
 * Get the state shared across executions
 * @param path Optional state resource path override. Defaults to \`getStatePath()\`.
 */
async getState(path?: string): Promise<any>

/**
 * Get a variable by path
 * @param path path of the variable
 * @returns variable value
 */
async getVariable(path: string): Promise<string>

/**
 * Set a variable by path, create if not exist
 * @param path path of the variable
 * @param value value of the variable
 * @param isSecretIfNotExist if the variable does not exist, create it as secret or not (default: false)
 * @param descriptionIfNotExist if the variable does not exist, create it with this description (default: "")
 */
async setVariable(path: string, value: string, isSecretIfNotExist?: boolean, descriptionIfNotExist?: string): Promise<void>

/**
 * Build a PostgreSQL connection URL from a database resource
 * @param path - Path to the database resource
 * @returns PostgreSQL connection URL string
 */
async databaseUrlFromResource(path: string): Promise<string>

async polarsConnectionSettings(s3_resource_path: string | undefined): Promise<any>

async duckdbConnectionSettings(s3_resource_path: string | undefined): Promise<any>

/**
 * Get S3 client settings from a resource or workspace default
 * @param s3_resource_path - Path to S3 resource (uses workspace default if undefined)
 * @param workspace - Workspace to read from (defaults to the \`WM_WORKSPACE\` env var)
 * @returns S3 client configuration settings
 */
async denoS3LightClientSettings(s3_resource_path: string | undefined, workspace: string | undefined = undefined): Promise<DenoS3LightClientSettings>

/**
 * Load the content of a file stored in S3. If the s3ResourcePath is undefined, it will default to the workspace S3 resource.
 * 
 * \`\`\`typescript
 * let fileContent = await wmill.loadS3FileContent(inputFile)
 * // if the file is a raw text file, it can be decoded and printed directly:
 * const text = new TextDecoder().decode(fileContentStream)
 * console.log(text);
 * \`\`\`
 * 
 * @param workspace - Workspace to read from (defaults to the \`WM_WORKSPACE\` env var)
 */
async loadS3File(s3object: S3Object, s3ResourcePath: string | undefined = undefined, workspace: string | undefined = undefined): Promise<Uint8Array | undefined>

/**
 * Load the content of a file stored in S3 as a stream. If the s3ResourcePath is undefined, it will default to the workspace S3 resource.
 * 
 * \`\`\`typescript
 * let fileContentBlob = await wmill.loadS3FileStream(inputFile)
 * // if the content is plain text, the blob can be read directly:
 * console.log(await fileContentBlob.text());
 * \`\`\`
 * 
 * @param workspace - Workspace to read from (defaults to the \`WM_WORKSPACE\` env var)
 */
async loadS3FileStream(s3object: S3Object, s3ResourcePath: string | undefined = undefined, workspace: string | undefined = undefined): Promise<Blob | undefined>

/**
 * Persist a file to the S3 bucket. If the s3ResourcePath is undefined, it will default to the workspace S3 resource.
 * 
 * \`\`\`typescript
 * const s3object = await writeS3File(s3Object, "Hello Windmill!")
 * const fileContentAsUtf8Str = (await s3object.toArray()).toString('utf-8')
 * console.log(fileContentAsUtf8Str)
 * \`\`\`
 * 
 * @param workspace - Workspace to write to (defaults to the \`WM_WORKSPACE\` env var)
 */
async writeS3File(s3object: S3Object | undefined, fileContent: string | Blob, s3ResourcePath: string | undefined = undefined, contentType: string | undefined = undefined, contentDisposition: string | undefined = undefined, workspace: string | undefined = undefined): Promise<S3Object>

/**
 * Permanently delete a file from S3 by key.
 * 
 * \`\`\`typescript
 * await wmill.deleteS3File({ s3: "path/to/file.txt" })
 * \`\`\`
 * 
 * @param s3object - S3 object identifying the file to delete (must have \`s3\` set)
 * @param workspace - Workspace to delete from (defaults to the \`WM_WORKSPACE\` env var)
 */
async deleteS3File(s3object: S3Object, workspace: string | undefined = undefined): Promise<void>

/**
 * Sign S3 objects to be used by anonymous users in public apps
 * @param s3objects s3 objects to sign
 * @returns signed s3 objects
 */
async signS3Objects(s3objects: S3Object[]): Promise<S3Object[]>

/**
 * Sign S3 object to be used by anonymous users in public apps
 * @param s3object s3 object to sign
 * @returns signed s3 object
 */
async signS3Object(s3object: S3Object): Promise<S3Object>

/**
 * Generate a presigned public URL for an array of S3 objects.
 * If an S3 object is not signed yet, it will be signed first.
 * @param s3Objects s3 objects to sign
 * @returns list of signed public URLs
 */
async getPresignedS3PublicUrls(s3Objects: S3Object[], { baseUrl }: { baseUrl?: string } = {}): Promise<string[]>

/**
 * Generate a presigned public URL for an S3 object. If the S3 object is not signed yet, it will be signed first.
 * @param s3Object s3 object to sign
 * @returns signed public URL
 */
async getPresignedS3PublicUrl(s3Objects: S3Object, { baseUrl }: { baseUrl?: string } = {}): Promise<string>

/**
 * Get URLs needed for resuming a flow after this step
 * @param approver approver name
 * @param flowLevel if true, generate resume URLs for the parent flow instead of the specific step.
 *                  This allows pre-approvals that can be consumed by any later suspend step in the same flow.
 * @returns approval page UI URL, resume and cancel API URLs for resuming the flow
 */
async getResumeUrls(approver?: string, flowLevel?: boolean): Promise<{
  approvalPage: string;
  resume: string;
  cancel: string;
}>

/**
 * @deprecated use getResumeUrls instead
 */
getResumeEndpoints(approver?: string): Promise<{
  approvalPage: string;
  resume: string;
  cancel: string;
}>

/**
 * Get an OIDC jwt token for auth to external services (e.g: Vault, AWS) (ee only)
 * @param audience audience of the token
 * @param expiresIn Optional number of seconds until the token expires
 * @returns jwt token
 */
async getIdToken(audience: string, expiresIn?: number): Promise<string>

/**
 * Convert a base64-encoded string to Uint8Array
 * @param data - Base64-encoded string
 * @returns Decoded Uint8Array
 */
base64ToUint8Array(data: string): Uint8Array

/**
 * Convert a Uint8Array to base64-encoded string
 * @param arrayBuffer - Uint8Array to encode
 * @returns Base64-encoded string
 */
uint8ArrayToBase64(arrayBuffer: Uint8Array): string

/**
 * Get email from workspace username
 * This method is particularly useful for apps that require the email address of the viewer.
 * Indeed, in the viewer context, WM_USERNAME is set to the username of the viewer but WM_EMAIL is set to the email of the creator of the app.
 * @param username
 * @returns email address
 */
async usernameToEmail(username: string): Promise<string>

/**
 * Sends an interactive approval request via Slack, allowing optional customization of the message, approver, and form fields.
 * 
 * **[Enterprise Edition Only]** To include form fields in the Slack approval request, go to **Advanced -> Suspend -> Form**
 * and define a form. Learn more at [Windmill Documentation](https://www.windmill.dev/docs/flows/flow_approval#form).
 * 
 * @param {Object} options - The configuration options for the Slack approval request.
 * @param {string} options.slackResourcePath - The path to the Slack resource in Windmill.
 * @param {string} options.channelId - The Slack channel ID where the approval request will be sent.
 * @param {string} [options.message] - Optional custom message to include in the Slack approval request.
 * @param {string} [options.approver] - Optional user ID or name of the approver for the request.
 * @param {DefaultArgs} [options.defaultArgsJson] - Optional object defining or overriding the default arguments to a form field.
 * @param {Enums} [options.dynamicEnumsJson] - Optional object overriding the enum default values of an enum form field.
 * @param {string} [options.resumeButtonText] - Optional text for the resume button.
 * @param {string} [options.cancelButtonText] - Optional text for the cancel button.
 * 
 * @returns {Promise<void>} Resolves when the Slack approval request is successfully sent.
 * 
 * @throws {Error} If the function is not called within a flow or flow preview.
 * @throws {Error} If the \`JobService.getSlackApprovalPayload\` call fails.
 * 
 * **Usage Example:**
 * \`\`\`typescript
 * await requestInteractiveSlackApproval({
 *   slackResourcePath: "/u/alex/my_slack_resource",
 *   channelId: "admins-slack-channel",
 *   message: "Please approve this request",
 *   approver: "approver123",
 *   defaultArgsJson: { key1: "value1", key2: 42 },
 *   dynamicEnumsJson: { foo: ["choice1", "choice2"], bar: ["optionA", "optionB"] },
 *   resumeButtonText: "Resume",
 *   cancelButtonText: "Cancel",
 * });
 * \`\`\`
 * 
 * **Note:** This function requires execution within a Windmill flow or flow preview.
 */
async requestInteractiveSlackApproval({ slackResourcePath, channelId, message, approver, defaultArgsJson, dynamicEnumsJson, resumeButtonText, cancelButtonText, }: SlackApprovalOptions): Promise<void>

/**
 * Sends an interactive approval request via Teams, allowing optional customization of the message, approver, and form fields.
 * 
 * **[Enterprise Edition Only]** To include form fields in the Teams approval request, go to **Advanced -> Suspend -> Form**
 * and define a form. Learn more at [Windmill Documentation](https://www.windmill.dev/docs/flows/flow_approval#form).
 * 
 * @param {Object} options - The configuration options for the Teams approval request.
 * @param {string} options.teamName - The Teams team name where the approval request will be sent.
 * @param {string} options.channelName - The Teams channel name where the approval request will be sent.
 * @param {string} [options.message] - Optional custom message to include in the Teams approval request.
 * @param {string} [options.approver] - Optional user ID or name of the approver for the request.
 * @param {DefaultArgs} [options.defaultArgsJson] - Optional object defining or overriding the default arguments to a form field.
 * @param {Enums} [options.dynamicEnumsJson] - Optional object overriding the enum default values of an enum form field.
 * 
 * @returns {Promise<void>} Resolves when the Teams approval request is successfully sent.
 * 
 * @throws {Error} If the function is not called within a flow or flow preview.
 * @throws {Error} If the \`JobService.getTeamsApprovalPayload\` call fails.
 * 
 * **Usage Example:**
 * \`\`\`typescript
 * await requestInteractiveTeamsApproval({
 *   teamName: "admins-teams",
 *   channelName: "admins-teams-channel",
 *   message: "Please approve this request",
 *   approver: "approver123",
 *   defaultArgsJson: { key1: "value1", key2: 42 },
 *   dynamicEnumsJson: { foo: ["choice1", "choice2"], bar: ["optionA", "optionB"] },
 * });
 * \`\`\`
 * 
 * **Note:** This function requires execution within a Windmill flow or flow preview.
 */
async requestInteractiveTeamsApproval({ teamName, channelName, message, approver, defaultArgsJson, dynamicEnumsJson, }: TeamsApprovalOptions): Promise<void>

setWorkflowCtx(ctx: WorkflowCtx | null): void

async sleep(seconds: number): Promise<void>

async step<T>(name: string, fn: () => T | Promise<T>): Promise<T>

/**
 * Create a task that dispatches to a separate Windmill script.
 * 
 * @example
 * const extract = taskScript("f/data/extract");
 * // inside workflow: await extract({ url: "https://..." })
 */
taskScript(path: string, options?: TaskOptions): (...args: any[]) => PromiseLike<any>

/**
 * Create a task that dispatches to a separate Windmill flow.
 * 
 * @example
 * const pipeline = taskFlow("f/etl/pipeline");
 * // inside workflow: await pipeline({ input: data })
 */
taskFlow(path: string, options?: TaskOptions): (...args: any[]) => PromiseLike<any>

/**
 * Mark an async function as a workflow-as-code entry point.
 * 
 * The function must be **deterministic**: given the same inputs it must call
 * tasks in the same order on every replay. Branching on task results is fine
 * (results are replayed from checkpoint), but branching on external state
 * (current time, random values, external API calls) must use \`step()\` to
 * checkpoint the value so replays see the same result.
 */
workflow<T>(fn: (...args: any[]) => Promise<T>): void

/**
 * Suspend the workflow and wait for an external approval.
 * 
 * Use \`getResumeUrls()\` (wrapped in \`step()\`) to obtain resume/cancel/approvalPage
 * URLs before calling this function.
 * 
 * @example
 * const urls = await step("urls", () => getResumeUrls());
 * await step("notify", () => sendEmail(urls.approvalPage));
 * const { value, approver } = await waitForApproval({ timeout: 3600 });
 */
waitForApproval(options?: { timeout?: number; form?: object; selfApproval?: boolean; }): PromiseLike<{ value: any; approver: string; approved: boolean }>

/**
 * Process items in parallel with optional concurrency control.
 * 
 * Each item is processed by calling \`fn(item)\`, which should be a task().
 * Items are dispatched in batches of \`concurrency\` (default: all at once).
 * 
 * @example
 * const process = task(async (item: string) => { ... });
 * const results = await parallel(items, process, { concurrency: 5 });
 */
async parallel<T, R>(items: T[], fn: (item: T) => PromiseLike<R> | R, options?: { concurrency?: number },): Promise<R[]>

/**
 * Commit Kafka offsets for a trigger with auto_commit disabled.
 * @param triggerPath - Path to the Kafka trigger (from event.wm_trigger.trigger_path)
 * @param topic - Kafka topic name (from event.topic)
 * @param partition - Partition number (from event.partition)
 * @param offset - Message offset to commit (from event.offset)
 */
async commitKafkaOffsets(triggerPath: string, topic: string, partition: number, offset: number,): Promise<void>

/**
 * Parse an S3 object from URI string or record format
 * @param s3Object - S3 object as URI string (\`s3://storage/key\`, \`s3:///key\`
 *   for the default storage) or record. Any other string throws rather than
 *   falling back to an auto-generated key: an auto key is requested by
 *   omitting the object, and a fallback would silently misplace the upload
 *   on any typo.
 * @returns S3 object record with storage and s3 key
 */
parseS3Object(s3Object: S3Object): S3ObjectRecord

/**
 * Create a SQL template function for PostgreSQL/datatable queries
 * @param name - Database/datatable name (default: "main")
 * @returns SQL template function for building parameterized queries
 * @example
 * let sql = wmill.datatable()
 * let name = 'Robin'
 * let age = 21
 * await sql\`
 *   SELECT * FROM friends
 *     WHERE name = \${name} AND age = \${age}::int
 * \`.fetch()
 */
datatable(name: string = "main"): DatatableSqlTemplateFunction

/**
 * Create a SQL template function for DuckDB/ducklake queries
 * @param name - DuckDB database name, optionally with a schema as \`name:schema\` (default: "main")
 * @returns SQL template function for building parameterized queries
 * @example
 * let sql = wmill.ducklake()
 * let name = 'Robin'
 * let age = 21
 * await sql\`
 *   SELECT * FROM friends
 *     WHERE name = \${name} AND age = \${age}
 * \`.fetch()
 * @example
 * // Target a specific schema within the ducklake
 * let sql = wmill.ducklake("my_lake:analytics")
 */
ducklake(name: string = "main"): SqlTemplateFunction

/**
 * Idempotently materialize \`selectSql\` into a ducklake table for one
 * partition (or the whole table when \`partition\` is omitted) — the client-side
 * equivalent of the \`// materialize\` engine.
 * With \`uniqueKey\` it upserts the slice (delete-by-key + insert); otherwise it
 * replaces it (whole table → \`CREATE OR REPLACE\`; partition → delete + insert).
 * Safe to re-run for the same partition (backfill / failure-recovery).
 * 
 * Returns a lazy statement — call \`.execute()\` to run it:
 * \`await wmill.upsertPartition({ table, selectSql, partition }).execute()\`.
 */
upsertPartition(opts: DucklakeMaterializeOptions): SqlStatement<any>

/**
 * INSERT-only materialization (no dedup/replace) for append-only tables.
 * Re-running the same partition duplicates rows — use only for immutable
 * event-log sources.
 * 
 * Returns a lazy statement — call \`.execute()\` to run it:
 * \`await wmill.appendPartition({ table, selectSql, partition }).execute()\`.
 */
appendPartition(opts: Omit<DucklakeMaterializeOptions, "uniqueKey">,): SqlStatement<any>
`,
  "write-script-csharp": `---
name: write-script-csharp
description: MUST use when writing C# scripts.
---

## CLI Commands

Place scripts in a folder.

After writing, tell the user which command fits what they want to do:

- \`wmill script preview <script_path>\` — **default when iterating on a local script.** Runs the local file without deploying.
- \`wmill script run <path>\` — runs the script **already deployed** in the workspace. Use only when the user explicitly wants to test the deployed version, not local edits.
- \`wmill generate-metadata\` — regenerate the local \`.script.yaml\` (input schema) and \`.lock\` (resolved dependencies) for scripts you changed, and refresh their content hashes in \`wmill-lock.yaml\`. Local files only — **not** a deploy. See "Keep metadata in sync" below.
- 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 script", "try it", "test it", "does it work" while there are **local edits to the script file**, use \`script preview\`. Do NOT push the script to then \`script run\` it — pushing is a deploy, and deploying just to test overwrites the workspace version with untested changes.

Only use \`script run\` when:
- The user explicitly says "run the deployed version" / "run what's on the server".
- There is no local script being edited (you're just invoking an existing script).

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.

### Keep metadata in sync after editing

\`wmill-lock.yaml\` tracks a content hash for each item. Editing a script's content — most importantly **adding or removing an import** or **changing \`main\`'s arguments** — invalidates that hash and leaves the \`.lock\`, the \`.script.yaml\` input schema, and the hash row out of date. Run \`wmill generate-metadata\` (scoped to what you touched) after such edits so the resolved lock, the auto-generated args UI (driven by \`.script.yaml\`), and \`wmill-lock.yaml\` all match the code. Leaving them stale produces spurious diffs in git-sync and CI.

This only writes local files (it is **not** a deploy), but it re-resolves dependencies, so it can bump unpinned versions (the same as deploying from the UI; expected, not a bug). So by default offer it and run it once the user agrees, rather than running it silently after every edit — unless the project's \`AGENTS.md\` opts into running metadata automatically (see the "Keeping metadata in sync" preference there). Either way YOU run the command, not the user. After running it, diff the regenerated \`.lock\` / \`.script.lock\` files and tell the user which dependency versions changed (e.g. \`requests 2.31.0 → 2.32.0\`), so they can catch an unwanted bump before deploying — even under \`Metadata: auto\`, since it's information, not a confirmation gate. Pin versions in code to keep them fixed.

With no path argument, \`generate-metadata\` regenerates only the items whose content hash drifted — not everything. Imports propagate: editing a script that others import marks every importer stale too, so a one-line change to a shared module can regenerate many locks (by design — their locks must reflect the imported code). If it touches more than you expect, run \`wmill generate-metadata --dry-run\` — it lists each stale item with a reason (\`content changed\` or \`depends on <path>\`) without changing anything — then narrow with a path argument (\`wmill generate-metadata f/foo\`) or \`--strict-folder-boundaries\`.

If the on-disk \`.lock\` and \`.script.yaml\` are already correct and only \`wmill-lock.yaml\` needs its hashes refreshed (hash drift, or bootstrapping missing entries), use \`wmill generate-metadata rehash\` — it re-records hashes from disk with no backend round-trip and no dependency changes.

### After writing — offer to test, don't wait passively

If the user hasn't already told you to run/test/preview the script, offer it as a one-sentence next step (e.g. "Want me to run \`wmill script preview\` with sample args?"). Do not present a multi-option menu.

If the user already asked to test/run/try the script in their original request, skip the offer and just execute \`wmill script preview <path> -d '<args>'\` directly — pick plausible args from the script's declared parameters. The shape varies by language: \`main(...)\` for code languages, the SQL dialect's own placeholder syntax (\`$1\` for PostgreSQL, \`?\` for MySQL/Snowflake, \`@P1\` for MSSQL, \`@name\` for BigQuery, etc.), positional \`$1\`, \`$2\`, … for Bash, \`param(...)\` for PowerShell.

\`wmill script preview\` does not deploy, but it still executes script code and may cause side effects; run it yourself when the user asked to test/preview (or after confirming that execution is intended). \`wmill generate-metadata\` does not deploy either — it only writes local files (locks, schemas, hashes) — but offer it before running (or run automatically if the project's \`AGENTS.md\` opts in), per "Keep metadata in sync" above. Deploying to the workspace (\`git push\` or \`wmill sync push\` depending on how the repo is wired — see the **Deploying** section) is the only step that mutates remote state — do it only when the user explicitly asks to deploy/publish/push.

For a **visual** open-the-script-in-the-dev-page preview (rather than \`script preview\`'s run-and-print-result), use the \`preview\` skill.

Use \`wmill resource-type list --schema\` to discover available resource types.

# C#

The script must contain a public static \`Main\` method inside a class:

\`\`\`csharp
public class Script
{
    public static object Main(string name, int count)
    {
        return new { Name = name, Count = count };
    }
}
\`\`\`

**Important:**
- Class name is irrelevant
- Method must be \`public static\`
- Return type can be \`object\` or specific type

## NuGet Packages

Add packages using the \`#r\` directive at the top:

\`\`\`csharp
#r "nuget: Newtonsoft.Json, 13.0.3"
#r "nuget: RestSharp, 110.2.0"

using Newtonsoft.Json;
using RestSharp;

public class Script
{
    public static object Main(string url)
    {
        var client = new RestClient(url);
        var request = new RestRequest();
        var response = client.Get(request);
        return JsonConvert.DeserializeObject(response.Content);
    }
}
\`\`\`
`,
  "write-script-deno": `---
name: write-script-deno
description: Use ONLY when a TypeScript script specifically requires the Deno runtime (Deno stdlib or deno.land URL imports). For all other TypeScript, use write-script-bun instead.
---

## CLI Commands

Place scripts in a folder.

After writing, tell the user which command fits what they want to do:

- \`wmill script preview <script_path>\` — **default when iterating on a local script.** Runs the local file without deploying.
- \`wmill script run <path>\` — runs the script **already deployed** in the workspace. Use only when the user explicitly wants to test the deployed version, not local edits.
- \`wmill generate-metadata\` — regenerate the local \`.script.yaml\` (input schema) and \`.lock\` (resolved dependencies) for scripts you changed, and refresh their content hashes in \`wmill-lock.yaml\`. Local files only — **not** a deploy. See "Keep metadata in sync" below.
- 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 script", "try it", "test it", "does it work" while there are **local edits to the script file**, use \`script preview\`. Do NOT push the script to then \`script run\` it — pushing is a deploy, and deploying just to test overwrites the workspace version with untested changes.

Only use \`script run\` when:
- The user explicitly says "run the deployed version" / "run what's on the server".
- There is no local script being edited (you're just invoking an existing script).

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.

### Keep metadata in sync after editing

\`wmill-lock.yaml\` tracks a content hash for each item. Editing a script's content — most importantly **adding or removing an import** or **changing \`main\`'s arguments** — invalidates that hash and leaves the \`.lock\`, the \`.script.yaml\` input schema, and the hash row out of date. Run \`wmill generate-metadata\` (scoped to what you touched) after such edits so the resolved lock, the auto-generated args UI (driven by \`.script.yaml\`), and \`wmill-lock.yaml\` all match the code. Leaving them stale produces spurious diffs in git-sync and CI.

This only writes local files (it is **not** a deploy), but it re-resolves dependencies, so it can bump unpinned versions (the same as deploying from the UI; expected, not a bug). So by default offer it and run it once the user agrees, rather than running it silently after every edit — unless the project's \`AGENTS.md\` opts into running metadata automatically (see the "Keeping metadata in sync" preference there). Either way YOU run the command, not the user. After running it, diff the regenerated \`.lock\` / \`.script.lock\` files and tell the user which dependency versions changed (e.g. \`requests 2.31.0 → 2.32.0\`), so they can catch an unwanted bump before deploying — even under \`Metadata: auto\`, since it's information, not a confirmation gate. Pin versions in code to keep them fixed.

With no path argument, \`generate-metadata\` regenerates only the items whose content hash drifted — not everything. Imports propagate: editing a script that others import marks every importer stale too, so a one-line change to a shared module can regenerate many locks (by design — their locks must reflect the imported code). If it touches more than you expect, run \`wmill generate-metadata --dry-run\` — it lists each stale item with a reason (\`content changed\` or \`depends on <path>\`) without changing anything — then narrow with a path argument (\`wmill generate-metadata f/foo\`) or \`--strict-folder-boundaries\`.

If the on-disk \`.lock\` and \`.script.yaml\` are already correct and only \`wmill-lock.yaml\` needs its hashes refreshed (hash drift, or bootstrapping missing entries), use \`wmill generate-metadata rehash\` — it re-records hashes from disk with no backend round-trip and no dependency changes.

### After writing — offer to test, don't wait passively

If the user hasn't already told you to run/test/preview the script, offer it as a one-sentence next step (e.g. "Want me to run \`wmill script preview\` with sample args?"). Do not present a multi-option menu.

If the user already asked to test/run/try the script in their original request, skip the offer and just execute \`wmill script preview <path> -d '<args>'\` directly — pick plausible args from the script's declared parameters. The shape varies by language: \`main(...)\` for code languages, the SQL dialect's own placeholder syntax (\`$1\` for PostgreSQL, \`?\` for MySQL/Snowflake, \`@P1\` for MSSQL, \`@name\` for BigQuery, etc.), positional \`$1\`, \`$2\`, … for Bash, \`param(...)\` for PowerShell.

\`wmill script preview\` does not deploy, but it still executes script code and may cause side effects; run it yourself when the user asked to test/preview (or after confirming that execution is intended). \`wmill generate-metadata\` does not deploy either — it only writes local files (locks, schemas, hashes) — but offer it before running (or run automatically if the project's \`AGENTS.md\` opts in), per "Keep metadata in sync" above. Deploying to the workspace (\`git push\` or \`wmill sync push\` depending on how the repo is wired — see the **Deploying** section) is the only step that mutates remote state — do it only when the user explicitly asks to deploy/publish/push.

For a **visual** open-the-script-in-the-dev-page preview (rather than \`script preview\`'s run-and-print-result), use the \`preview\` skill.

Use \`wmill resource-type list --schema\` to discover available resource types.

# TypeScript (Deno)

Deno runtime with npm support via \`npm:\` prefix and native Deno libraries.

**Prefer Bun (\`write-script-bun\`) for TypeScript.** Only use Deno when the script specifically requires the Deno runtime — Deno's standard library or \`deno.land\` URL imports that have no npm equivalent. For all other TypeScript, use Bun instead.

## Structure

Export a single **async** function called \`main\`:

\`\`\`typescript
export async function main(param1: string, param2: number) {
  // Your code here
  return { result: param1, count: param2 };
}
\`\`\`

Do not call the main function. Libraries are installed automatically.

## Resource Types

On Windmill, credentials and configuration are stored in resources and passed as parameters to main.

Use the \`RT\` namespace for resource types:

\`\`\`typescript
export async function main(stripe: RT.Stripe) {
  // stripe contains API key and config from the resource
}
\`\`\`

Only use resource types if you need them to satisfy the instructions. Always use the RT namespace.

Before using a resource type, check the \`rt.d.ts\` file in the project root to see all available resource types and their fields. This file is generated by \`wmill resource-type generate-namespace\`.

## Imports

\`\`\`typescript
// npm packages use npm: prefix
import Stripe from "npm:stripe";
import { someFunction } from "npm:some-package";

// Deno standard library
import { serve } from "https://deno.land/std/http/server.ts";
\`\`\`

## Windmill Client

Import the windmill client for platform interactions:

\`\`\`typescript
import * as wmill from "windmill-client";
\`\`\`

**Prefer \`windmill-client\` over raw \`fetch\` for anything that talks to Windmill** — reading resources/variables/states, running scripts and flows, S3 object operations, etc. It handles auth, the workspace, and the base URL for you. Reserve \`fetch\` for calling *external* HTTP APIs that aren't Windmill.

The full \`windmill-client\` API reference (every exported function and its signature) is included in this skill below — consult it for the exact method instead of guessing or falling back to \`fetch\`.

## Preprocessor Scripts

For preprocessor scripts, the function should be named \`preprocessor\` and receives an \`event\` parameter:

\`\`\`typescript
type Event = {
  kind:
    | "webhook"
    | "http"
    | "websocket"
    | "kafka"
    | "email"
    | "nats"
    | "postgres"
    | "sqs"
    | "mqtt"
    | "gcp";
  body: any;
  headers: Record<string, string>;
  query: Record<string, string>;
};

export async function preprocessor(event: Event) {
  return {
    param1: event.body.field1,
    param2: event.query.id,
  };
}
\`\`\`

## S3 Object Operations

Windmill provides built-in support for S3-compatible storage operations. The \`wmill.S3Object\` type covers both the \`s3://storage/key\` URI form (\`s3:///key\` for the workspace default storage) and the \`{ s3, storage? }\` record form — always use it instead of redefining your own.

### Receiving an S3Object as a script parameter

\`\`\`typescript
import * as wmill from "windmill-client";

export async function main(file: wmill.S3Object) {
  const content = await wmill.loadS3File(file);
  // ...
}
\`\`\`

### S3 operations

\`\`\`typescript
import * as wmill from "windmill-client";

// Load file content from S3
const content: Uint8Array = await wmill.loadS3File(s3object);

// Load file as stream
const blob: Blob = await wmill.loadS3FileStream(s3object);

// Write file to S3
const result: wmill.S3Object = await wmill.writeS3File(
  s3object, // Target path (or undefined to auto-generate)
  fileContent, // string or Blob
  s3ResourcePath // Optional: specific S3 resource to use
);
\`\`\`


# TypeScript SDK (windmill-client)

Import: import * as wmill from 'windmill-client'

workerHasInternalServer(): boolean

/**
 * Initialize the Windmill client with authentication token and base URL
 * @param token - Authentication token (defaults to WM_TOKEN env variable)
 * @param baseUrl - API base URL (defaults to BASE_INTERNAL_URL or BASE_URL env variable)
 */
setClient(token?: string, baseUrl?: string): void

/**
 * Create a client configuration from env variables
 * @returns client configuration
 */
getWorkspace(): string

/**
 * Get a resource value by path
 * @param path path of the resource,  default to internal state path
 * @param undefinedIfEmpty if the resource does not exist, return undefined instead of throwing an error
 * @returns resource value
 */
async getResource(path?: string, undefinedIfEmpty?: boolean): Promise<any>

/**
 * Get the true root job id
 * @param jobId job id to get the root job id from (default to current job)
 * @returns root job id
 */
async getRootJobId(jobId?: string): Promise<string>

/**
 * @deprecated Use runScriptByPath or runScriptByHash instead
 */
async runScript(path: string | null = null, hash_: string | null = null, args: Record<string, any> | null = null, verbose: boolean = false, tag: string | null = null): Promise<any>

/**
 * Run a script synchronously by its path and wait for the result
 * @param path - Script path in Windmill
 * @param args - Arguments to pass to the script
 * @param verbose - Enable verbose logging
 * @param tag - Override the worker tag the job runs on
 * @returns Script execution result
 */
async runScriptByPath(path: string, args: Record<string, any> | null = null, verbose: boolean = false, tag: string | null = null): Promise<any>

/**
 * Run a script synchronously by its hash and wait for the result
 * @param hash_ - Script hash in Windmill
 * @param args - Arguments to pass to the script
 * @param verbose - Enable verbose logging
 * @param tag - Override the worker tag the job runs on
 * @returns Script execution result
 */
async runScriptByHash(hash_: string, args: Record<string, any> | null = null, verbose: boolean = false, tag: string | null = null): Promise<any>

/**
 * Append a text to the result stream
 * @param text text to append to the result stream
 */
appendToResultStream(text: string): void

/**
 * Stream to the result stream
 * @param stream stream to stream to the result stream
 */
async streamResult(stream: AsyncIterable<string>): Promise<void>

/**
 * Run a flow synchronously by its path and wait for the result
 * @param path - Flow path in Windmill
 * @param args - Arguments to pass to the flow
 * @param verbose - Enable verbose logging
 * @param tag - Override the worker tag the job runs on
 * @returns Flow execution result
 */
async runFlow(path: string | null = null, args: Record<string, any> | null = null, verbose: boolean = false, tag: string | null = null): Promise<any>

/**
 * Wait for a job to complete and return its result
 * @param jobId - ID of the job to wait for
 * @param verbose - Enable verbose logging
 * @returns Job result when completed
 */
async waitJob(jobId: string, verbose: boolean = false): Promise<any>

/**
 * Get the result of a completed job
 * @param jobId - ID of the completed job
 * @returns Job result
 */
async getResult(jobId: string): Promise<any>

/**
 * Get the result of a job if completed, or its current status
 * @param jobId - ID of the job
 * @returns Object with started, completed, success, and result properties
 */
async getResultMaybe(jobId: string): Promise<any>

/**
 * @deprecated Use runScriptByPathAsync or runScriptByHashAsync instead
 */
async runScriptAsync(path: string | null, hash_: string | null, args: Record<string, any> | null, scheduledInSeconds: number | null = null, tag: string | null = null): Promise<string>

/**
 * Run a script asynchronously by its path
 * @param path - Script path in Windmill
 * @param args - Arguments to pass to the script
 * @param scheduledInSeconds - Schedule execution for a future time (in seconds)
 * @param tag - Override the worker tag the job runs on
 * @returns Job ID of the created job
 */
async runScriptByPathAsync(path: string, args: Record<string, any> | null = null, scheduledInSeconds: number | null = null, tag: string | null = null): Promise<string>

/**
 * Run a script asynchronously by its hash
 * @param hash_ - Script hash in Windmill
 * @param args - Arguments to pass to the script
 * @param scheduledInSeconds - Schedule execution for a future time (in seconds)
 * @param tag - Override the worker tag the job runs on
 * @returns Job ID of the created job
 */
async runScriptByHashAsync(hash_: string, args: Record<string, any> | null = null, scheduledInSeconds: number | null = null, tag: string | null = null): Promise<string>

/**
 * Run a flow asynchronously by its path
 * @param path - Flow path in Windmill
 * @param args - Arguments to pass to the flow
 * @param scheduledInSeconds - Schedule execution for a future time (in seconds)
 * @param doNotTrackInParent - If false, tracks state in parent job (only use when fully awaiting the job)
 * @param tag - Override the worker tag the job runs on
 * @returns Job ID of the created job
 */
async runFlowAsync(path: string | null, args: Record<string, any> | null, scheduledInSeconds: number | null = null, // can only be set to false if this the job will be fully await and not concurrent with any other job // as otherwise the child flow and its own child will store their state in the parent job which will // lead to incorrectness and failures doNotTrackInParent: boolean = true, tag: string | null = null): Promise<string>

/**
 * Resolve a resource value in case the default value was picked because the input payload was undefined
 * @param obj resource value or path of the resource under the format \`$res:path\`
 * @returns resource value
 */
async resolveDefaultResource(obj: any): Promise<any>

/**
 * Get the state file path from environment variables
 * @returns State path string
 */
getStatePath(): string

/**
 * Set a resource value by path
 * @param path path of the resource to set, default to state path
 * @param value new value of the resource to set
 * @param initializeToTypeIfNotExist if the resource does not exist, initialize it with this type
 */
async setResource(value: any, path?: string, initializeToTypeIfNotExist?: string): Promise<void>

/**
 * Set the state
 * @param state state to set
 * @deprecated use setState instead
 */
async setInternalState(state: any): Promise<void>

/**
 * Set the state
 * @param state state to set
 * @param path Optional state resource path override. Defaults to \`getStatePath()\`.
 */
async setState(state: any, path?: string): Promise<void>

/**
 * Set the progress
 * Progress cannot go back and limited to 0% to 99% range
 * @param percent Progress to set in %
 * @param jobId? Job to set progress for
 */
async setProgress(percent: number, jobId?: any): Promise<void>

/**
 * Get the progress
 * @param jobId? Job to get progress from
 * @returns Optional clamped between 0 and 100 progress value
 */
async getProgress(jobId?: any): Promise<number | null>

/**
 * Set a flow user state
 * @param key key of the state
 * @param value value of the state
 */
async setFlowUserState(key: string, value: any, errorIfNotPossible?: boolean): Promise<void>

/**
 * Get a flow user state
 * @param path path of the variable
 */
async getFlowUserState(key: string, errorIfNotPossible?: boolean): Promise<any>

/**
 * Get the internal state
 * @deprecated use getState instead
 */
async getInternalState(): Promise<any>

/**
 * Get the state shared across executions
 * @param path Optional state resource path override. Defaults to \`getStatePath()\`.
 */
async getState(path?: string): Promise<any>

/**
 * Get a variable by path
 * @param path path of the variable
 * @returns variable value
 */
async getVariable(path: string): Promise<string>

/**
 * Set a variable by path, create if not exist
 * @param path path of the variable
 * @param value value of the variable
 * @param isSecretIfNotExist if the variable does not exist, create it as secret or not (default: false)
 * @param descriptionIfNotExist if the variable does not exist, create it with this description (default: "")
 */
async setVariable(path: string, value: string, isSecretIfNotExist?: boolean, descriptionIfNotExist?: string): Promise<void>

/**
 * Build a PostgreSQL connection URL from a database resource
 * @param path - Path to the database resource
 * @returns PostgreSQL connection URL string
 */
async databaseUrlFromResource(path: string): Promise<string>

async polarsConnectionSettings(s3_resource_path: string | undefined): Promise<any>

async duckdbConnectionSettings(s3_resource_path: string | undefined): Promise<any>

/**
 * Get S3 client settings from a resource or workspace default
 * @param s3_resource_path - Path to S3 resource (uses workspace default if undefined)
 * @param workspace - Workspace to read from (defaults to the \`WM_WORKSPACE\` env var)
 * @returns S3 client configuration settings
 */
async denoS3LightClientSettings(s3_resource_path: string | undefined, workspace: string | undefined = undefined): Promise<DenoS3LightClientSettings>

/**
 * Load the content of a file stored in S3. If the s3ResourcePath is undefined, it will default to the workspace S3 resource.
 * 
 * \`\`\`typescript
 * let fileContent = await wmill.loadS3FileContent(inputFile)
 * // if the file is a raw text file, it can be decoded and printed directly:
 * const text = new TextDecoder().decode(fileContentStream)
 * console.log(text);
 * \`\`\`
 * 
 * @param workspace - Workspace to read from (defaults to the \`WM_WORKSPACE\` env var)
 */
async loadS3File(s3object: S3Object, s3ResourcePath: string | undefined = undefined, workspace: string | undefined = undefined): Promise<Uint8Array | undefined>

/**
 * Load the content of a file stored in S3 as a stream. If the s3ResourcePath is undefined, it will default to the workspace S3 resource.
 * 
 * \`\`\`typescript
 * let fileContentBlob = await wmill.loadS3FileStream(inputFile)
 * // if the content is plain text, the blob can be read directly:
 * console.log(await fileContentBlob.text());
 * \`\`\`
 * 
 * @param workspace - Workspace to read from (defaults to the \`WM_WORKSPACE\` env var)
 */
async loadS3FileStream(s3object: S3Object, s3ResourcePath: string | undefined = undefined, workspace: string | undefined = undefined): Promise<Blob | undefined>

/**
 * Persist a file to the S3 bucket. If the s3ResourcePath is undefined, it will default to the workspace S3 resource.
 * 
 * \`\`\`typescript
 * const s3object = await writeS3File(s3Object, "Hello Windmill!")
 * const fileContentAsUtf8Str = (await s3object.toArray()).toString('utf-8')
 * console.log(fileContentAsUtf8Str)
 * \`\`\`
 * 
 * @param workspace - Workspace to write to (defaults to the \`WM_WORKSPACE\` env var)
 */
async writeS3File(s3object: S3Object | undefined, fileContent: string | Blob, s3ResourcePath: string | undefined = undefined, contentType: string | undefined = undefined, contentDisposition: string | undefined = undefined, workspace: string | undefined = undefined): Promise<S3Object>

/**
 * Permanently delete a file from S3 by key.
 * 
 * \`\`\`typescript
 * await wmill.deleteS3File({ s3: "path/to/file.txt" })
 * \`\`\`
 * 
 * @param s3object - S3 object identifying the file to delete (must have \`s3\` set)
 * @param workspace - Workspace to delete from (defaults to the \`WM_WORKSPACE\` env var)
 */
async deleteS3File(s3object: S3Object, workspace: string | undefined = undefined): Promise<void>

/**
 * Sign S3 objects to be used by anonymous users in public apps
 * @param s3objects s3 objects to sign
 * @returns signed s3 objects
 */
async signS3Objects(s3objects: S3Object[]): Promise<S3Object[]>

/**
 * Sign S3 object to be used by anonymous users in public apps
 * @param s3object s3 object to sign
 * @returns signed s3 object
 */
async signS3Object(s3object: S3Object): Promise<S3Object>

/**
 * Generate a presigned public URL for an array of S3 objects.
 * If an S3 object is not signed yet, it will be signed first.
 * @param s3Objects s3 objects to sign
 * @returns list of signed public URLs
 */
async getPresignedS3PublicUrls(s3Objects: S3Object[], { baseUrl }: { baseUrl?: string } = {}): Promise<string[]>

/**
 * Generate a presigned public URL for an S3 object. If the S3 object is not signed yet, it will be signed first.
 * @param s3Object s3 object to sign
 * @returns signed public URL
 */
async getPresignedS3PublicUrl(s3Objects: S3Object, { baseUrl }: { baseUrl?: string } = {}): Promise<string>

/**
 * Get URLs needed for resuming a flow after this step
 * @param approver approver name
 * @param flowLevel if true, generate resume URLs for the parent flow instead of the specific step.
 *                  This allows pre-approvals that can be consumed by any later suspend step in the same flow.
 * @returns approval page UI URL, resume and cancel API URLs for resuming the flow
 */
async getResumeUrls(approver?: string, flowLevel?: boolean): Promise<{
  approvalPage: string;
  resume: string;
  cancel: string;
}>

/**
 * @deprecated use getResumeUrls instead
 */
getResumeEndpoints(approver?: string): Promise<{
  approvalPage: string;
  resume: string;
  cancel: string;
}>

/**
 * Get an OIDC jwt token for auth to external services (e.g: Vault, AWS) (ee only)
 * @param audience audience of the token
 * @param expiresIn Optional number of seconds until the token expires
 * @returns jwt token
 */
async getIdToken(audience: string, expiresIn?: number): Promise<string>

/**
 * Convert a base64-encoded string to Uint8Array
 * @param data - Base64-encoded string
 * @returns Decoded Uint8Array
 */
base64ToUint8Array(data: string): Uint8Array

/**
 * Convert a Uint8Array to base64-encoded string
 * @param arrayBuffer - Uint8Array to encode
 * @returns Base64-encoded string
 */
uint8ArrayToBase64(arrayBuffer: Uint8Array): string

/**
 * Get email from workspace username
 * This method is particularly useful for apps that require the email address of the viewer.
 * Indeed, in the viewer context, WM_USERNAME is set to the username of the viewer but WM_EMAIL is set to the email of the creator of the app.
 * @param username
 * @returns email address
 */
async usernameToEmail(username: string): Promise<string>

/**
 * Sends an interactive approval request via Slack, allowing optional customization of the message, approver, and form fields.
 * 
 * **[Enterprise Edition Only]** To include form fields in the Slack approval request, go to **Advanced -> Suspend -> Form**
 * and define a form. Learn more at [Windmill Documentation](https://www.windmill.dev/docs/flows/flow_approval#form).
 * 
 * @param {Object} options - The configuration options for the Slack approval request.
 * @param {string} options.slackResourcePath - The path to the Slack resource in Windmill.
 * @param {string} options.channelId - The Slack channel ID where the approval request will be sent.
 * @param {string} [options.message] - Optional custom message to include in the Slack approval request.
 * @param {string} [options.approver] - Optional user ID or name of the approver for the request.
 * @param {DefaultArgs} [options.defaultArgsJson] - Optional object defining or overriding the default arguments to a form field.
 * @param {Enums} [options.dynamicEnumsJson] - Optional object overriding the enum default values of an enum form field.
 * @param {string} [options.resumeButtonText] - Optional text for the resume button.
 * @param {string} [options.cancelButtonText] - Optional text for the cancel button.
 * 
 * @returns {Promise<void>} Resolves when the Slack approval request is successfully sent.
 * 
 * @throws {Error} If the function is not called within a flow or flow preview.
 * @throws {Error} If the \`JobService.getSlackApprovalPayload\` call fails.
 * 
 * **Usage Example:**
 * \`\`\`typescript
 * await requestInteractiveSlackApproval({
 *   slackResourcePath: "/u/alex/my_slack_resource",
 *   channelId: "admins-slack-channel",
 *   message: "Please approve this request",
 *   approver: "approver123",
 *   defaultArgsJson: { key1: "value1", key2: 42 },
 *   dynamicEnumsJson: { foo: ["choice1", "choice2"], bar: ["optionA", "optionB"] },
 *   resumeButtonText: "Resume",
 *   cancelButtonText: "Cancel",
 * });
 * \`\`\`
 * 
 * **Note:** This function requires execution within a Windmill flow or flow preview.
 */
async requestInteractiveSlackApproval({ slackResourcePath, channelId, message, approver, defaultArgsJson, dynamicEnumsJson, resumeButtonText, cancelButtonText, }: SlackApprovalOptions): Promise<void>

/**
 * Sends an interactive approval request via Teams, allowing optional customization of the message, approver, and form fields.
 * 
 * **[Enterprise Edition Only]** To include form fields in the Teams approval request, go to **Advanced -> Suspend -> Form**
 * and define a form. Learn more at [Windmill Documentation](https://www.windmill.dev/docs/flows/flow_approval#form).
 * 
 * @param {Object} options - The configuration options for the Teams approval request.
 * @param {string} options.teamName - The Teams team name where the approval request will be sent.
 * @param {string} options.channelName - The Teams channel name where the approval request will be sent.
 * @param {string} [options.message] - Optional custom message to include in the Teams approval request.
 * @param {string} [options.approver] - Optional user ID or name of the approver for the request.
 * @param {DefaultArgs} [options.defaultArgsJson] - Optional object defining or overriding the default arguments to a form field.
 * @param {Enums} [options.dynamicEnumsJson] - Optional object overriding the enum default values of an enum form field.
 * 
 * @returns {Promise<void>} Resolves when the Teams approval request is successfully sent.
 * 
 * @throws {Error} If the function is not called within a flow or flow preview.
 * @throws {Error} If the \`JobService.getTeamsApprovalPayload\` call fails.
 * 
 * **Usage Example:**
 * \`\`\`typescript
 * await requestInteractiveTeamsApproval({
 *   teamName: "admins-teams",
 *   channelName: "admins-teams-channel",
 *   message: "Please approve this request",
 *   approver: "approver123",
 *   defaultArgsJson: { key1: "value1", key2: 42 },
 *   dynamicEnumsJson: { foo: ["choice1", "choice2"], bar: ["optionA", "optionB"] },
 * });
 * \`\`\`
 * 
 * **Note:** This function requires execution within a Windmill flow or flow preview.
 */
async requestInteractiveTeamsApproval({ teamName, channelName, message, approver, defaultArgsJson, dynamicEnumsJson, }: TeamsApprovalOptions): Promise<void>

setWorkflowCtx(ctx: WorkflowCtx | null): void

async sleep(seconds: number): Promise<void>

async step<T>(name: string, fn: () => T | Promise<T>): Promise<T>

/**
 * Create a task that dispatches to a separate Windmill script.
 * 
 * @example
 * const extract = taskScript("f/data/extract");
 * // inside workflow: await extract({ url: "https://..." })
 */
taskScript(path: string, options?: TaskOptions): (...args: any[]) => PromiseLike<any>

/**
 * Create a task that dispatches to a separate Windmill flow.
 * 
 * @example
 * const pipeline = taskFlow("f/etl/pipeline");
 * // inside workflow: await pipeline({ input: data })
 */
taskFlow(path: string, options?: TaskOptions): (...args: any[]) => PromiseLike<any>

/**
 * Mark an async function as a workflow-as-code entry point.
 * 
 * The function must be **deterministic**: given the same inputs it must call
 * tasks in the same order on every replay. Branching on task results is fine
 * (results are replayed from checkpoint), but branching on external state
 * (current time, random values, external API calls) must use \`step()\` to
 * checkpoint the value so replays see the same result.
 */
workflow<T>(fn: (...args: any[]) => Promise<T>): void

/**
 * Suspend the workflow and wait for an external approval.
 * 
 * Use \`getResumeUrls()\` (wrapped in \`step()\`) to obtain resume/cancel/approvalPage
 * URLs before calling this function.
 * 
 * @example
 * const urls = await step("urls", () => getResumeUrls());
 * await step("notify", () => sendEmail(urls.approvalPage));
 * const { value, approver } = await waitForApproval({ timeout: 3600 });
 */
waitForApproval(options?: { timeout?: number; form?: object; selfApproval?: boolean; }): PromiseLike<{ value: any; approver: string; approved: boolean }>

/**
 * Process items in parallel with optional concurrency control.
 * 
 * Each item is processed by calling \`fn(item)\`, which should be a task().
 * Items are dispatched in batches of \`concurrency\` (default: all at once).
 * 
 * @example
 * const process = task(async (item: string) => { ... });
 * const results = await parallel(items, process, { concurrency: 5 });
 */
async parallel<T, R>(items: T[], fn: (item: T) => PromiseLike<R> | R, options?: { concurrency?: number },): Promise<R[]>

/**
 * Commit Kafka offsets for a trigger with auto_commit disabled.
 * @param triggerPath - Path to the Kafka trigger (from event.wm_trigger.trigger_path)
 * @param topic - Kafka topic name (from event.topic)
 * @param partition - Partition number (from event.partition)
 * @param offset - Message offset to commit (from event.offset)
 */
async commitKafkaOffsets(triggerPath: string, topic: string, partition: number, offset: number,): Promise<void>

/**
 * Parse an S3 object from URI string or record format
 * @param s3Object - S3 object as URI string (\`s3://storage/key\`, \`s3:///key\`
 *   for the default storage) or record. Any other string throws rather than
 *   falling back to an auto-generated key: an auto key is requested by
 *   omitting the object, and a fallback would silently misplace the upload
 *   on any typo.
 * @returns S3 object record with storage and s3 key
 */
parseS3Object(s3Object: S3Object): S3ObjectRecord

/**
 * Create a SQL template function for PostgreSQL/datatable queries
 * @param name - Database/datatable name (default: "main")
 * @returns SQL template function for building parameterized queries
 * @example
 * let sql = wmill.datatable()
 * let name = 'Robin'
 * let age = 21
 * await sql\`
 *   SELECT * FROM friends
 *     WHERE name = \${name} AND age = \${age}::int
 * \`.fetch()
 */
datatable(name: string = "main"): DatatableSqlTemplateFunction

/**
 * Create a SQL template function for DuckDB/ducklake queries
 * @param name - DuckDB database name, optionally with a schema as \`name:schema\` (default: "main")
 * @returns SQL template function for building parameterized queries
 * @example
 * let sql = wmill.ducklake()
 * let name = 'Robin'
 * let age = 21
 * await sql\`
 *   SELECT * FROM friends
 *     WHERE name = \${name} AND age = \${age}
 * \`.fetch()
 * @example
 * // Target a specific schema within the ducklake
 * let sql = wmill.ducklake("my_lake:analytics")
 */
ducklake(name: string = "main"): SqlTemplateFunction

/**
 * Idempotently materialize \`selectSql\` into a ducklake table for one
 * partition (or the whole table when \`partition\` is omitted) — the client-side
 * equivalent of the \`// materialize\` engine.
 * With \`uniqueKey\` it upserts the slice (delete-by-key + insert); otherwise it
 * replaces it (whole table → \`CREATE OR REPLACE\`; partition → delete + insert).
 * Safe to re-run for the same partition (backfill / failure-recovery).
 * 
 * Returns a lazy statement — call \`.execute()\` to run it:
 * \`await wmill.upsertPartition({ table, selectSql, partition }).execute()\`.
 */
upsertPartition(opts: DucklakeMaterializeOptions): SqlStatement<any>

/**
 * INSERT-only materialization (no dedup/replace) for append-only tables.
 * Re-running the same partition duplicates rows — use only for immutable
 * event-log sources.
 * 
 * Returns a lazy statement — call \`.execute()\` to run it:
 * \`await wmill.appendPartition({ table, selectSql, partition }).execute()\`.
 */
appendPartition(opts: Omit<DucklakeMaterializeOptions, "uniqueKey">,): SqlStatement<any>
`,
  "write-script-duckdb": `---
name: write-script-duckdb
description: MUST use when writing DuckDB queries.
---

## CLI Commands

Place scripts in a folder.

After writing, tell the user which command fits what they want to do:

- \`wmill script preview <script_path>\` — **default when iterating on a local script.** Runs the local file without deploying.
- \`wmill script run <path>\` — runs the script **already deployed** in the workspace. Use only when the user explicitly wants to test the deployed version, not local edits.
- \`wmill generate-metadata\` — regenerate the local \`.script.yaml\` (input schema) and \`.lock\` (resolved dependencies) for scripts you changed, and refresh their content hashes in \`wmill-lock.yaml\`. Local files only — **not** a deploy. See "Keep metadata in sync" below.
- 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 script", "try it", "test it", "does it work" while there are **local edits to the script file**, use \`script preview\`. Do NOT push the script to then \`script run\` it — pushing is a deploy, and deploying just to test overwrites the workspace version with untested changes.

Only use \`script run\` when:
- The user explicitly says "run the deployed version" / "run what's on the server".
- There is no local script being edited (you're just invoking an existing script).

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.

### Keep metadata in sync after editing

\`wmill-lock.yaml\` tracks a content hash for each item. Editing a script's content — most importantly **adding or removing an import** or **changing \`main\`'s arguments** — invalidates that hash and leaves the \`.lock\`, the \`.script.yaml\` input schema, and the hash row out of date. Run \`wmill generate-metadata\` (scoped to what you touched) after such edits so the resolved lock, the auto-generated args UI (driven by \`.script.yaml\`), and \`wmill-lock.yaml\` all match the code. Leaving them stale produces spurious diffs in git-sync and CI.

This only writes local files (it is **not** a deploy), but it re-resolves dependencies, so it can bump unpinned versions (the same as deploying from the UI; expected, not a bug). So by default offer it and run it once the user agrees, rather than running it silently after every edit — unless the project's \`AGENTS.md\` opts into running metadata automatically (see the "Keeping metadata in sync" preference there). Either way YOU run the command, not the user. After running it, diff the regenerated \`.lock\` / \`.script.lock\` files and tell the user which dependency versions changed (e.g. \`requests 2.31.0 → 2.32.0\`), so they can catch an unwanted bump before deploying — even under \`Metadata: auto\`, since it's information, not a confirmation gate. Pin versions in code to keep them fixed.

With no path argument, \`generate-metadata\` regenerates only the items whose content hash drifted — not everything. Imports propagate: editing a script that others import marks every importer stale too, so a one-line change to a shared module can regenerate many locks (by design — their locks must reflect the imported code). If it touches more than you expect, run \`wmill generate-metadata --dry-run\` — it lists each stale item with a reason (\`content changed\` or \`depends on <path>\`) without changing anything — then narrow with a path argument (\`wmill generate-metadata f/foo\`) or \`--strict-folder-boundaries\`.

If the on-disk \`.lock\` and \`.script.yaml\` are already correct and only \`wmill-lock.yaml\` needs its hashes refreshed (hash drift, or bootstrapping missing entries), use \`wmill generate-metadata rehash\` — it re-records hashes from disk with no backend round-trip and no dependency changes.

### After writing — offer to test, don't wait passively

If the user hasn't already told you to run/test/preview the script, offer it as a one-sentence next step (e.g. "Want me to run \`wmill script preview\` with sample args?"). Do not present a multi-option menu.

If the user already asked to test/run/try the script in their original request, skip the offer and just execute \`wmill script preview <path> -d '<args>'\` directly — pick plausible args from the script's declared parameters. The shape varies by language: \`main(...)\` for code languages, the SQL dialect's own placeholder syntax (\`$1\` for PostgreSQL, \`?\` for MySQL/Snowflake, \`@P1\` for MSSQL, \`@name\` for BigQuery, etc.), positional \`$1\`, \`$2\`, … for Bash, \`param(...)\` for PowerShell.

\`wmill script preview\` does not deploy, but it still executes script code and may cause side effects; run it yourself when the user asked to test/preview (or after confirming that execution is intended). \`wmill generate-metadata\` does not deploy either — it only writes local files (locks, schemas, hashes) — but offer it before running (or run automatically if the project's \`AGENTS.md\` opts in), per "Keep metadata in sync" above. Deploying to the workspace (\`git push\` or \`wmill sync push\` depending on how the repo is wired — see the **Deploying** section) is the only step that mutates remote state — do it only when the user explicitly asks to deploy/publish/push.

For a **visual** open-the-script-in-the-dev-page preview (rather than \`script preview\`'s run-and-print-result), use the \`preview\` skill.

Use \`wmill resource-type list --schema\` to discover available resource types.

# DuckDB

Arguments are defined with comments and used with \`$name\` syntax:

\`\`\`sql
-- $name (text) = default
-- $age (integer)
SELECT * FROM users WHERE name = $name AND age > $age;
\`\`\`

## Ducklake Integration

Attach Ducklake for data lake operations:

\`\`\`sql
-- Main ducklake
ATTACH 'ducklake' AS dl;

-- Named ducklake
ATTACH 'ducklake://my_lake' AS dl;

-- Then query
SELECT * FROM dl.schema.table;
\`\`\`

## External Database Connections

Connect to external databases using resources:

\`\`\`sql
ATTACH '$res:path/to/resource' AS db (TYPE postgres);
SELECT * FROM db.schema.table;
\`\`\`

## S3 File Operations

Read files from S3 storage:

\`\`\`sql
-- Default storage
SELECT * FROM read_csv('s3:///path/to/file.csv');

-- Named storage
SELECT * FROM read_csv('s3://storage_name/path/to/file.csv');

-- Parquet files
SELECT * FROM read_parquet('s3:///path/to/file.parquet');

-- JSON files
SELECT * FROM read_json('s3:///path/to/file.json');
\`\`\`

### Receiving an S3Object as a script parameter

Declare the arg with type \`(s3object)\`. Windmill renders an S3 file picker for it
and binds the arg as the bare \`s3://storage/key\` URI, which DuckDB's reader
functions consume directly:

\`\`\`sql
-- $file (s3object)
SELECT * FROM read_parquet($file);
\`\`\`

Works with any DuckDB reader: \`read_csv($file)\`, \`read_json($file)\`, etc.

### Writing query results to S3

DuckDB writes to S3 natively via \`COPY ... TO\`:

\`\`\`sql
COPY (SELECT * FROM users) TO 's3:///exports/users.parquet' (FORMAT PARQUET);
\`\`\`

Use this instead of the \`-- s3\` streaming directive supported by the other SQL
dialects — that directive is not available in DuckDB.
`,
  "write-script-go": `---
name: write-script-go
description: MUST use when writing Go scripts.
---

## CLI Commands

Place scripts in a folder.

After writing, tell the user which command fits what they want to do:

- \`wmill script preview <script_path>\` — **default when iterating on a local script.** Runs the local file without deploying.
- \`wmill script run <path>\` — runs the script **already deployed** in the workspace. Use only when the user explicitly wants to test the deployed version, not local edits.
- \`wmill generate-metadata\` — regenerate the local \`.script.yaml\` (input schema) and \`.lock\` (resolved dependencies) for scripts you changed, and refresh their content hashes in \`wmill-lock.yaml\`. Local files only — **not** a deploy. See "Keep metadata in sync" below.
- 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 script", "try it", "test it", "does it work" while there are **local edits to the script file**, use \`script preview\`. Do NOT push the script to then \`script run\` it — pushing is a deploy, and deploying just to test overwrites the workspace version with untested changes.

Only use \`script run\` when:
- The user explicitly says "run the deployed version" / "run what's on the server".
- There is no local script being edited (you're just invoking an existing script).

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.

### Keep metadata in sync after editing

\`wmill-lock.yaml\` tracks a content hash for each item. Editing a script's content — most importantly **adding or removing an import** or **changing \`main\`'s arguments** — invalidates that hash and leaves the \`.lock\`, the \`.script.yaml\` input schema, and the hash row out of date. Run \`wmill generate-metadata\` (scoped to what you touched) after such edits so the resolved lock, the auto-generated args UI (driven by \`.script.yaml\`), and \`wmill-lock.yaml\` all match the code. Leaving them stale produces spurious diffs in git-sync and CI.

This only writes local files (it is **not** a deploy), but it re-resolves dependencies, so it can bump unpinned versions (the same as deploying from the UI; expected, not a bug). So by default offer it and run it once the user agrees, rather than running it silently after every edit — unless the project's \`AGENTS.md\` opts into running metadata automatically (see the "Keeping metadata in sync" preference there). Either way YOU run the command, not the user. After running it, diff the regenerated \`.lock\` / \`.script.lock\` files and tell the user which dependency versions changed (e.g. \`requests 2.31.0 → 2.32.0\`), so they can catch an unwanted bump before deploying — even under \`Metadata: auto\`, since it's information, not a confirmation gate. Pin versions in code to keep them fixed.

With no path argument, \`generate-metadata\` regenerates only the items whose content hash drifted — not everything. Imports propagate: editing a script that others import marks every importer stale too, so a one-line change to a shared module can regenerate many locks (by design — their locks must reflect the imported code). If it touches more than you expect, run \`wmill generate-metadata --dry-run\` — it lists each stale item with a reason (\`content changed\` or \`depends on <path>\`) without changing anything — then narrow with a path argument (\`wmill generate-metadata f/foo\`) or \`--strict-folder-boundaries\`.

If the on-disk \`.lock\` and \`.script.yaml\` are already correct and only \`wmill-lock.yaml\` needs its hashes refreshed (hash drift, or bootstrapping missing entries), use \`wmill generate-metadata rehash\` — it re-records hashes from disk with no backend round-trip and no dependency changes.

### After writing — offer to test, don't wait passively

If the user hasn't already told you to run/test/preview the script, offer it as a one-sentence next step (e.g. "Want me to run \`wmill script preview\` with sample args?"). Do not present a multi-option menu.

If the user already asked to test/run/try the script in their original request, skip the offer and just execute \`wmill script preview <path> -d '<args>'\` directly — pick plausible args from the script's declared parameters. The shape varies by language: \`main(...)\` for code languages, the SQL dialect's own placeholder syntax (\`$1\` for PostgreSQL, \`?\` for MySQL/Snowflake, \`@P1\` for MSSQL, \`@name\` for BigQuery, etc.), positional \`$1\`, \`$2\`, … for Bash, \`param(...)\` for PowerShell.

\`wmill script preview\` does not deploy, but it still executes script code and may cause side effects; run it yourself when the user asked to test/preview (or after confirming that execution is intended). \`wmill generate-metadata\` does not deploy either — it only writes local files (locks, schemas, hashes) — but offer it before running (or run automatically if the project's \`AGENTS.md\` opts in), per "Keep metadata in sync" above. Deploying to the workspace (\`git push\` or \`wmill sync push\` depending on how the repo is wired — see the **Deploying** section) is the only step that mutates remote state — do it only when the user explicitly asks to deploy/publish/push.

For a **visual** open-the-script-in-the-dev-page preview (rather than \`script preview\`'s run-and-print-result), use the \`preview\` skill.

Use \`wmill resource-type list --schema\` to discover available resource types.

# Go

## Structure

The file package must be \`inner\` and export a function called \`main\`:

\`\`\`go
package inner

func main(param1 string, param2 int) (map[string]interface{}, error) {
    return map[string]interface{}{
        "result": param1,
        "count":  param2,
    }, nil
}
\`\`\`

**Important:**
- Package must be \`inner\`
- Return type must be \`({return_type}, error)\`
- Function name is \`main\` (lowercase)

## Return Types

The return type can be any Go type that can be serialized to JSON:

\`\`\`go
package inner

type Result struct {
    Name  string \`json:"name"\`
    Count int    \`json:"count"\`
}

func main(name string, count int) (Result, error) {
    return Result{
        Name:  name,
        Count: count,
    }, nil
}
\`\`\`

## Error Handling

Return errors as the second return value:

\`\`\`go
package inner

import "errors"

func main(value int) (string, error) {
    if value < 0 {
        return "", errors.New("value must be positive")
    }
    return "success", nil
}
\`\`\`
`,
  "write-script-graphql": `---
name: write-script-graphql
description: MUST use when writing GraphQL queries.
---

## CLI Commands

Place scripts in a folder.

After writing, tell the user which command fits what they want to do:

- \`wmill script preview <script_path>\` — **default when iterating on a local script.** Runs the local file without deploying.
- \`wmill script run <path>\` — runs the script **already deployed** in the workspace. Use only when the user explicitly wants to test the deployed version, not local edits.
- \`wmill generate-metadata\` — regenerate the local \`.script.yaml\` (input schema) and \`.lock\` (resolved dependencies) for scripts you changed, and refresh their content hashes in \`wmill-lock.yaml\`. Local files only — **not** a deploy. See "Keep metadata in sync" below.
- 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 script", "try it", "test it", "does it work" while there are **local edits to the script file**, use \`script preview\`. Do NOT push the script to then \`script run\` it — pushing is a deploy, and deploying just to test overwrites the workspace version with untested changes.

Only use \`script run\` when:
- The user explicitly says "run the deployed version" / "run what's on the server".
- There is no local script being edited (you're just invoking an existing script).

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.

### Keep metadata in sync after editing

\`wmill-lock.yaml\` tracks a content hash for each item. Editing a script's content — most importantly **adding or removing an import** or **changing \`main\`'s arguments** — invalidates that hash and leaves the \`.lock\`, the \`.script.yaml\` input schema, and the hash row out of date. Run \`wmill generate-metadata\` (scoped to what you touched) after such edits so the resolved lock, the auto-generated args UI (driven by \`.script.yaml\`), and \`wmill-lock.yaml\` all match the code. Leaving them stale produces spurious diffs in git-sync and CI.

This only writes local files (it is **not** a deploy), but it re-resolves dependencies, so it can bump unpinned versions (the same as deploying from the UI; expected, not a bug). So by default offer it and run it once the user agrees, rather than running it silently after every edit — unless the project's \`AGENTS.md\` opts into running metadata automatically (see the "Keeping metadata in sync" preference there). Either way YOU run the command, not the user. After running it, diff the regenerated \`.lock\` / \`.script.lock\` files and tell the user which dependency versions changed (e.g. \`requests 2.31.0 → 2.32.0\`), so they can catch an unwanted bump before deploying — even under \`Metadata: auto\`, since it's information, not a confirmation gate. Pin versions in code to keep them fixed.

With no path argument, \`generate-metadata\` regenerates only the items whose content hash drifted — not everything. Imports propagate: editing a script that others import marks every importer stale too, so a one-line change to a shared module can regenerate many locks (by design — their locks must reflect the imported code). If it touches more than you expect, run \`wmill generate-metadata --dry-run\` — it lists each stale item with a reason (\`content changed\` or \`depends on <path>\`) without changing anything — then narrow with a path argument (\`wmill generate-metadata f/foo\`) or \`--strict-folder-boundaries\`.

If the on-disk \`.lock\` and \`.script.yaml\` are already correct and only \`wmill-lock.yaml\` needs its hashes refreshed (hash drift, or bootstrapping missing entries), use \`wmill generate-metadata rehash\` — it re-records hashes from disk with no backend round-trip and no dependency changes.

### After writing — offer to test, don't wait passively

If the user hasn't already told you to run/test/preview the script, offer it as a one-sentence next step (e.g. "Want me to run \`wmill script preview\` with sample args?"). Do not present a multi-option menu.

If the user already asked to test/run/try the script in their original request, skip the offer and just execute \`wmill script preview <path> -d '<args>'\` directly — pick plausible args from the script's declared parameters. The shape varies by language: \`main(...)\` for code languages, the SQL dialect's own placeholder syntax (\`$1\` for PostgreSQL, \`?\` for MySQL/Snowflake, \`@P1\` for MSSQL, \`@name\` for BigQuery, etc.), positional \`$1\`, \`$2\`, … for Bash, \`param(...)\` for PowerShell.

\`wmill script preview\` does not deploy, but it still executes script code and may cause side effects; run it yourself when the user asked to test/preview (or after confirming that execution is intended). \`wmill generate-metadata\` does not deploy either — it only writes local files (locks, schemas, hashes) — but offer it before running (or run automatically if the project's \`AGENTS.md\` opts in), per "Keep metadata in sync" above. Deploying to the workspace (\`git push\` or \`wmill sync push\` depending on how the repo is wired — see the **Deploying** section) is the only step that mutates remote state — do it only when the user explicitly asks to deploy/publish/push.

For a **visual** open-the-script-in-the-dev-page preview (rather than \`script preview\`'s run-and-print-result), use the \`preview\` skill.

Use \`wmill resource-type list --schema\` to discover available resource types.

# GraphQL

## Structure

Write GraphQL queries or mutations. Arguments can be added as query parameters:

\`\`\`graphql
query GetUser($id: ID!) {
  user(id: $id) {
    id
    name
    email
  }
}
\`\`\`

## Variables

Variables are passed as script arguments and automatically bound to the query:

\`\`\`graphql
query SearchProducts($query: String!, $limit: Int = 10) {
  products(search: $query, first: $limit) {
    edges {
      node {
        id
        name
        price
      }
    }
  }
}
\`\`\`

## Mutations

\`\`\`graphql
mutation CreateUser($input: CreateUserInput!) {
  createUser(input: $input) {
    id
    name
    createdAt
  }
}
\`\`\`
`,
  "write-script-java": `---
name: write-script-java
description: MUST use when writing Java scripts.
---

## CLI Commands

Place scripts in a folder.

After writing, tell the user which command fits what they want to do:

- \`wmill script preview <script_path>\` — **default when iterating on a local script.** Runs the local file without deploying.
- \`wmill script run <path>\` — runs the script **already deployed** in the workspace. Use only when the user explicitly wants to test the deployed version, not local edits.
- \`wmill generate-metadata\` — regenerate the local \`.script.yaml\` (input schema) and \`.lock\` (resolved dependencies) for scripts you changed, and refresh their content hashes in \`wmill-lock.yaml\`. Local files only — **not** a deploy. See "Keep metadata in sync" below.
- 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 script", "try it", "test it", "does it work" while there are **local edits to the script file**, use \`script preview\`. Do NOT push the script to then \`script run\` it — pushing is a deploy, and deploying just to test overwrites the workspace version with untested changes.

Only use \`script run\` when:
- The user explicitly says "run the deployed version" / "run what's on the server".
- There is no local script being edited (you're just invoking an existing script).

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.

### Keep metadata in sync after editing

\`wmill-lock.yaml\` tracks a content hash for each item. Editing a script's content — most importantly **adding or removing an import** or **changing \`main\`'s arguments** — invalidates that hash and leaves the \`.lock\`, the \`.script.yaml\` input schema, and the hash row out of date. Run \`wmill generate-metadata\` (scoped to what you touched) after such edits so the resolved lock, the auto-generated args UI (driven by \`.script.yaml\`), and \`wmill-lock.yaml\` all match the code. Leaving them stale produces spurious diffs in git-sync and CI.

This only writes local files (it is **not** a deploy), but it re-resolves dependencies, so it can bump unpinned versions (the same as deploying from the UI; expected, not a bug). So by default offer it and run it once the user agrees, rather than running it silently after every edit — unless the project's \`AGENTS.md\` opts into running metadata automatically (see the "Keeping metadata in sync" preference there). Either way YOU run the command, not the user. After running it, diff the regenerated \`.lock\` / \`.script.lock\` files and tell the user which dependency versions changed (e.g. \`requests 2.31.0 → 2.32.0\`), so they can catch an unwanted bump before deploying — even under \`Metadata: auto\`, since it's information, not a confirmation gate. Pin versions in code to keep them fixed.

With no path argument, \`generate-metadata\` regenerates only the items whose content hash drifted — not everything. Imports propagate: editing a script that others import marks every importer stale too, so a one-line change to a shared module can regenerate many locks (by design — their locks must reflect the imported code). If it touches more than you expect, run \`wmill generate-metadata --dry-run\` — it lists each stale item with a reason (\`content changed\` or \`depends on <path>\`) without changing anything — then narrow with a path argument (\`wmill generate-metadata f/foo\`) or \`--strict-folder-boundaries\`.

If the on-disk \`.lock\` and \`.script.yaml\` are already correct and only \`wmill-lock.yaml\` needs its hashes refreshed (hash drift, or bootstrapping missing entries), use \`wmill generate-metadata rehash\` — it re-records hashes from disk with no backend round-trip and no dependency changes.

### After writing — offer to test, don't wait passively

If the user hasn't already told you to run/test/preview the script, offer it as a one-sentence next step (e.g. "Want me to run \`wmill script preview\` with sample args?"). Do not present a multi-option menu.

If the user already asked to test/run/try the script in their original request, skip the offer and just execute \`wmill script preview <path> -d '<args>'\` directly — pick plausible args from the script's declared parameters. The shape varies by language: \`main(...)\` for code languages, the SQL dialect's own placeholder syntax (\`$1\` for PostgreSQL, \`?\` for MySQL/Snowflake, \`@P1\` for MSSQL, \`@name\` for BigQuery, etc.), positional \`$1\`, \`$2\`, … for Bash, \`param(...)\` for PowerShell.

\`wmill script preview\` does not deploy, but it still executes script code and may cause side effects; run it yourself when the user asked to test/preview (or after confirming that execution is intended). \`wmill generate-metadata\` does not deploy either — it only writes local files (locks, schemas, hashes) — but offer it before running (or run automatically if the project's \`AGENTS.md\` opts in), per "Keep metadata in sync" above. Deploying to the workspace (\`git push\` or \`wmill sync push\` depending on how the repo is wired — see the **Deploying** section) is the only step that mutates remote state — do it only when the user explicitly asks to deploy/publish/push.

For a **visual** open-the-script-in-the-dev-page preview (rather than \`script preview\`'s run-and-print-result), use the \`preview\` skill.

Use \`wmill resource-type list --schema\` to discover available resource types.

# Java

The script must contain a Main public class with a \`public static main()\` method:

\`\`\`java
public class Main {
    public static Object main(String name, int count) {
        java.util.Map<String, Object> result = new java.util.HashMap<>();
        result.put("name", name);
        result.put("count", count);
        return result;
    }
}
\`\`\`

**Important:**
- Class must be named \`Main\`
- Method must be \`public static Object main(...)\`
- Return type is \`Object\` or \`void\`

## Maven Dependencies

Add dependencies using comments at the top:

\`\`\`java
//requirements:
//com.google.code.gson:gson:2.10.1
//org.apache.httpcomponents:httpclient:4.5.14

import com.google.gson.Gson;

public class Main {
    public static Object main(String input) {
        Gson gson = new Gson();
        return gson.fromJson(input, Object.class);
    }
}
\`\`\`
`,
  "write-script-mssql": `---
name: write-script-mssql
description: MUST use when writing MS SQL Server queries.
---

## CLI Commands

Place scripts in a folder.

After writing, tell the user which command fits what they want to do:

- \`wmill script preview <script_path>\` — **default when iterating on a local script.** Runs the local file without deploying.
- \`wmill script run <path>\` — runs the script **already deployed** in the workspace. Use only when the user explicitly wants to test the deployed version, not local edits.
- \`wmill generate-metadata\` — regenerate the local \`.script.yaml\` (input schema) and \`.lock\` (resolved dependencies) for scripts you changed, and refresh their content hashes in \`wmill-lock.yaml\`. Local files only — **not** a deploy. See "Keep metadata in sync" below.
- 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 script", "try it", "test it", "does it work" while there are **local edits to the script file**, use \`script preview\`. Do NOT push the script to then \`script run\` it — pushing is a deploy, and deploying just to test overwrites the workspace version with untested changes.

Only use \`script run\` when:
- The user explicitly says "run the deployed version" / "run what's on the server".
- There is no local script being edited (you're just invoking an existing script).

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.

### Keep metadata in sync after editing

\`wmill-lock.yaml\` tracks a content hash for each item. Editing a script's content — most importantly **adding or removing an import** or **changing \`main\`'s arguments** — invalidates that hash and leaves the \`.lock\`, the \`.script.yaml\` input schema, and the hash row out of date. Run \`wmill generate-metadata\` (scoped to what you touched) after such edits so the resolved lock, the auto-generated args UI (driven by \`.script.yaml\`), and \`wmill-lock.yaml\` all match the code. Leaving them stale produces spurious diffs in git-sync and CI.

This only writes local files (it is **not** a deploy), but it re-resolves dependencies, so it can bump unpinned versions (the same as deploying from the UI; expected, not a bug). So by default offer it and run it once the user agrees, rather than running it silently after every edit — unless the project's \`AGENTS.md\` opts into running metadata automatically (see the "Keeping metadata in sync" preference there). Either way YOU run the command, not the user. After running it, diff the regenerated \`.lock\` / \`.script.lock\` files and tell the user which dependency versions changed (e.g. \`requests 2.31.0 → 2.32.0\`), so they can catch an unwanted bump before deploying — even under \`Metadata: auto\`, since it's information, not a confirmation gate. Pin versions in code to keep them fixed.

With no path argument, \`generate-metadata\` regenerates only the items whose content hash drifted — not everything. Imports propagate: editing a script that others import marks every importer stale too, so a one-line change to a shared module can regenerate many locks (by design — their locks must reflect the imported code). If it touches more than you expect, run \`wmill generate-metadata --dry-run\` — it lists each stale item with a reason (\`content changed\` or \`depends on <path>\`) without changing anything — then narrow with a path argument (\`wmill generate-metadata f/foo\`) or \`--strict-folder-boundaries\`.

If the on-disk \`.lock\` and \`.script.yaml\` are already correct and only \`wmill-lock.yaml\` needs its hashes refreshed (hash drift, or bootstrapping missing entries), use \`wmill generate-metadata rehash\` — it re-records hashes from disk with no backend round-trip and no dependency changes.

### After writing — offer to test, don't wait passively

If the user hasn't already told you to run/test/preview the script, offer it as a one-sentence next step (e.g. "Want me to run \`wmill script preview\` with sample args?"). Do not present a multi-option menu.

If the user already asked to test/run/try the script in their original request, skip the offer and just execute \`wmill script preview <path> -d '<args>'\` directly — pick plausible args from the script's declared parameters. The shape varies by language: \`main(...)\` for code languages, the SQL dialect's own placeholder syntax (\`$1\` for PostgreSQL, \`?\` for MySQL/Snowflake, \`@P1\` for MSSQL, \`@name\` for BigQuery, etc.), positional \`$1\`, \`$2\`, … for Bash, \`param(...)\` for PowerShell.

\`wmill script preview\` does not deploy, but it still executes script code and may cause side effects; run it yourself when the user asked to test/preview (or after confirming that execution is intended). \`wmill generate-metadata\` does not deploy either — it only writes local files (locks, schemas, hashes) — but offer it before running (or run automatically if the project's \`AGENTS.md\` opts in), per "Keep metadata in sync" above. Deploying to the workspace (\`git push\` or \`wmill sync push\` depending on how the repo is wired — see the **Deploying** section) is the only step that mutates remote state — do it only when the user explicitly asks to deploy/publish/push.

For a **visual** open-the-script-in-the-dev-page preview (rather than \`script preview\`'s run-and-print-result), use the \`preview\` skill.

Use \`wmill resource-type list --schema\` to discover available resource types.

# Microsoft SQL Server (MSSQL)

Arguments use \`@P1\`, \`@P2\`, etc.

Name the parameters by adding comments before the statement:

\`\`\`sql
-- @P1 name1 (varchar)
-- @P2 name2 (int) = 0
SELECT * FROM users WHERE name = @P1 AND age > @P2;
\`\`\`

## Receiving an S3Object as a script parameter

Declare the arg with type \`(s3object)\`. Windmill renders an S3 file picker for
it, downloads the file, and binds it as \`nvarchar(max)\` JSON text — Parquet/CSV
files are decoded server-side into a JSON array of records, JSON/JSONL pass
through. Consume with \`OPENJSON\`:

\`\`\`sql
-- @P1 file (s3object)
SELECT id, name
FROM OPENJSON(@P1)
WITH (id INT, name NVARCHAR(200));
\`\`\`

## Streaming query results to S3

Add a \`-- s3\` directive at the top of the script to stream the result set to S3
instead of returning rows. Windmill writes the file and returns its \`S3Object\`
as the script result.

\`\`\`sql
-- s3 prefix=exports/users format=parquet
SELECT id, name FROM users;
\`\`\`

All keys are optional: \`prefix\` (object key prefix), \`storage\` (named storage —
omit to use the workspace default), \`format\` (\`json\` (default), \`parquet\`, or
\`csv\`). Use this for large result sets — rows stream directly to S3 instead of
being buffered as the script return value.
`,
  "write-script-mysql": `---
name: write-script-mysql
description: MUST use when writing MySQL queries.
---

## CLI Commands

Place scripts in a folder.

After writing, tell the user which command fits what they want to do:

- \`wmill script preview <script_path>\` — **default when iterating on a local script.** Runs the local file without deploying.
- \`wmill script run <path>\` — runs the script **already deployed** in the workspace. Use only when the user explicitly wants to test the deployed version, not local edits.
- \`wmill generate-metadata\` — regenerate the local \`.script.yaml\` (input schema) and \`.lock\` (resolved dependencies) for scripts you changed, and refresh their content hashes in \`wmill-lock.yaml\`. Local files only — **not** a deploy. See "Keep metadata in sync" below.
- 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 script", "try it", "test it", "does it work" while there are **local edits to the script file**, use \`script preview\`. Do NOT push the script to then \`script run\` it — pushing is a deploy, and deploying just to test overwrites the workspace version with untested changes.

Only use \`script run\` when:
- The user explicitly says "run the deployed version" / "run what's on the server".
- There is no local script being edited (you're just invoking an existing script).

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.

### Keep metadata in sync after editing

\`wmill-lock.yaml\` tracks a content hash for each item. Editing a script's content — most importantly **adding or removing an import** or **changing \`main\`'s arguments** — invalidates that hash and leaves the \`.lock\`, the \`.script.yaml\` input schema, and the hash row out of date. Run \`wmill generate-metadata\` (scoped to what you touched) after such edits so the resolved lock, the auto-generated args UI (driven by \`.script.yaml\`), and \`wmill-lock.yaml\` all match the code. Leaving them stale produces spurious diffs in git-sync and CI.

This only writes local files (it is **not** a deploy), but it re-resolves dependencies, so it can bump unpinned versions (the same as deploying from the UI; expected, not a bug). So by default offer it and run it once the user agrees, rather than running it silently after every edit — unless the project's \`AGENTS.md\` opts into running metadata automatically (see the "Keeping metadata in sync" preference there). Either way YOU run the command, not the user. After running it, diff the regenerated \`.lock\` / \`.script.lock\` files and tell the user which dependency versions changed (e.g. \`requests 2.31.0 → 2.32.0\`), so they can catch an unwanted bump before deploying — even under \`Metadata: auto\`, since it's information, not a confirmation gate. Pin versions in code to keep them fixed.

With no path argument, \`generate-metadata\` regenerates only the items whose content hash drifted — not everything. Imports propagate: editing a script that others import marks every importer stale too, so a one-line change to a shared module can regenerate many locks (by design — their locks must reflect the imported code). If it touches more than you expect, run \`wmill generate-metadata --dry-run\` — it lists each stale item with a reason (\`content changed\` or \`depends on <path>\`) without changing anything — then narrow with a path argument (\`wmill generate-metadata f/foo\`) or \`--strict-folder-boundaries\`.

If the on-disk \`.lock\` and \`.script.yaml\` are already correct and only \`wmill-lock.yaml\` needs its hashes refreshed (hash drift, or bootstrapping missing entries), use \`wmill generate-metadata rehash\` — it re-records hashes from disk with no backend round-trip and no dependency changes.

### After writing — offer to test, don't wait passively

If the user hasn't already told you to run/test/preview the script, offer it as a one-sentence next step (e.g. "Want me to run \`wmill script preview\` with sample args?"). Do not present a multi-option menu.

If the user already asked to test/run/try the script in their original request, skip the offer and just execute \`wmill script preview <path> -d '<args>'\` directly — pick plausible args from the script's declared parameters. The shape varies by language: \`main(...)\` for code languages, the SQL dialect's own placeholder syntax (\`$1\` for PostgreSQL, \`?\` for MySQL/Snowflake, \`@P1\` for MSSQL, \`@name\` for BigQuery, etc.), positional \`$1\`, \`$2\`, … for Bash, \`param(...)\` for PowerShell.

\`wmill script preview\` does not deploy, but it still executes script code and may cause side effects; run it yourself when the user asked to test/preview (or after confirming that execution is intended). \`wmill generate-metadata\` does not deploy either — it only writes local files (locks, schemas, hashes) — but offer it before running (or run automatically if the project's \`AGENTS.md\` opts in), per "Keep metadata in sync" above. Deploying to the workspace (\`git push\` or \`wmill sync push\` depending on how the repo is wired — see the **Deploying** section) is the only step that mutates remote state — do it only when the user explicitly asks to deploy/publish/push.

For a **visual** open-the-script-in-the-dev-page preview (rather than \`script preview\`'s run-and-print-result), use the \`preview\` skill.

Use \`wmill resource-type list --schema\` to discover available resource types.

# MySQL

Arguments use \`?\` placeholders.

Name the parameters by adding comments before the statement:

\`\`\`sql
-- ? name1 (text)
-- ? name2 (int) = 0
SELECT * FROM users WHERE name = ? AND age > ?;
\`\`\`

## Receiving an S3Object as a script parameter

Declare the arg with type \`(s3object)\`. Windmill renders an S3 file picker for
it, downloads the file, and binds it as JSON text — Parquet/CSV files are
decoded server-side into a JSON array of records, JSON/JSONL pass through.
Consume with \`JSON_TABLE\`:

\`\`\`sql
-- ? file (s3object)
SELECT id, name
FROM JSON_TABLE(?, '$[*]'
  COLUMNS (id INT PATH '$.id', name VARCHAR(200) PATH '$.name')
) AS r;
\`\`\`

## Streaming query results to S3

Add a \`-- s3\` directive at the top of the script to stream the result set to S3
instead of returning rows. Windmill writes the file and returns its \`S3Object\`
as the script result.

\`\`\`sql
-- s3 prefix=exports/users format=parquet
SELECT id, name FROM users;
\`\`\`

All keys are optional: \`prefix\` (object key prefix), \`storage\` (named storage —
omit to use the workspace default), \`format\` (\`json\` (default), \`parquet\`, or
\`csv\`). Use this for large result sets — rows stream directly to S3 instead of
being buffered as the script return value.
`,
  "write-script-php": `---
name: write-script-php
description: MUST use when writing PHP scripts.
---

## CLI Commands

Place scripts in a folder.

After writing, tell the user which command fits what they want to do:

- \`wmill script preview <script_path>\` — **default when iterating on a local script.** Runs the local file without deploying.
- \`wmill script run <path>\` — runs the script **already deployed** in the workspace. Use only when the user explicitly wants to test the deployed version, not local edits.
- \`wmill generate-metadata\` — regenerate the local \`.script.yaml\` (input schema) and \`.lock\` (resolved dependencies) for scripts you changed, and refresh their content hashes in \`wmill-lock.yaml\`. Local files only — **not** a deploy. See "Keep metadata in sync" below.
- 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 script", "try it", "test it", "does it work" while there are **local edits to the script file**, use \`script preview\`. Do NOT push the script to then \`script run\` it — pushing is a deploy, and deploying just to test overwrites the workspace version with untested changes.

Only use \`script run\` when:
- The user explicitly says "run the deployed version" / "run what's on the server".
- There is no local script being edited (you're just invoking an existing script).

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.

### Keep metadata in sync after editing

\`wmill-lock.yaml\` tracks a content hash for each item. Editing a script's content — most importantly **adding or removing an import** or **changing \`main\`'s arguments** — invalidates that hash and leaves the \`.lock\`, the \`.script.yaml\` input schema, and the hash row out of date. Run \`wmill generate-metadata\` (scoped to what you touched) after such edits so the resolved lock, the auto-generated args UI (driven by \`.script.yaml\`), and \`wmill-lock.yaml\` all match the code. Leaving them stale produces spurious diffs in git-sync and CI.

This only writes local files (it is **not** a deploy), but it re-resolves dependencies, so it can bump unpinned versions (the same as deploying from the UI; expected, not a bug). So by default offer it and run it once the user agrees, rather than running it silently after every edit — unless the project's \`AGENTS.md\` opts into running metadata automatically (see the "Keeping metadata in sync" preference there). Either way YOU run the command, not the user. After running it, diff the regenerated \`.lock\` / \`.script.lock\` files and tell the user which dependency versions changed (e.g. \`requests 2.31.0 → 2.32.0\`), so they can catch an unwanted bump before deploying — even under \`Metadata: auto\`, since it's information, not a confirmation gate. Pin versions in code to keep them fixed.

With no path argument, \`generate-metadata\` regenerates only the items whose content hash drifted — not everything. Imports propagate: editing a script that others import marks every importer stale too, so a one-line change to a shared module can regenerate many locks (by design — their locks must reflect the imported code). If it touches more than you expect, run \`wmill generate-metadata --dry-run\` — it lists each stale item with a reason (\`content changed\` or \`depends on <path>\`) without changing anything — then narrow with a path argument (\`wmill generate-metadata f/foo\`) or \`--strict-folder-boundaries\`.

If the on-disk \`.lock\` and \`.script.yaml\` are already correct and only \`wmill-lock.yaml\` needs its hashes refreshed (hash drift, or bootstrapping missing entries), use \`wmill generate-metadata rehash\` — it re-records hashes from disk with no backend round-trip and no dependency changes.

### After writing — offer to test, don't wait passively

If the user hasn't already told you to run/test/preview the script, offer it as a one-sentence next step (e.g. "Want me to run \`wmill script preview\` with sample args?"). Do not present a multi-option menu.

If the user already asked to test/run/try the script in their original request, skip the offer and just execute \`wmill script preview <path> -d '<args>'\` directly — pick plausible args from the script's declared parameters. The shape varies by language: \`main(...)\` for code languages, the SQL dialect's own placeholder syntax (\`$1\` for PostgreSQL, \`?\` for MySQL/Snowflake, \`@P1\` for MSSQL, \`@name\` for BigQuery, etc.), positional \`$1\`, \`$2\`, … for Bash, \`param(...)\` for PowerShell.

\`wmill script preview\` does not deploy, but it still executes script code and may cause side effects; run it yourself when the user asked to test/preview (or after confirming that execution is intended). \`wmill generate-metadata\` does not deploy either — it only writes local files (locks, schemas, hashes) — but offer it before running (or run automatically if the project's \`AGENTS.md\` opts in), per "Keep metadata in sync" above. Deploying to the workspace (\`git push\` or \`wmill sync push\` depending on how the repo is wired — see the **Deploying** section) is the only step that mutates remote state — do it only when the user explicitly asks to deploy/publish/push.

For a **visual** open-the-script-in-the-dev-page preview (rather than \`script preview\`'s run-and-print-result), use the \`preview\` skill.

Use \`wmill resource-type list --schema\` to discover available resource types.

# PHP

## Structure

The script must start with \`<?php\` and contain at least one function called \`main\`:

\`\`\`php
<?php

function main(string $param1, int $param2) {
    return ["result" => $param1, "count" => $param2];
}
\`\`\`

## Resource Types

On Windmill, credentials and configuration are stored in resources and passed as parameters to main.

You need to **redefine** the type of the resources that are needed before the main function. Always check if the class already exists using \`class_exists\`:

\`\`\`php
<?php

if (!class_exists('Postgresql')) {
    class Postgresql {
        public string $host;
        public int $port;
        public string $user;
        public string $password;
        public string $dbname;
    }
}

function main(Postgresql $db) {
    // $db contains the database connection details
}
\`\`\`

The resource type name has to be exactly as specified.

## Library Dependencies

Specify library dependencies as comments before the main function:

\`\`\`php
<?php

// require:
// guzzlehttp/guzzle
// stripe/stripe-php@^10.0

function main() {
    // Libraries are available
}
\`\`\`

One dependency per line. No need to require autoload, it is already done.
`,
  "write-script-postgresql": `---
name: write-script-postgresql
description: MUST use when writing PostgreSQL queries.
---

## CLI Commands

Place scripts in a folder.

After writing, tell the user which command fits what they want to do:

- \`wmill script preview <script_path>\` — **default when iterating on a local script.** Runs the local file without deploying.
- \`wmill script run <path>\` — runs the script **already deployed** in the workspace. Use only when the user explicitly wants to test the deployed version, not local edits.
- \`wmill generate-metadata\` — regenerate the local \`.script.yaml\` (input schema) and \`.lock\` (resolved dependencies) for scripts you changed, and refresh their content hashes in \`wmill-lock.yaml\`. Local files only — **not** a deploy. See "Keep metadata in sync" below.
- 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 script", "try it", "test it", "does it work" while there are **local edits to the script file**, use \`script preview\`. Do NOT push the script to then \`script run\` it — pushing is a deploy, and deploying just to test overwrites the workspace version with untested changes.

Only use \`script run\` when:
- The user explicitly says "run the deployed version" / "run what's on the server".
- There is no local script being edited (you're just invoking an existing script).

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.

### Keep metadata in sync after editing

\`wmill-lock.yaml\` tracks a content hash for each item. Editing a script's content — most importantly **adding or removing an import** or **changing \`main\`'s arguments** — invalidates that hash and leaves the \`.lock\`, the \`.script.yaml\` input schema, and the hash row out of date. Run \`wmill generate-metadata\` (scoped to what you touched) after such edits so the resolved lock, the auto-generated args UI (driven by \`.script.yaml\`), and \`wmill-lock.yaml\` all match the code. Leaving them stale produces spurious diffs in git-sync and CI.

This only writes local files (it is **not** a deploy), but it re-resolves dependencies, so it can bump unpinned versions (the same as deploying from the UI; expected, not a bug). So by default offer it and run it once the user agrees, rather than running it silently after every edit — unless the project's \`AGENTS.md\` opts into running metadata automatically (see the "Keeping metadata in sync" preference there). Either way YOU run the command, not the user. After running it, diff the regenerated \`.lock\` / \`.script.lock\` files and tell the user which dependency versions changed (e.g. \`requests 2.31.0 → 2.32.0\`), so they can catch an unwanted bump before deploying — even under \`Metadata: auto\`, since it's information, not a confirmation gate. Pin versions in code to keep them fixed.

With no path argument, \`generate-metadata\` regenerates only the items whose content hash drifted — not everything. Imports propagate: editing a script that others import marks every importer stale too, so a one-line change to a shared module can regenerate many locks (by design — their locks must reflect the imported code). If it touches more than you expect, run \`wmill generate-metadata --dry-run\` — it lists each stale item with a reason (\`content changed\` or \`depends on <path>\`) without changing anything — then narrow with a path argument (\`wmill generate-metadata f/foo\`) or \`--strict-folder-boundaries\`.

If the on-disk \`.lock\` and \`.script.yaml\` are already correct and only \`wmill-lock.yaml\` needs its hashes refreshed (hash drift, or bootstrapping missing entries), use \`wmill generate-metadata rehash\` — it re-records hashes from disk with no backend round-trip and no dependency changes.

### After writing — offer to test, don't wait passively

If the user hasn't already told you to run/test/preview the script, offer it as a one-sentence next step (e.g. "Want me to run \`wmill script preview\` with sample args?"). Do not present a multi-option menu.

If the user already asked to test/run/try the script in their original request, skip the offer and just execute \`wmill script preview <path> -d '<args>'\` directly — pick plausible args from the script's declared parameters. The shape varies by language: \`main(...)\` for code languages, the SQL dialect's own placeholder syntax (\`$1\` for PostgreSQL, \`?\` for MySQL/Snowflake, \`@P1\` for MSSQL, \`@name\` for BigQuery, etc.), positional \`$1\`, \`$2\`, … for Bash, \`param(...)\` for PowerShell.

\`wmill script preview\` does not deploy, but it still executes script code and may cause side effects; run it yourself when the user asked to test/preview (or after confirming that execution is intended). \`wmill generate-metadata\` does not deploy either — it only writes local files (locks, schemas, hashes) — but offer it before running (or run automatically if the project's \`AGENTS.md\` opts in), per "Keep metadata in sync" above. Deploying to the workspace (\`git push\` or \`wmill sync push\` depending on how the repo is wired — see the **Deploying** section) is the only step that mutates remote state — do it only when the user explicitly asks to deploy/publish/push.

For a **visual** open-the-script-in-the-dev-page preview (rather than \`script preview\`'s run-and-print-result), use the \`preview\` skill.

Use \`wmill resource-type list --schema\` to discover available resource types.

# PostgreSQL

Arguments are obtained directly in the statement with \`$1::{type}\`, \`$2::{type}\`, etc.

Name the parameters by adding comments at the beginning of the script (without specifying the type):

\`\`\`sql
-- $1 name1
-- $2 name2 = default_value
SELECT * FROM users WHERE name = $1::TEXT AND age > $2::INT;
\`\`\`

## Receiving an S3Object as a script parameter

Declare the arg with type \`(s3object)\`. Windmill renders an S3 file picker for
it, downloads the file, and binds it as a \`jsonb\` parameter — Parquet/CSV files
are decoded server-side into a JSON array of records, JSON/JSONL pass through.
Consume with \`jsonb_to_recordset\` (or any \`jsonb\` API):

\`\`\`sql
-- $1 file (s3object)
SELECT *
FROM jsonb_to_recordset($1::jsonb) AS r(id INT, name TEXT);
\`\`\`

## Streaming query results to S3

Add a \`-- s3\` directive at the top of the script to stream the result set to S3
instead of returning rows. Windmill writes the file and returns its \`S3Object\`
as the script result.

\`\`\`sql
-- s3 prefix=exports/users format=parquet
SELECT id, name FROM users;
\`\`\`

All keys are optional: \`prefix\` (object key prefix), \`storage\` (named storage —
omit to use the workspace default), \`format\` (\`json\` (default), \`parquet\`, or
\`csv\`). Use this for large result sets — rows stream directly to S3 instead of
being buffered as the script return value.
`,
  "write-script-powershell": `---
name: write-script-powershell
description: MUST use when writing PowerShell scripts.
---

## CLI Commands

Place scripts in a folder.

After writing, tell the user which command fits what they want to do:

- \`wmill script preview <script_path>\` — **default when iterating on a local script.** Runs the local file without deploying.
- \`wmill script run <path>\` — runs the script **already deployed** in the workspace. Use only when the user explicitly wants to test the deployed version, not local edits.
- \`wmill generate-metadata\` — regenerate the local \`.script.yaml\` (input schema) and \`.lock\` (resolved dependencies) for scripts you changed, and refresh their content hashes in \`wmill-lock.yaml\`. Local files only — **not** a deploy. See "Keep metadata in sync" below.
- 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 script", "try it", "test it", "does it work" while there are **local edits to the script file**, use \`script preview\`. Do NOT push the script to then \`script run\` it — pushing is a deploy, and deploying just to test overwrites the workspace version with untested changes.

Only use \`script run\` when:
- The user explicitly says "run the deployed version" / "run what's on the server".
- There is no local script being edited (you're just invoking an existing script).

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.

### Keep metadata in sync after editing

\`wmill-lock.yaml\` tracks a content hash for each item. Editing a script's content — most importantly **adding or removing an import** or **changing \`main\`'s arguments** — invalidates that hash and leaves the \`.lock\`, the \`.script.yaml\` input schema, and the hash row out of date. Run \`wmill generate-metadata\` (scoped to what you touched) after such edits so the resolved lock, the auto-generated args UI (driven by \`.script.yaml\`), and \`wmill-lock.yaml\` all match the code. Leaving them stale produces spurious diffs in git-sync and CI.

This only writes local files (it is **not** a deploy), but it re-resolves dependencies, so it can bump unpinned versions (the same as deploying from the UI; expected, not a bug). So by default offer it and run it once the user agrees, rather than running it silently after every edit — unless the project's \`AGENTS.md\` opts into running metadata automatically (see the "Keeping metadata in sync" preference there). Either way YOU run the command, not the user. After running it, diff the regenerated \`.lock\` / \`.script.lock\` files and tell the user which dependency versions changed (e.g. \`requests 2.31.0 → 2.32.0\`), so they can catch an unwanted bump before deploying — even under \`Metadata: auto\`, since it's information, not a confirmation gate. Pin versions in code to keep them fixed.

With no path argument, \`generate-metadata\` regenerates only the items whose content hash drifted — not everything. Imports propagate: editing a script that others import marks every importer stale too, so a one-line change to a shared module can regenerate many locks (by design — their locks must reflect the imported code). If it touches more than you expect, run \`wmill generate-metadata --dry-run\` — it lists each stale item with a reason (\`content changed\` or \`depends on <path>\`) without changing anything — then narrow with a path argument (\`wmill generate-metadata f/foo\`) or \`--strict-folder-boundaries\`.

If the on-disk \`.lock\` and \`.script.yaml\` are already correct and only \`wmill-lock.yaml\` needs its hashes refreshed (hash drift, or bootstrapping missing entries), use \`wmill generate-metadata rehash\` — it re-records hashes from disk with no backend round-trip and no dependency changes.

### After writing — offer to test, don't wait passively

If the user hasn't already told you to run/test/preview the script, offer it as a one-sentence next step (e.g. "Want me to run \`wmill script preview\` with sample args?"). Do not present a multi-option menu.

If the user already asked to test/run/try the script in their original request, skip the offer and just execute \`wmill script preview <path> -d '<args>'\` directly — pick plausible args from the script's declared parameters. The shape varies by language: \`main(...)\` for code languages, the SQL dialect's own placeholder syntax (\`$1\` for PostgreSQL, \`?\` for MySQL/Snowflake, \`@P1\` for MSSQL, \`@name\` for BigQuery, etc.), positional \`$1\`, \`$2\`, … for Bash, \`param(...)\` for PowerShell.

\`wmill script preview\` does not deploy, but it still executes script code and may cause side effects; run it yourself when the user asked to test/preview (or after confirming that execution is intended). \`wmill generate-metadata\` does not deploy either — it only writes local files (locks, schemas, hashes) — but offer it before running (or run automatically if the project's \`AGENTS.md\` opts in), per "Keep metadata in sync" above. Deploying to the workspace (\`git push\` or \`wmill sync push\` depending on how the repo is wired — see the **Deploying** section) is the only step that mutates remote state — do it only when the user explicitly asks to deploy/publish/push.

For a **visual** open-the-script-in-the-dev-page preview (rather than \`script preview\`'s run-and-print-result), use the \`preview\` skill.

Use \`wmill resource-type list --schema\` to discover available resource types.

# PowerShell

## Structure

Arguments are obtained by calling the \`param\` function on the first line:

\`\`\`powershell
param($Name, $Count = 0, [int]$Age)

# Your code here
Write-Output "Processing $Name, count: $Count, age: $Age"

# Return object
@{
    name = $Name
    count = $Count
    age = $Age
}
\`\`\`

## Parameter Types

You can specify types for parameters:

\`\`\`powershell
param(
    [string]$Name,
    [int]$Count = 0,
    [bool]$Enabled = $true,
    [array]$Items
)

@{
    name = $Name
    count = $Count
    enabled = $Enabled
    items = $Items
}
\`\`\`

## Return Values

Return values by outputting them at the end of the script:

\`\`\`powershell
param($Input)

$result = @{
    processed = $true
    data = $Input
    timestamp = Get-Date -Format "o"
}

$result
\`\`\`
`,
  "write-script-python3": `---
name: write-script-python3
description: MUST use when writing Python scripts.
---

## CLI Commands

Place scripts in a folder.

After writing, tell the user which command fits what they want to do:

- \`wmill script preview <script_path>\` — **default when iterating on a local script.** Runs the local file without deploying.
- \`wmill script run <path>\` — runs the script **already deployed** in the workspace. Use only when the user explicitly wants to test the deployed version, not local edits.
- \`wmill generate-metadata\` — regenerate the local \`.script.yaml\` (input schema) and \`.lock\` (resolved dependencies) for scripts you changed, and refresh their content hashes in \`wmill-lock.yaml\`. Local files only — **not** a deploy. See "Keep metadata in sync" below.
- 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 script", "try it", "test it", "does it work" while there are **local edits to the script file**, use \`script preview\`. Do NOT push the script to then \`script run\` it — pushing is a deploy, and deploying just to test overwrites the workspace version with untested changes.

Only use \`script run\` when:
- The user explicitly says "run the deployed version" / "run what's on the server".
- There is no local script being edited (you're just invoking an existing script).

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.

### Keep metadata in sync after editing

\`wmill-lock.yaml\` tracks a content hash for each item. Editing a script's content — most importantly **adding or removing an import** or **changing \`main\`'s arguments** — invalidates that hash and leaves the \`.lock\`, the \`.script.yaml\` input schema, and the hash row out of date. Run \`wmill generate-metadata\` (scoped to what you touched) after such edits so the resolved lock, the auto-generated args UI (driven by \`.script.yaml\`), and \`wmill-lock.yaml\` all match the code. Leaving them stale produces spurious diffs in git-sync and CI.

This only writes local files (it is **not** a deploy), but it re-resolves dependencies, so it can bump unpinned versions (the same as deploying from the UI; expected, not a bug). So by default offer it and run it once the user agrees, rather than running it silently after every edit — unless the project's \`AGENTS.md\` opts into running metadata automatically (see the "Keeping metadata in sync" preference there). Either way YOU run the command, not the user. After running it, diff the regenerated \`.lock\` / \`.script.lock\` files and tell the user which dependency versions changed (e.g. \`requests 2.31.0 → 2.32.0\`), so they can catch an unwanted bump before deploying — even under \`Metadata: auto\`, since it's information, not a confirmation gate. Pin versions in code to keep them fixed.

With no path argument, \`generate-metadata\` regenerates only the items whose content hash drifted — not everything. Imports propagate: editing a script that others import marks every importer stale too, so a one-line change to a shared module can regenerate many locks (by design — their locks must reflect the imported code). If it touches more than you expect, run \`wmill generate-metadata --dry-run\` — it lists each stale item with a reason (\`content changed\` or \`depends on <path>\`) without changing anything — then narrow with a path argument (\`wmill generate-metadata f/foo\`) or \`--strict-folder-boundaries\`.

If the on-disk \`.lock\` and \`.script.yaml\` are already correct and only \`wmill-lock.yaml\` needs its hashes refreshed (hash drift, or bootstrapping missing entries), use \`wmill generate-metadata rehash\` — it re-records hashes from disk with no backend round-trip and no dependency changes.

### After writing — offer to test, don't wait passively

If the user hasn't already told you to run/test/preview the script, offer it as a one-sentence next step (e.g. "Want me to run \`wmill script preview\` with sample args?"). Do not present a multi-option menu.

If the user already asked to test/run/try the script in their original request, skip the offer and just execute \`wmill script preview <path> -d '<args>'\` directly — pick plausible args from the script's declared parameters. The shape varies by language: \`main(...)\` for code languages, the SQL dialect's own placeholder syntax (\`$1\` for PostgreSQL, \`?\` for MySQL/Snowflake, \`@P1\` for MSSQL, \`@name\` for BigQuery, etc.), positional \`$1\`, \`$2\`, … for Bash, \`param(...)\` for PowerShell.

\`wmill script preview\` does not deploy, but it still executes script code and may cause side effects; run it yourself when the user asked to test/preview (or after confirming that execution is intended). \`wmill generate-metadata\` does not deploy either — it only writes local files (locks, schemas, hashes) — but offer it before running (or run automatically if the project's \`AGENTS.md\` opts in), per "Keep metadata in sync" above. Deploying to the workspace (\`git push\` or \`wmill sync push\` depending on how the repo is wired — see the **Deploying** section) is the only step that mutates remote state — do it only when the user explicitly asks to deploy/publish/push.

For a **visual** open-the-script-in-the-dev-page preview (rather than \`script preview\`'s run-and-print-result), use the \`preview\` skill.

Use \`wmill resource-type list --schema\` to discover available resource types.

# Python

## Structure

The script must contain at least one function called \`main\`:

\`\`\`python
def main(param1: str, param2: int):
    # Your code here
    return {"result": param1, "count": param2}
\`\`\`

Do not call the main function. Libraries are installed automatically.

## Resource Types

On Windmill, credentials and configuration are stored in resources and passed as parameters to main.

You need to **redefine** the type of the resources that are needed before the main function as TypedDict:

\`\`\`python
from typing import TypedDict

class postgresql(TypedDict):
    host: str
    port: int
    user: str
    password: str
    dbname: str

def main(db: postgresql):
    # db contains the database connection details
    pass
\`\`\`

**Important rules:**

- The resource type name must be **IN LOWERCASE**
- Only include resource types if they are actually needed
- If an import conflicts with a resource type name, **rename the imported object, not the type name**
- Make sure to import TypedDict from typing **if you're using it**

## Imports

Libraries are installed automatically. Do not show installation instructions.

\`\`\`python
import requests
import pandas as pd
from datetime import datetime
\`\`\`

If an import name conflicts with a resource type:

\`\`\`python
# Wrong - don't rename the type
import stripe as stripe_lib
class stripe_type(TypedDict): ...

# Correct - rename the import
import stripe as stripe_sdk
class stripe(TypedDict):
    api_key: str
\`\`\`

## Windmill Client

Import the windmill client for platform interactions:

\`\`\`python
import wmill
\`\`\`

See the SDK documentation for available methods.

## Preprocessor Scripts

For preprocessor scripts, the function should be named \`preprocessor\` and receives an \`event\` parameter:

\`\`\`python
from typing import TypedDict, Literal, Any

class Event(TypedDict):
    kind: Literal["webhook", "http", "websocket", "kafka", "email", "nats", "postgres", "sqs", "mqtt", "gcp"]
    body: Any
    headers: dict[str, str]
    query: dict[str, str]

def preprocessor(event: Event):
    # Transform the event into flow input parameters
    return {
        "param1": event["body"]["field1"],
        "param2": event["query"]["id"]
    }
\`\`\`

## S3 Object Operations

Windmill provides built-in support for S3-compatible storage operations.

### Receiving an S3Object as a script parameter

To accept a file from S3 as input to a script, type the parameter with \`S3Object\` (imported from \`wmill\`):

\`\`\`python
import wmill
from wmill import S3Object

def main(file: S3Object):
    content = wmill.load_s3_file(file)
    # ...
\`\`\`

### S3 operations

\`\`\`python
import wmill

# Load file content from S3
content: bytes = wmill.load_s3_file(s3object)

# Load file as stream reader
reader: BufferedReader = wmill.load_s3_file_reader(s3object)

# Write file to S3
result: S3Object = wmill.write_s3_file(
    s3object,           # Target path (or None to auto-generate)
    file_content,       # bytes or BufferedReader
    s3_resource_path,   # Optional: specific S3 resource
    content_type,       # Optional: MIME type
    content_disposition # Optional: Content-Disposition header
)
\`\`\`


# Python SDK (wmill)

Import: import wmill

def worker_has_internal_server() -> bool

def get_mocked_api() -> Optional[dict]

# Get the HTTP client instance.
# 
# Returns:
#     Configured httpx.Client for API requests
def get_client() -> httpx.Client

# Make an HTTP GET request to the Windmill API.
# 
# Args:
#     endpoint: API endpoint path
#     raise_for_status: Whether to raise an exception on HTTP errors
#     **kwargs: Additional arguments passed to httpx.get
# 
# Returns:
#     HTTP response object
def get(endpoint, raise_for_status = True, **kwargs) -> httpx.Response

# Make an HTTP POST request to the Windmill API.
# 
# Args:
#     endpoint: API endpoint path
#     raise_for_status: Whether to raise an exception on HTTP errors
#     **kwargs: Additional arguments passed to httpx.post
# 
# Returns:
#     HTTP response object
def post(endpoint, raise_for_status = True, **kwargs) -> httpx.Response

# Create a new authentication token.
# 
# Args:
#     duration: Token validity duration (default: 1 day)
# 
# Returns:
#     New authentication token string
def create_token(duration = dt.timedelta(days=1)) -> str

# Create a script job and return its job id.
# 
# .. deprecated:: Use run_script_by_path_async or run_script_by_hash_async instead.
def run_script_async(path: str = None, hash_: str = None, args: dict = None, scheduled_in_secs: int = None, tag: str = None) -> str

# Create a script job by path and return its job id.
def run_script_by_path_async(path: str, args: dict = None, scheduled_in_secs: int = None, tag: str = None) -> str

# Create a script job by hash and return its job id.
def run_script_by_hash_async(hash_: str, args: dict = None, scheduled_in_secs: int = None, tag: str = None) -> str

# Create a flow job and return its job id.
def run_flow_async(path: str, args: dict = None, scheduled_in_secs: int = None, do_not_track_in_parent: bool = True, tag: str = None) -> str

# Run script synchronously and return its result.
# 
# .. deprecated:: Use run_script_by_path or run_script_by_hash instead.
def run_script(path: str = None, hash_: str = None, args: dict = None, timeout: dt.timedelta | int | float | None = None, verbose: bool = False, cleanup: bool = True, assert_result_is_not_none: bool = False, tag: str = None) -> Any

# Run script by path synchronously and return its result.
def run_script_by_path(path: str, args: dict = None, timeout: dt.timedelta | int | float | None = None, verbose: bool = False, cleanup: bool = True, assert_result_is_not_none: bool = False, tag: str = None) -> Any

# Run script by hash synchronously and return its result.
def run_script_by_hash(hash_: str, args: dict = None, timeout: dt.timedelta | int | float | None = None, verbose: bool = False, cleanup: bool = True, assert_result_is_not_none: bool = False, tag: str = None) -> Any

# Run a script on the current worker without creating a job.
# 
# On agent workers (no internal server), falls back to running a normal
# preview job and waiting for the result.
def run_inline_script_preview(content: str, language: str, args: dict = None) -> Any

# Wait for a job to complete and return its result.
# 
# Args:
#     job_id: ID of the job to wait for
#     timeout: Maximum time to wait (seconds or timedelta)
#     verbose: Enable verbose logging
#     cleanup: Register cleanup handler to cancel job on exit
#     assert_result_is_not_none: Raise exception if result is None
# 
# Returns:
#     Job result when completed
# 
# Raises:
#     TimeoutError: If timeout is reached
#     Exception: If job fails
def wait_job(job_id, timeout: dt.timedelta | int | float | None = None, verbose: bool = False, cleanup: bool = True, assert_result_is_not_none: bool = False)

# Cancel a specific job by ID.
# 
# Args:
#     job_id: UUID of the job to cancel
#     reason: Optional reason for cancellation
# 
# Returns:
#     Response message from the cancel endpoint
def cancel_job(job_id: str, reason: str = None) -> str

# Cancel currently running executions of the same script.
def cancel_running() -> dict

# Get job details by ID.
# 
# Args:
#     job_id: UUID of the job
# 
# Returns:
#     Job details dictionary
def get_job(job_id: str) -> dict

# Get the root job ID for a flow hierarchy.
# 
# Args:
#     job_id: Job ID (defaults to current WM_JOB_ID)
# 
# Returns:
#     Root job ID
def get_root_job_id(job_id: str | None = None) -> dict

# Get an OIDC JWT token for authentication to external services.
# 
# Args:
#     audience: Token audience (e.g., "vault", "aws")
#     expires_in: Optional expiration time in seconds
# 
# Returns:
#     JWT token string
def get_id_token(audience: str, expires_in: int | None = None) -> str

# Get the status of a job.
# 
# Args:
#     job_id: UUID of the job
# 
# Returns:
#     Job status: "RUNNING", "WAITING", or "COMPLETED"
def get_job_status(job_id: str) -> JobStatus

# Get the result of a completed job.
# 
# Args:
#     job_id: UUID of the completed job
#     assert_result_is_not_none: Raise exception if result is None
# 
# Returns:
#     Job result
def get_result(job_id: str, assert_result_is_not_none: bool = True) -> Any

# Get a variable value by path.
# 
# Args:
#     path: Variable path in Windmill
# 
# Returns:
#     Variable value as string
def get_variable(path: str) -> str

# Set a variable value by path, creating it if it doesn't exist.
# 
# Args:
#     path: Variable path in Windmill
#     value: Variable value to set
#     is_secret: Whether the variable should be secret (default: False)
def set_variable(path: str, value: str, is_secret: bool = False) -> None

# Get a resource value by path.
# 
# Args:
#     path: Resource path in Windmill
#     none_if_undefined: Return None instead of raising if not found
#     interpolated: if variables and resources are fully unrolled
# 
# Returns:
#     Resource value dictionary or None
def get_resource(path: str, none_if_undefined: bool = False, interpolated: bool = True) -> dict | None

# Set a resource value by path, creating it if it doesn't exist.
# 
# Args:
#     value: Resource value to set
#     path: Resource path in Windmill
#     resource_type: Resource type for creation
def set_resource(value: Any, path: str, resource_type: str)

# List resources from Windmill workspace.
# 
# Args:
#     resource_type: Optional resource type to filter by (e.g., "postgresql", "mysql", "s3")
#     page: Optional page number for pagination
#     per_page: Optional number of results per page
#     
# Returns:
#     List of resource dictionaries
def list_resources(resource_type: str = None, page: int = None, per_page: int = None) -> list[dict]

# Set the workflow state.
# 
# Args:
#     value: State value to set
#     path: Optional state resource path override.
def set_state(value: Any, path: str | None = None) -> None

# Get the workflow state.
# 
# Args:
#     path: Optional state resource path override.
# 
# Returns:
#     State value or None if not set
def get_state(path: str | None = None) -> Any

# Set job progress percentage (0-99).
# 
# Args:
#     value: Progress percentage
#     job_id: Job ID (defaults to current WM_JOB_ID)
def set_progress(value: int, job_id: Optional[str] = None)

# Get job progress percentage.
# 
# Args:
#     job_id: Job ID (defaults to current WM_JOB_ID)
# 
# Returns:
#     Progress value (0-100) or None if not set
def get_progress(job_id: Optional[str] = None) -> Any

# Set the user state of a flow at a given key
def set_flow_user_state(key: str, value: Any) -> None

# Get the user state of a flow at a given key
def get_flow_user_state(key: str) -> Any

# Get the Windmill server version.
# 
# Returns:
#     Version string
def version()

# Convenient helpers that takes an S3 resource as input and returns the settings necessary to
# initiate an S3 connection from DuckDB
def get_duckdb_connection_settings(s3_resource_path: str = '') -> DuckDbConnectionSettings | None

# Convenient helpers that takes an S3 resource as input and returns the settings necessary to
# initiate an S3 connection from Polars
def get_polars_connection_settings(s3_resource_path: str = '') -> PolarsConnectionSettings

# Convenient helpers that takes an S3 resource as input and returns the settings necessary to
# initiate an S3 connection using boto3
def get_boto3_connection_settings(s3_resource_path: str = '') -> Boto3ConnectionSettings

# Load a file from the workspace s3 bucket and returns its content as bytes.
# 
# '''python
# from wmill import S3Object
# 
# s3_obj = S3Object(s3="/path/to/my_file.txt")
# my_obj_content = client.load_s3_file(s3_obj)
# file_content = my_obj_content.decode("utf-8")
# '''
def load_s3_file(s3object: S3Object | str, s3_resource_path: str | None) -> bytes

# Load a file from the workspace s3 bucket and returns the bytes stream.
# 
# '''python
# from wmill import S3Object
# 
# s3_obj = S3Object(s3="/path/to/my_file.txt")
# with wmill.load_s3_file_reader(s3object, s3_resource_path) as file_reader:
#     print(file_reader.read())
# '''
def load_s3_file_reader(s3object: S3Object | str, s3_resource_path: str | None) -> BufferedReader

# Write a file to the workspace S3 bucket
# 
# '''python
# from wmill import S3Object
# 
# s3_obj = S3Object(s3="/path/to/my_file.txt")
# 
# # for an in memory bytes array:
# file_content = b'Hello Windmill!'
# client.write_s3_file(s3_obj, file_content)
# 
# # for a file:
# with open("my_file.txt", "rb") as my_file:
#     client.write_s3_file(s3_obj, my_file)
# '''
def write_s3_file(s3object: S3Object | str | None, file_content: BufferedReader | bytes, s3_resource_path: str | None, content_type: str | None = None, content_disposition: str | None = None) -> S3Object

# Permanently delete a file from the workspace S3 bucket.
# 
# '''python
# from wmill import S3Object
# 
# s3_obj = S3Object(s3="/path/to/my_file.txt")
# client.delete_s3_object(s3_obj)
# '''
def delete_s3_object(s3object: S3Object | str, s3_resource_path: str | None = None) -> None

# Sign S3 objects for use by anonymous users in public apps.
# 
# Args:
#     s3_objects: List of S3 objects to sign
# 
# Returns:
#     List of signed S3 objects
def sign_s3_objects(s3_objects: list[S3Object | str]) -> list[S3Object]

# Sign a single S3 object for use by anonymous users in public apps.
# 
# Args:
#     s3_object: S3 object to sign
# 
# Returns:
#     Signed S3 object
def sign_s3_object(s3_object: S3Object | str) -> S3Object

# Generate presigned public URLs for an array of S3 objects.
# If an S3 object is not signed yet, it will be signed first.
# 
# Args:
#     s3_objects: List of S3 objects to sign
#     base_url: Optional base URL for the presigned URLs (defaults to WM_BASE_URL)
# 
# Returns:
#     List of signed public URLs
# 
# Example:
#     >>> s3_objs = [S3Object(s3="/path/to/file1.txt"), S3Object(s3="/path/to/file2.txt")]
#     >>> urls = client.get_presigned_s3_public_urls(s3_objs)
def get_presigned_s3_public_urls(s3_objects: list[S3Object | str], base_url: str | None = None) -> list[str]

# Generate a presigned public URL for an S3 object.
# If the S3 object is not signed yet, it will be signed first.
# 
# Args:
#     s3_object: S3 object to sign
#     base_url: Optional base URL for the presigned URL (defaults to WM_BASE_URL)
# 
# Returns:
#     Signed public URL
# 
# Example:
#     >>> s3_obj = S3Object(s3="/path/to/file.txt")
#     >>> url = client.get_presigned_s3_public_url(s3_obj)
def get_presigned_s3_public_url(s3_object: S3Object | str, base_url: str | None = None) -> str

# Get the current user information.
# 
# Returns:
#     User details dictionary
def whoami() -> dict

# Get the current user information (alias for whoami).
# 
# Returns:
#     User details dictionary
def user() -> dict

# Get the state resource path from environment.
# 
# Returns:
#     State path string
def state_path() -> str

# Get the workflow state.
# 
# Returns:
#     State value or None if not set
def state() -> Any

# Set the state in the shared folder using pickle
def set_shared_state_pickle(value: Any, path: str = 'state.pickle') -> None

# Get the state in the shared folder using pickle
def get_shared_state_pickle(path: str = 'state.pickle') -> Any

# Set the state in the shared folder using pickle
def set_shared_state(value: Any, path: str = 'state.json') -> None

# Get the state in the shared folder using pickle
def get_shared_state(path: str = 'state.json') -> None

# Get URLs needed for resuming a flow after suspension.
# 
# Args:
#     approver: Optional approver name
#     flow_level: If True, generate resume URLs for the parent flow instead of the
#         specific step. This allows pre-approvals that can be consumed by any later
#         suspend step in the same flow.
# 
# Returns:
#     Dictionary with approvalPage, resume, and cancel URLs
def get_resume_urls(approver: str = None, flow_level: bool = None) -> dict

# Sends an interactive approval request via Slack, allowing optional customization of the message, approver, and form fields.
# 
# **[Enterprise Edition Only]** To include form fields in the Slack approval request, use the "Advanced -> Suspend -> Form" functionality.
# Learn more at: https://www.windmill.dev/docs/flows/flow_approval#form
# 
# :param slack_resource_path: The path to the Slack resource in Windmill.
# :type slack_resource_path: str
# :param channel_id: The Slack channel ID where the approval request will be sent.
# :type channel_id: str
# :param message: Optional custom message to include in the Slack approval request.
# :type message: str, optional
# :param approver: Optional user ID or name of the approver for the request.
# :type approver: str, optional
# :param default_args_json: Optional dictionary defining or overriding the default arguments for form fields.
# :type default_args_json: dict, optional
# :param dynamic_enums_json: Optional dictionary overriding the enum default values of enum form fields.
# :type dynamic_enums_json: dict, optional
# 
# :raises Exception: If the function is not called within a flow or flow preview.
# :raises Exception: If the required flow job or flow step environment variables are not set.
# 
# :return: None
# 
# **Usage Example:**
#     >>> client.request_interactive_slack_approval(
#     ...     slack_resource_path="/u/alex/my_slack_resource",
#     ...     channel_id="admins-slack-channel",
#     ...     message="Please approve this request",
#     ...     approver="approver123",
#     ...     default_args_json={"key1": "value1", "key2": 42},
#     ...     dynamic_enums_json={"foo": ["choice1", "choice2"], "bar": ["optionA", "optionB"]},
#     ... )
# 
# **Notes:**
# - This function must be executed within a Windmill flow or flow preview.
# - The function checks for required environment variables (\`WM_FLOW_JOB_ID\`, \`WM_FLOW_STEP_ID\`) to ensure it is run in the appropriate context.
def request_interactive_slack_approval(slack_resource_path: str, channel_id: str, message: str = None, approver: str = None, default_args_json: dict = None, dynamic_enums_json: dict = None) -> None

# Get email from workspace username
# This method is particularly useful for apps that require the email address of the viewer.
# Indeed, in the viewer context WM_USERNAME is set to the username of the viewer but WM_EMAIL is set to the email of the creator of the app.
def username_to_email(username: str) -> str

# Send a message to a Microsoft Teams conversation with conversation_id, where success is used to style the message
def send_teams_message(conversation_id: str, text: str, success: bool = True, card_block: dict = None)

# Get a DataTable client for SQL queries.
# 
# Args:
#     name: Database name (default: "main")
# 
# Returns:
#     DataTableClient instance
def datatable(name: str = 'main')

# Get a DuckLake client for DuckDB queries.
# 
# Args:
#     name: Database name (default: "main")
# 
# Returns:
#     DucklakeClient instance
def ducklake(name: str = 'main')

def init_global_client(f)

def deprecate(in_favor_of: str)

# Get the current workspace ID.
# 
# Returns:
#     Workspace ID string
def get_workspace() -> str

def get_version() -> str

# Run a script synchronously by hash and return its result.
# 
# Args:
#     hash: Script hash
#     args: Script arguments
#     verbose: Enable verbose logging
#     assert_result_is_not_none: Raise exception if result is None
#     cleanup: Register cleanup handler to cancel job on exit
#     timeout: Maximum time to wait
#     tag: Override the worker tag the job runs on
# 
# Returns:
#     Script result
def run_script_sync(hash: str, args: Dict[str, Any] = None, verbose: bool = False, assert_result_is_not_none: bool = True, cleanup: bool = True, timeout: dt.timedelta = None, tag: str = None) -> Any

# Run a script synchronously by path and return its result.
# 
# Args:
#     path: Script path
#     args: Script arguments
#     verbose: Enable verbose logging
#     assert_result_is_not_none: Raise exception if result is None
#     cleanup: Register cleanup handler to cancel job on exit
#     timeout: Maximum time to wait
#     tag: Override the worker tag the job runs on
# 
# Returns:
#     Script result
def run_script_by_path_sync(path: str, args: Dict[str, Any] = None, verbose: bool = False, assert_result_is_not_none: bool = True, cleanup: bool = True, timeout: dt.timedelta = None, tag: str = None) -> Any

# Convenient helpers that takes an S3 resource as input and returns the settings necessary to
# initiate an S3 connection from DuckDB
def duckdb_connection_settings(s3_resource_path: str = '') -> DuckDbConnectionSettings

# Convenient helpers that takes an S3 resource as input and returns the settings necessary to
# initiate an S3 connection from Polars
def polars_connection_settings(s3_resource_path: str = '') -> PolarsConnectionSettings

# Convenient helpers that takes an S3 resource as input and returns the settings necessary to
# initiate an S3 connection using boto3
def boto3_connection_settings(s3_resource_path: str = '') -> Boto3ConnectionSettings

# Get the state resource path from environment.
# 
# Returns:
#     State path string
def get_state_path() -> str

# Parse resource syntax from string.
def parse_resource_syntax(s: str) -> Optional[str]

# Parse S3 object from a \`s3://<storage>/<key>\` URI string (\`s3:///<key>\`
# for the default storage) or S3Object format. Any other string raises
# rather than falling back to an auto-generated key: an auto key is
# requested by omitting the object, and a fallback would silently misplace
# the upload on any typo.
def parse_s3_object(s3_object: S3Object | str) -> S3Object

# Parse variable syntax from string.
def parse_variable_syntax(s: str) -> Optional[str]

# Append a text to the result stream.
# 
# Args:
#     text: text to append to the result stream
def append_to_result_stream(text: str) -> None

# Stream to the result stream.
# 
# Args:
#     stream: stream to stream to the result stream
def stream_result(stream) -> None

# Execute a SQL query against the DataTable.
# 
# Args:
#     sql: SQL query string with $1, $2, etc. placeholders
#     *args: Positional arguments to bind to query placeholders
# 
# Returns:
#     SqlQuery instance for fetching results
def query(sql: str, *args) -> SqlQuery

# Idempotently materialize the rows of \`select_sql\` into ducklake
# \`table\` for one \`partition\` (or the whole table when \`partition\` is
# None). Client-side equivalent of the \`// materialize\` engine: with
# \`unique_key\` it upserts within the slice (delete-by-key + insert);
# without it, it replaces (whole table → CREATE OR REPLACE; partition →
# delete the partition + insert). Re-running the same slice is safe — the
# backfill / failure-recovery contract.
# 
# The partition value is bound as a DuckDB arg (never string-interpolated)
# so it cannot inject SQL. \`select_sql\` is trusted (your own query).
def upsert_partition(table: str, select_sql: str, partition: str = None, unique_key: str = None, partition_col: str = '_wm_partition', schema: str = None)

# INSERT-only materialization (no dedup / no replace) for an immutable
# event-log table — for one \`partition\`, or the whole table when
# \`partition\` is None. NOTE: unlike \`upsert_partition\`, re-running the same
# slice duplicates rows — use only for append-only sources.
def append_partition(table: str, select_sql: str, partition: str = None, partition_col: str = '_wm_partition', schema: str = None)

# Read a materialized ducklake table, optionally a single partition.
def read(table: str, partition: str = None, partition_col: str = '_wm_partition', schema: str = None)

# Execute query and fetch results.
# 
# Args:
#     result_collection: Optional result collection mode
# 
# Returns:
#     Query results
def fetch(result_collection: str | None = None)

# Execute query and fetch first row of results.
# 
# Returns:
#     First row of query results
def fetch_one()

# Execute query and fetch first row of results. Return result as a scalar value.
# 
# Returns:
#     First row of query result as a scalar value
def fetch_one_scalar()

# Execute query and don't return any results.
#         
def execute()

# DuckDB executor requires explicit argument types at declaration
# These types exist in both DuckDB and Postgres
# Check that the types exist if you plan to extend this function for other SQL engines.
def infer_sql_type(value) -> str

def parse_sql_client_name(name: str) -> tuple[str, Optional[str]]

# Decorator that marks a function as a workflow task.
# 
# Works in both WAC v1 (sync, HTTP-based dispatch) and WAC v2
# (async, checkpoint/replay) modes:
# 
# - **v2 (inside @workflow)**: dispatches as a checkpoint step.
# - **v1 (WM_JOB_ID set, no @workflow)**: dispatches via HTTP API.
# - **Standalone**: executes the function body directly.
# 
# Usage::
# 
#     @task
#     async def extract_data(url: str): ...
# 
#     @task(path="f/external_script", timeout=600, tag="gpu")
#     async def run_external(x: int): ...
def task(_func = None, path: Optional[str] = None, tag: Optional[str] = None, timeout: Optional[int] = None, cache_ttl: Optional[int] = None, priority: Optional[int] = None, concurrency_limit: Optional[int] = None, concurrency_key: Optional[str] = None, concurrency_time_window_s: Optional[int] = None)

# Create a task that dispatches to a separate Windmill script.
# 
# Usage::
# 
#     extract = task_script("f/data/extract", timeout=600)
# 
#     @workflow
#     async def main():
#         data = await extract(url="https://...")
def task_script(path: str, timeout: Optional[int] = None, tag: Optional[str] = None, cache_ttl: Optional[int] = None, priority: Optional[int] = None, concurrency_limit: Optional[int] = None, concurrency_key: Optional[str] = None, concurrency_time_window_s: Optional[int] = None)

# Create a task that dispatches to a separate Windmill flow.
# 
# Usage::
# 
#     pipeline = task_flow("f/etl/pipeline", priority=10)
# 
#     @workflow
#     async def main():
#         result = await pipeline(input=data)
def task_flow(path: str, timeout: Optional[int] = None, tag: Optional[str] = None, cache_ttl: Optional[int] = None, priority: Optional[int] = None, concurrency_limit: Optional[int] = None, concurrency_key: Optional[str] = None, concurrency_time_window_s: Optional[int] = None)

# Decorator marking an async function as a workflow-as-code entry point.
# 
# The function must be **deterministic**: given the same inputs it must call
# tasks in the same order on every replay. Branching on task results is fine
# (results are replayed from checkpoint), but branching on external state
# (current time, random values, external API calls) must use \`\`step()\`\` to
# checkpoint the value so replays see the same result.
def workflow(func)

# Execute \`\`fn\`\` inline and checkpoint the result.
# 
# On replay the cached value is returned without re-executing \`\`fn\`\`.
# Use for lightweight deterministic operations (timestamps, random IDs,
# config reads) that should not incur the overhead of a child job.
async def step(name: str, fn)

# Server-side sleep — suspend the workflow for the given duration without holding a worker.
# 
# Inside a @workflow, the parent job suspends and auto-resumes after \`\`seconds\`\`.
# Outside a workflow, falls back to \`\`asyncio.sleep\`\`.
async def sleep(seconds: int)

# Suspend the workflow and wait for an external approval.
# 
# Use \`\`get_resume_urls()\`\` (wrapped in \`\`step()\`\`) to obtain
# resume/cancel/approval URLs before calling this function.
# 
# Returns a dict with \`\`value\`\` (form data), \`\`approver\`\`, and \`\`approved\`\`.
# 
# Args:
#     timeout: Approval timeout in seconds (default 1800).
#     form: Optional form schema for the approval page.
#     self_approval: Whether the user who triggered the flow can approve it (default True).
# 
# Example::
# 
#     urls = await step("urls", lambda: get_resume_urls())
#     await step("notify", lambda: send_email(urls["approvalPage"]))
#     result = await wait_for_approval(timeout=3600)
async def wait_for_approval(timeout: int = 1800, form: dict | None = None, self_approval: bool = True) -> dict

# Process items in parallel with optional concurrency control.
# 
# Each item is processed by calling \`\`fn(item)\`\`, which should be a @task.
# Items are dispatched in batches of \`\`concurrency\`\` (default: all at once).
# 
# Example::
# 
#     @task
#     async def process(item: str):
#         ...
# 
#     results = await parallel(items, process, concurrency=5)
async def parallel(items, fn, concurrency: Optional[int] = None)

# Commit Kafka offsets for a trigger with auto_commit disabled.
# 
# Args:
#     trigger_path: Path to the Kafka trigger (from event['wm_trigger']['trigger_path'])
#     topic: Kafka topic name (from event['topic'])
#     partition: Partition number (from event['partition'])
#     offset: Message offset to commit (from event['offset'])
def commit_kafka_offsets(trigger_path: str, topic: str, partition: int, offset: int) -> None

`,
  "write-script-rlang": `---
name: write-script-rlang
description: MUST use when writing R scripts.
---

## CLI Commands

Place scripts in a folder.

After writing, tell the user which command fits what they want to do:

- \`wmill script preview <script_path>\` — **default when iterating on a local script.** Runs the local file without deploying.
- \`wmill script run <path>\` — runs the script **already deployed** in the workspace. Use only when the user explicitly wants to test the deployed version, not local edits.
- \`wmill generate-metadata\` — regenerate the local \`.script.yaml\` (input schema) and \`.lock\` (resolved dependencies) for scripts you changed, and refresh their content hashes in \`wmill-lock.yaml\`. Local files only — **not** a deploy. See "Keep metadata in sync" below.
- 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 script", "try it", "test it", "does it work" while there are **local edits to the script file**, use \`script preview\`. Do NOT push the script to then \`script run\` it — pushing is a deploy, and deploying just to test overwrites the workspace version with untested changes.

Only use \`script run\` when:
- The user explicitly says "run the deployed version" / "run what's on the server".
- There is no local script being edited (you're just invoking an existing script).

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.

### Keep metadata in sync after editing

\`wmill-lock.yaml\` tracks a content hash for each item. Editing a script's content — most importantly **adding or removing an import** or **changing \`main\`'s arguments** — invalidates that hash and leaves the \`.lock\`, the \`.script.yaml\` input schema, and the hash row out of date. Run \`wmill generate-metadata\` (scoped to what you touched) after such edits so the resolved lock, the auto-generated args UI (driven by \`.script.yaml\`), and \`wmill-lock.yaml\` all match the code. Leaving them stale produces spurious diffs in git-sync and CI.

This only writes local files (it is **not** a deploy), but it re-resolves dependencies, so it can bump unpinned versions (the same as deploying from the UI; expected, not a bug). So by default offer it and run it once the user agrees, rather than running it silently after every edit — unless the project's \`AGENTS.md\` opts into running metadata automatically (see the "Keeping metadata in sync" preference there). Either way YOU run the command, not the user. After running it, diff the regenerated \`.lock\` / \`.script.lock\` files and tell the user which dependency versions changed (e.g. \`requests 2.31.0 → 2.32.0\`), so they can catch an unwanted bump before deploying — even under \`Metadata: auto\`, since it's information, not a confirmation gate. Pin versions in code to keep them fixed.

With no path argument, \`generate-metadata\` regenerates only the items whose content hash drifted — not everything. Imports propagate: editing a script that others import marks every importer stale too, so a one-line change to a shared module can regenerate many locks (by design — their locks must reflect the imported code). If it touches more than you expect, run \`wmill generate-metadata --dry-run\` — it lists each stale item with a reason (\`content changed\` or \`depends on <path>\`) without changing anything — then narrow with a path argument (\`wmill generate-metadata f/foo\`) or \`--strict-folder-boundaries\`.

If the on-disk \`.lock\` and \`.script.yaml\` are already correct and only \`wmill-lock.yaml\` needs its hashes refreshed (hash drift, or bootstrapping missing entries), use \`wmill generate-metadata rehash\` — it re-records hashes from disk with no backend round-trip and no dependency changes.

### After writing — offer to test, don't wait passively

If the user hasn't already told you to run/test/preview the script, offer it as a one-sentence next step (e.g. "Want me to run \`wmill script preview\` with sample args?"). Do not present a multi-option menu.

If the user already asked to test/run/try the script in their original request, skip the offer and just execute \`wmill script preview <path> -d '<args>'\` directly — pick plausible args from the script's declared parameters. The shape varies by language: \`main(...)\` for code languages, the SQL dialect's own placeholder syntax (\`$1\` for PostgreSQL, \`?\` for MySQL/Snowflake, \`@P1\` for MSSQL, \`@name\` for BigQuery, etc.), positional \`$1\`, \`$2\`, … for Bash, \`param(...)\` for PowerShell.

\`wmill script preview\` does not deploy, but it still executes script code and may cause side effects; run it yourself when the user asked to test/preview (or after confirming that execution is intended). \`wmill generate-metadata\` does not deploy either — it only writes local files (locks, schemas, hashes) — but offer it before running (or run automatically if the project's \`AGENTS.md\` opts in), per "Keep metadata in sync" above. Deploying to the workspace (\`git push\` or \`wmill sync push\` depending on how the repo is wired — see the **Deploying** section) is the only step that mutates remote state — do it only when the user explicitly asks to deploy/publish/push.

For a **visual** open-the-script-in-the-dev-page preview (rather than \`script preview\`'s run-and-print-result), use the \`preview\` skill.

Use \`wmill resource-type list --schema\` to discover available resource types.

# R

## Structure

Define a \`main\` function using \`<-\` or \`=\` assignment. Parameters become the script inputs:

\`\`\`r
library(dplyr)
library(jsonlite)

main <- function(x, name = "default", flag = TRUE) {
    df <- tibble(x = x, name = name)
    result <- df %>% mutate(greeting = paste("Hello", name))
    return(toJSON(result, auto_unbox = TRUE))
}
\`\`\`

**Important:**
- The \`main\` function is required
- Use \`library()\` to load packages — they are resolved and installed automatically
- \`jsonlite\` is always available (used internally for argument parsing)
- Return values must be JSON-serializable

## Parameters

R types map to Windmill types:
- \`numeric\` → float/int
- \`character\` → string
- \`logical\` → bool (use \`TRUE\`/\`FALSE\`)
- \`list\` → object/dict
- \`NULL\` → null

Default values are inferred from the function signature:

\`\`\`r
main <- function(
    name,              # required string
    count = 10,        # optional int, default 10
    verbose = FALSE    # optional bool, default FALSE
) {
    # ...
}
\`\`\`

## Resources and Variables

Use the built-in Windmill helpers (no import needed):

\`\`\`r
main <- function() {
    # Get a variable
    api_key <- get_variable("f/my_folder/api_key")

    # Get a resource (returns a list)
    db <- get_resource("f/my_folder/postgres_config")
    host <- db$host
    port <- db$port

    return(list(host = host, port = port))
}
\`\`\`

## Output

Return any JSON-serializable value from \`main\`. The return value becomes the step result:

\`\`\`r
main <- function(x) {
    # Return a scalar
    return(x + 1)

    # Or a list (becomes JSON object)
    return(list(result = x + 1, status = "ok"))
}
\`\`\`

## Annotations

Control execution behavior with comment annotations:

\`\`\`r
#renv_verbose = true        # Show verbose renv output during resolution
#renv_install_verbose = true # Show verbose output during package installation
#sandbox = true              # Run in nsjail sandbox (requires nsjail)
\`\`\`
`,
  "write-script-rust": `---
name: write-script-rust
description: MUST use when writing Rust scripts.
---

## CLI Commands

Place scripts in a folder.

After writing, tell the user which command fits what they want to do:

- \`wmill script preview <script_path>\` — **default when iterating on a local script.** Runs the local file without deploying.
- \`wmill script run <path>\` — runs the script **already deployed** in the workspace. Use only when the user explicitly wants to test the deployed version, not local edits.
- \`wmill generate-metadata\` — regenerate the local \`.script.yaml\` (input schema) and \`.lock\` (resolved dependencies) for scripts you changed, and refresh their content hashes in \`wmill-lock.yaml\`. Local files only — **not** a deploy. See "Keep metadata in sync" below.
- 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 script", "try it", "test it", "does it work" while there are **local edits to the script file**, use \`script preview\`. Do NOT push the script to then \`script run\` it — pushing is a deploy, and deploying just to test overwrites the workspace version with untested changes.

Only use \`script run\` when:
- The user explicitly says "run the deployed version" / "run what's on the server".
- There is no local script being edited (you're just invoking an existing script).

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.

### Keep metadata in sync after editing

\`wmill-lock.yaml\` tracks a content hash for each item. Editing a script's content — most importantly **adding or removing an import** or **changing \`main\`'s arguments** — invalidates that hash and leaves the \`.lock\`, the \`.script.yaml\` input schema, and the hash row out of date. Run \`wmill generate-metadata\` (scoped to what you touched) after such edits so the resolved lock, the auto-generated args UI (driven by \`.script.yaml\`), and \`wmill-lock.yaml\` all match the code. Leaving them stale produces spurious diffs in git-sync and CI.

This only writes local files (it is **not** a deploy), but it re-resolves dependencies, so it can bump unpinned versions (the same as deploying from the UI; expected, not a bug). So by default offer it and run it once the user agrees, rather than running it silently after every edit — unless the project's \`AGENTS.md\` opts into running metadata automatically (see the "Keeping metadata in sync" preference there). Either way YOU run the command, not the user. After running it, diff the regenerated \`.lock\` / \`.script.lock\` files and tell the user which dependency versions changed (e.g. \`requests 2.31.0 → 2.32.0\`), so they can catch an unwanted bump before deploying — even under \`Metadata: auto\`, since it's information, not a confirmation gate. Pin versions in code to keep them fixed.

With no path argument, \`generate-metadata\` regenerates only the items whose content hash drifted — not everything. Imports propagate: editing a script that others import marks every importer stale too, so a one-line change to a shared module can regenerate many locks (by design — their locks must reflect the imported code). If it touches more than you expect, run \`wmill generate-metadata --dry-run\` — it lists each stale item with a reason (\`content changed\` or \`depends on <path>\`) without changing anything — then narrow with a path argument (\`wmill generate-metadata f/foo\`) or \`--strict-folder-boundaries\`.

If the on-disk \`.lock\` and \`.script.yaml\` are already correct and only \`wmill-lock.yaml\` needs its hashes refreshed (hash drift, or bootstrapping missing entries), use \`wmill generate-metadata rehash\` — it re-records hashes from disk with no backend round-trip and no dependency changes.

### After writing — offer to test, don't wait passively

If the user hasn't already told you to run/test/preview the script, offer it as a one-sentence next step (e.g. "Want me to run \`wmill script preview\` with sample args?"). Do not present a multi-option menu.

If the user already asked to test/run/try the script in their original request, skip the offer and just execute \`wmill script preview <path> -d '<args>'\` directly — pick plausible args from the script's declared parameters. The shape varies by language: \`main(...)\` for code languages, the SQL dialect's own placeholder syntax (\`$1\` for PostgreSQL, \`?\` for MySQL/Snowflake, \`@P1\` for MSSQL, \`@name\` for BigQuery, etc.), positional \`$1\`, \`$2\`, … for Bash, \`param(...)\` for PowerShell.

\`wmill script preview\` does not deploy, but it still executes script code and may cause side effects; run it yourself when the user asked to test/preview (or after confirming that execution is intended). \`wmill generate-metadata\` does not deploy either — it only writes local files (locks, schemas, hashes) — but offer it before running (or run automatically if the project's \`AGENTS.md\` opts in), per "Keep metadata in sync" above. Deploying to the workspace (\`git push\` or \`wmill sync push\` depending on how the repo is wired — see the **Deploying** section) is the only step that mutates remote state — do it only when the user explicitly asks to deploy/publish/push.

For a **visual** open-the-script-in-the-dev-page preview (rather than \`script preview\`'s run-and-print-result), use the \`preview\` skill.

Use \`wmill resource-type list --schema\` to discover available resource types.

# Rust

## Structure

The script must contain a function called \`main\` with proper return type:

\`\`\`rust
use anyhow::anyhow;
use serde::Serialize;

#[derive(Serialize, Debug)]
struct ReturnType {
    result: String,
    count: i32,
}

fn main(param1: String, param2: i32) -> anyhow::Result<ReturnType> {
    Ok(ReturnType {
        result: param1,
        count: param2,
    })
}
\`\`\`

**Important:**
- Arguments should be owned types
- Return type must be serializable (\`#[derive(Serialize)]\`)
- Return type is \`anyhow::Result<T>\`

## Dependencies

Packages must be specified with a partial cargo.toml at the beginning of the script:

\`\`\`rust
//! \`\`\`cargo
//! [dependencies]
//! anyhow = "1.0.86"
//! reqwest = { version = "0.11", features = ["json"] }
//! tokio = { version = "1", features = ["full"] }
//! \`\`\`

use anyhow::anyhow;
// ... rest of the code
\`\`\`

**Note:** Serde is already included, no need to add it again.

## Async Functions

If you need to handle async functions (e.g., using tokio), keep the main function sync and create the runtime inside:

\`\`\`rust
//! \`\`\`cargo
//! [dependencies]
//! anyhow = "1.0.86"
//! tokio = { version = "1", features = ["full"] }
//! reqwest = { version = "0.11", features = ["json"] }
//! \`\`\`

use anyhow::anyhow;
use serde::Serialize;

#[derive(Serialize, Debug)]
struct Response {
    data: String,
}

fn main(url: String) -> anyhow::Result<Response> {
    let rt = tokio::runtime::Runtime::new()?;
    rt.block_on(async {
        let resp = reqwest::get(&url).await?.text().await?;
        Ok(Response { data: resp })
    })
}
\`\`\`
`,
  "write-script-snowflake": `---
name: write-script-snowflake
description: MUST use when writing Snowflake queries.
---

## CLI Commands

Place scripts in a folder.

After writing, tell the user which command fits what they want to do:

- \`wmill script preview <script_path>\` — **default when iterating on a local script.** Runs the local file without deploying.
- \`wmill script run <path>\` — runs the script **already deployed** in the workspace. Use only when the user explicitly wants to test the deployed version, not local edits.
- \`wmill generate-metadata\` — regenerate the local \`.script.yaml\` (input schema) and \`.lock\` (resolved dependencies) for scripts you changed, and refresh their content hashes in \`wmill-lock.yaml\`. Local files only — **not** a deploy. See "Keep metadata in sync" below.
- 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 script", "try it", "test it", "does it work" while there are **local edits to the script file**, use \`script preview\`. Do NOT push the script to then \`script run\` it — pushing is a deploy, and deploying just to test overwrites the workspace version with untested changes.

Only use \`script run\` when:
- The user explicitly says "run the deployed version" / "run what's on the server".
- There is no local script being edited (you're just invoking an existing script).

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.

### Keep metadata in sync after editing

\`wmill-lock.yaml\` tracks a content hash for each item. Editing a script's content — most importantly **adding or removing an import** or **changing \`main\`'s arguments** — invalidates that hash and leaves the \`.lock\`, the \`.script.yaml\` input schema, and the hash row out of date. Run \`wmill generate-metadata\` (scoped to what you touched) after such edits so the resolved lock, the auto-generated args UI (driven by \`.script.yaml\`), and \`wmill-lock.yaml\` all match the code. Leaving them stale produces spurious diffs in git-sync and CI.

This only writes local files (it is **not** a deploy), but it re-resolves dependencies, so it can bump unpinned versions (the same as deploying from the UI; expected, not a bug). So by default offer it and run it once the user agrees, rather than running it silently after every edit — unless the project's \`AGENTS.md\` opts into running metadata automatically (see the "Keeping metadata in sync" preference there). Either way YOU run the command, not the user. After running it, diff the regenerated \`.lock\` / \`.script.lock\` files and tell the user which dependency versions changed (e.g. \`requests 2.31.0 → 2.32.0\`), so they can catch an unwanted bump before deploying — even under \`Metadata: auto\`, since it's information, not a confirmation gate. Pin versions in code to keep them fixed.

With no path argument, \`generate-metadata\` regenerates only the items whose content hash drifted — not everything. Imports propagate: editing a script that others import marks every importer stale too, so a one-line change to a shared module can regenerate many locks (by design — their locks must reflect the imported code). If it touches more than you expect, run \`wmill generate-metadata --dry-run\` — it lists each stale item with a reason (\`content changed\` or \`depends on <path>\`) without changing anything — then narrow with a path argument (\`wmill generate-metadata f/foo\`) or \`--strict-folder-boundaries\`.

If the on-disk \`.lock\` and \`.script.yaml\` are already correct and only \`wmill-lock.yaml\` needs its hashes refreshed (hash drift, or bootstrapping missing entries), use \`wmill generate-metadata rehash\` — it re-records hashes from disk with no backend round-trip and no dependency changes.

### After writing — offer to test, don't wait passively

If the user hasn't already told you to run/test/preview the script, offer it as a one-sentence next step (e.g. "Want me to run \`wmill script preview\` with sample args?"). Do not present a multi-option menu.

If the user already asked to test/run/try the script in their original request, skip the offer and just execute \`wmill script preview <path> -d '<args>'\` directly — pick plausible args from the script's declared parameters. The shape varies by language: \`main(...)\` for code languages, the SQL dialect's own placeholder syntax (\`$1\` for PostgreSQL, \`?\` for MySQL/Snowflake, \`@P1\` for MSSQL, \`@name\` for BigQuery, etc.), positional \`$1\`, \`$2\`, … for Bash, \`param(...)\` for PowerShell.

\`wmill script preview\` does not deploy, but it still executes script code and may cause side effects; run it yourself when the user asked to test/preview (or after confirming that execution is intended). \`wmill generate-metadata\` does not deploy either — it only writes local files (locks, schemas, hashes) — but offer it before running (or run automatically if the project's \`AGENTS.md\` opts in), per "Keep metadata in sync" above. Deploying to the workspace (\`git push\` or \`wmill sync push\` depending on how the repo is wired — see the **Deploying** section) is the only step that mutates remote state — do it only when the user explicitly asks to deploy/publish/push.

For a **visual** open-the-script-in-the-dev-page preview (rather than \`script preview\`'s run-and-print-result), use the \`preview\` skill.

Use \`wmill resource-type list --schema\` to discover available resource types.

# Snowflake

Arguments use \`?\` placeholders.

Name the parameters by adding comments before the statement:

\`\`\`sql
-- ? name1 (text)
-- ? name2 (number) = 0
SELECT * FROM users WHERE name = ? AND age > ?;
\`\`\`

## Receiving an S3Object as a script parameter

Declare the arg with type \`(s3object)\`. Windmill renders an S3 file picker for
it, downloads the file, and binds it as JSON text — Parquet/CSV files are
decoded server-side into a JSON array of records, JSON/JSONL pass through.
Wrap the bind with \`PARSE_JSON(?)\` and walk it with \`LATERAL FLATTEN\`:

\`\`\`sql
-- ? file (s3object)
SELECT
  v.value:id::NUMBER AS id,
  v.value:name::STRING AS name
FROM LATERAL FLATTEN(input => PARSE_JSON(?)) v;
\`\`\`

## Streaming query results to S3

Add a \`-- s3\` directive at the top of the script to stream the result set to S3
instead of returning rows. Windmill writes the file and returns its \`S3Object\`
as the script result.

\`\`\`sql
-- s3 prefix=exports/users format=parquet
SELECT id, name FROM users;
\`\`\`

All keys are optional: \`prefix\` (object key prefix), \`storage\` (named storage —
omit to use the workspace default), \`format\` (\`json\` (default), \`parquet\`, or
\`csv\`). Use this for large result sets — rows stream directly to S3 instead of
being buffered, bypassing the 10000-row return cap.
`,
  "write-flow": `---
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_SUFFIX}}\` 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_NAMING}}

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_SUFFIX}}\` 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.

\`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

## 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

## 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 when inside a loop (\`forloopflow\` or \`whileloopflow\`)
- \`flow_input.iter.index\` - Current loop index when inside a loop (\`forloopflow\` or \`whileloopflow\`)

## Loop Structure Rules

- For \`whileloopflow\`, use module-level \`stop_after_if\` on the loop module itself when the loop should stop after an iteration result
- Do NOT put \`stop_after_if\` inside \`value\` of a \`whileloopflow\`
- \`stop_after_all_iters_if\` is for checks after the whole loop finishes, not the normal per-iteration break condition
- When a \`whileloopflow\` carries state forward between iterations, use \`flow_input.iter.value\` as the current loop value and provide an explicit first-iteration fallback when needed
- Use \`flow_input.iter.index\` only when the loop logic is truly based on the iteration index, not as a replacement for the current loop value
- 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:

\`\`\`yaml
- 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:
            state:
              type: javascript
              expr: flow_input.iter && flow_input.iter.value !== undefined ? flow_input.iter.value : flow_input.initial_state
- id: return_final_state
  value:
    type: rawscript
    input_transforms:
      final_state:
        type: javascript
        expr: results.loop_until_done[results.loop_until_done.length - 1]
\`\`\`

Incorrect \`whileloopflow\` patterns:

\`\`\`yaml
- id: loop_until_done
  value:
    type: whileloopflow
    stop_after_if:
      expr: result.done === true
\`\`\`

\`\`\`yaml
input_transforms:
  state:
    type: javascript
    expr: flow_input.iter.index
\`\`\`

\`\`\`yaml
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:

\`\`\`yaml
- 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\`):

\`\`\`yaml
- id: request_approval
  value:
    type: rawscript
    suspend:
      required_events: 1
\`\`\`

Incorrect shape (identity has no resume URLs — not a real approval):

\`\`\`yaml
- 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\`:

\`\`\`yaml
- id: route_order
  value:
    type: branchone
    ...
- id: send_confirmation
  value:
    input_transforms:
      routed:
        type: javascript
        expr: results.route_order
\`\`\`

Incorrect after \`branchone\`:

\`\`\`yaml
expr: results.create_shipment
expr: results.create_backorder
\`\`\`

Correct after \`branchall\`:

\`\`\`yaml
- 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:

\`\`\`json
{
  "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}"\`:

\`\`\`json
{
  "type": "object",
  "properties": {
    "database": {
      "type": "object",
      "format": "resource-postgresql",
      "description": "Database connection"
    }
  }
}
\`\`\`

### As Step Input (Static Reference)

Reference a specific resource using \`$res:\` prefix:

\`\`\`json
{
  "database": {
    "type": "static",
    "value": "$res:f/folder/my_database"
  }
}
\`\`\`


## OpenFlow Schema

{"OpenFlow":{"type":"object","description":"Top-level flow definition containing metadata, configuration, and the flow structure","properties":{"summary":{"type":"string","description":"Short description of what this flow does"},"description":{"type":"string","description":"Detailed documentation for this flow"},"value":{"$ref":"#/components/schemas/FlowValue"},"schema":{"type":"object","description":"JSON Schema for flow inputs. Use this to define input parameters, their types, defaults, and validation. For resource inputs, set type to 'object' and format to 'resource-<type>' (e.g., 'resource-stripe')"},"on_behalf_of_email":{"type":"string","description":"The flow will be run with the permissions of the user with this email."}},"required":["summary","value"]},"FlowValue":{"type":"object","description":"The flow structure containing modules and optional preprocessor/failure handlers","properties":{"modules":{"type":"array","description":"Array of steps that execute in sequence. Each step can be a script, subflow, loop, or branch","items":{"$ref":"#/components/schemas/FlowModule"}},"failure_module":{"description":"Special module that executes when the flow fails. Receives error object with message, name, stack, and step_id. Must have id 'failure'. Only supports script/rawscript types","$ref":"#/components/schemas/FlowModule"},"preprocessor_module":{"description":"Special module that runs before the first step on external triggers. Must have id 'preprocessor'. Only supports script/rawscript types. Cannot reference other step results","$ref":"#/components/schemas/FlowModule"},"same_worker":{"type":"boolean","description":"If true, all steps run on the same worker for better performance"},"preserve_step_tags":{"type":"boolean","description":"If true and the flow runs on a custom worker tag, steps that declare their own non-empty tag run on it instead of inheriting the flow tag. Steps without their own tag still inherit the flow tag."},"concurrent_limit":{"type":"number","description":"Maximum number of concurrent executions of this flow"},"concurrency_key":{"type":"string","description":"Expression to group concurrent executions (e.g., by user ID)"},"concurrency_time_window_s":{"type":"number","description":"Time window in seconds for concurrent_limit"},"debounce_delay_s":{"type":"integer","description":"Delay in seconds to debounce flow executions"},"debounce_key":{"type":"string","description":"Expression to group debounced executions"},"debounce_args_to_accumulate":{"type":"array","description":"Arguments to accumulate across debounced executions","items":{"type":"string"}},"max_total_debouncing_time":{"type":"integer","description":"Maximum total time in seconds that a job can be debounced"},"max_total_debounces_amount":{"type":"integer","description":"Maximum number of times a job can be debounced"},"skip_expr":{"type":"string","description":"JavaScript expression to conditionally skip the entire flow"},"cache_ttl":{"type":"number","description":"Cache duration in seconds for flow results"},"cache_ignore_s3_path":{"type":"boolean"},"delete_after_secs":{"type":"integer","description":"If set, delete the flow job's args, result and logs after this many seconds following job completion"},"flow_env":{"type":"object","description":"Environment variables available to all steps. Values can be strings, JSON values, or special references: '$var:path' (workspace variable) or '$res:path' (resource).","additionalProperties":{}},"priority":{"type":"number","description":"Execution priority (higher numbers run first)"},"early_return":{"type":"string","description":"JavaScript expression to return early from the flow"},"chat_input_enabled":{"type":"boolean","description":"Whether this flow accepts chat-style input"},"notes":{"type":"array","description":"Sticky notes attached to the flow","items":{"$ref":"#/components/schemas/FlowNote"}},"groups":{"type":"array","description":"Semantic groups of modules for organizational purposes","items":{"$ref":"#/components/schemas/FlowGroup"}}},"required":["modules"]},"Retry":{"type":"object","description":"Retry configuration for failed module executions","properties":{"constant":{"type":"object","description":"Retry with constant delay between attempts","properties":{"attempts":{"type":"integer","description":"Number of retry attempts"},"seconds":{"type":"integer","description":"Seconds to wait between retries"}}},"exponential":{"type":"object","description":"Retry with exponential backoff (delay doubles each time)","properties":{"attempts":{"type":"integer","description":"Number of retry attempts"},"multiplier":{"type":"integer","description":"Multiplier for exponential backoff"},"seconds":{"type":"integer","minimum":1,"description":"Initial delay in seconds"},"random_factor":{"type":"integer","minimum":0,"maximum":100,"description":"Random jitter percentage (0-100) to avoid thundering herd"}}},"retry_if":{"$ref":"#/components/schemas/RetryIf"}}},"FlowNote":{"type":"object","description":"A sticky note attached to a flow for documentation and annotation","properties":{"id":{"type":"string","description":"Unique identifier for the note"},"text":{"type":"string","description":"Content of the note"},"position":{"type":"object","description":"Position of the note in the flow editor","properties":{"x":{"type":"number","description":"X coordinate"},"y":{"type":"number","description":"Y coordinate"}},"required":["x","y"]},"size":{"type":"object","description":"Size of the note in the flow editor","properties":{"width":{"type":"number","description":"Width in pixels"},"height":{"type":"number","description":"Height in pixels"}},"required":["width","height"]},"color":{"type":"string","description":"Color of the note (e.g., \\"yellow\\", \\"#ffff00\\")"},"type":{"type":"string","enum":["free","group"],"description":"Type of note - 'free' for standalone notes, 'group' for notes that group other nodes"},"locked":{"type":"boolean","default":false,"description":"Whether the note is locked and cannot be edited or moved"},"contained_node_ids":{"type":"array","items":{"type":"string"},"description":"For group notes, the IDs of nodes contained within this group"}},"required":["id","text","color","type"]},"FlowGroup":{"type":"object","description":"A semantic group of flow modules for organizational purposes. Does not affect execution \\u2014 modules remain in their original position in the flow. Groups provide naming and collapsibility in the editor. Members are computed dynamically from all nodes on paths between start_id and end_id.","properties":{"summary":{"type":"string","description":"Display name for this group"},"note":{"type":"string","description":"Markdown note shown below the group header"},"autocollapse":{"type":"boolean","default":false,"description":"If true, this group is collapsed by default in the flow editor. UI hint only."},"start_id":{"type":"string","description":"ID of the first flow module in this group (topological entry point)"},"end_id":{"type":"string","description":"ID of the last flow module in this group (topological exit point)"},"color":{"type":"string","description":"Color for the group in the flow editor"}},"required":["start_id","end_id"]},"RetryIf":{"type":"object","description":"Conditional retry based on error or result","properties":{"expr":{"type":"string","description":"JavaScript expression that returns true to retry. Has access to 'result' and 'error' variables"}},"required":["expr"]},"StopAfterIf":{"type":"object","description":"Early termination condition for a module","properties":{"skip_if_stopped":{"type":"boolean","description":"If true, following steps are skipped when this condition triggers"},"expr":{"type":"string","description":"JavaScript expression evaluated after the module runs. Can use 'result' (step's result) or 'flow_input'. Return true to stop"},"error_message":{"type":"string","nullable":true,"description":"Custom error message when stopping with an error. Mutually exclusive with skip_if_stopped. If set to a non-empty string, the flow stops with this error. If empty string, a default error message is used. If null or omitted, no error is raised."},"error_include_result":{"type":"boolean","description":"When stopping with an error (error_message set), embed the stopping step's own result inside the raised error object (as error.result) instead of discarding it. The top-level result stays { error }. Defaults to false."}},"required":["expr"]},"FlowModule":{"type":"object","description":"A single step in a flow. Can be a script, subflow, loop, or branch","properties":{"id":{"type":"string","description":"Unique identifier for this step. Used to reference results via 'results.step_id'. Must be a valid identifier (alphanumeric, underscore, hyphen)"},"value":{"$ref":"#/components/schemas/FlowModuleValue"},"stop_after_if":{"description":"Early termination condition evaluated after this step completes","$ref":"#/components/schemas/StopAfterIf"},"stop_after_all_iters_if":{"description":"For loops only - early termination condition evaluated after all iterations complete","$ref":"#/components/schemas/StopAfterIf"},"skip_if":{"type":"object","description":"Conditionally skip this step based on previous results or flow inputs","properties":{"expr":{"type":"string","description":"JavaScript expression that returns true to skip. Can use 'flow_input' or 'results.<step_id>'"}},"required":["expr"]},"sleep":{"description":"Delay before executing this step (in seconds or as expression)","$ref":"#/components/schemas/InputTransform"},"cache_ttl":{"type":"number","description":"Cache duration in seconds for this step's results"},"cache_ignore_s3_path":{"type":"boolean"},"timeout":{"description":"Maximum execution time in seconds (static value or expression)","$ref":"#/components/schemas/InputTransform"},"delete_after_secs":{"type":"integer","description":"If set, delete the step's args, result and logs after this many seconds following job completion"},"summary":{"type":"string","description":"Short description of what this step does"},"mock":{"type":"object","description":"Mock configuration for testing without executing the actual step","properties":{"enabled":{"type":"boolean","description":"If true, return mock value instead of executing"},"return_value":{"description":"Value to return when mocked"}}},"suspend":{"type":"object","description":"Configuration for approval/resume steps that wait for user input","properties":{"required_events":{"type":"integer","description":"Number of approvals required before continuing"},"timeout":{"type":"integer","description":"Timeout in seconds before auto-continuing or canceling"},"resume_form":{"type":"object","description":"Form schema for collecting input when resuming","properties":{"schema":{"type":"object","description":"JSON Schema for the resume form"}}},"user_auth_required":{"type":"boolean","description":"If true, only authenticated users can approve"},"user_groups_required":{"description":"Expression or list of groups that can approve","$ref":"#/components/schemas/InputTransform"},"self_approval_disabled":{"type":"boolean","description":"If true, the user who started the flow cannot approve"},"hide_cancel":{"type":"boolean","description":"If true, hide the cancel button on the approval form"},"continue_on_disapprove_timeout":{"type":"boolean","description":"If true, continue flow on timeout instead of canceling"}}},"priority":{"type":"number","description":"Execution priority for this step (higher numbers run first)"},"continue_on_error":{"type":"boolean","description":"If true, flow continues even if this step fails"},"retry":{"description":"Retry configuration if this step fails","$ref":"#/components/schemas/Retry"},"debouncing":{"description":"Debounce configuration for this step (EE only)","type":"object","properties":{"debounce_delay_s":{"type":"integer","description":"Delay in seconds to debounce this step's executions across flow runs"},"debounce_key":{"type":"string","description":"Expression to group debounced executions. Supports $workspace and $args[name]. Default: $workspace/flow/<flow_path>-<step_id>"},"debounce_args_to_accumulate":{"type":"array","description":"Array-type arguments to accumulate across debounced executions","items":{"type":"string"}},"max_total_debouncing_time":{"type":"integer","description":"Maximum total time in seconds before forced execution"},"max_total_debounces_amount":{"type":"integer","description":"Maximum number of debounces before forced execution"}}}},"required":["value","id"]},"InputTransform":{"description":"Maps input parameters for a step. Can be a static value or a JavaScript expression that references previous results or flow inputs","oneOf":[{"$ref":"#/components/schemas/StaticTransform"},{"$ref":"#/components/schemas/JavascriptTransform"},{"$ref":"#/components/schemas/AiTransform"}],"discriminator":{"propertyName":"type","mapping":{"static":"#/components/schemas/StaticTransform","javascript":"#/components/schemas/JavascriptTransform","ai":"#/components/schemas/AiTransform"}}},"StaticTransform":{"type":"object","description":"Static value passed directly to the step. Use for hardcoded values or resource references like '$res:path/to/resource'","properties":{"value":{"description":"The static value. For resources, use format '$res:path/to/resource'"},"type":{"type":"string","enum":["static"]}},"required":["type"]},"JavascriptTransform":{"type":"object","description":"JavaScript expression evaluated at runtime. Can reference previous step results via 'results.step_id' or flow inputs via 'flow_input.property'. Inside loops, use 'flow_input.iter.value' for the current iteration value","properties":{"expr":{"type":"string","description":"JavaScript expression returning the value. Available variables - results (object with all previous step results), flow_input (flow inputs), flow_input.iter (in loops)"},"type":{"type":"string","enum":["javascript"]}},"required":["expr","type"]},"AiTransform":{"type":"object","description":"Value resolved by the AI runtime for this input. The AI engine decides how to satisfy the parameter.","properties":{"type":{"type":"string","enum":["ai"]}},"required":["type"]},"AIProviderKind":{"type":"string","description":"Supported AI provider types","enum":["openai","azure_openai","azure_foundry","anthropic","mistral","deepseek","googleai","groq","openrouter","togetherai","customai","aws_bedrock"]},"ProviderConfig":{"type":"object","description":"Complete AI provider configuration with resource reference and model selection","properties":{"kind":{"$ref":"#/components/schemas/AIProviderKind"},"resource":{"type":"string","description":"Resource reference in format '$res:{resource_path}' pointing to provider credentials"},"model":{"type":"string","description":"Model identifier (e.g., 'gpt-4', 'claude-3-opus-20240229', 'gemini-pro')"},"reasoning_effort":{"type":"string","description":"Provider-native reasoning effort token (e.g. 'low', 'high', 'none') for models that support extended thinking. Optional; unset leaves the provider default."}},"required":["kind","resource","model"]},"StaticProviderTransform":{"type":"object","description":"Static provider configuration passed directly to the AI agent","properties":{"value":{"$ref":"#/components/schemas/ProviderConfig"},"type":{"type":"string","enum":["static"]}},"required":["type","value"]},"ProviderTransform":{"description":"Provider configuration - can be static (ProviderConfig), JavaScript expression, or AI-determined","oneOf":[{"$ref":"#/components/schemas/StaticProviderTransform"},{"$ref":"#/components/schemas/JavascriptTransform"},{"$ref":"#/components/schemas/AiTransform"}],"discriminator":{"propertyName":"type","mapping":{"static":"#/components/schemas/StaticProviderTransform","javascript":"#/components/schemas/JavascriptTransform","ai":"#/components/schemas/AiTransform"}}},"MemoryOff":{"type":"object","description":"No conversation memory/context","properties":{"kind":{"type":"string","enum":["off"]}},"required":["kind"]},"MemoryAuto":{"type":"object","description":"Automatic context management","properties":{"kind":{"type":"string","enum":["auto"]},"context_length":{"type":"integer","description":"Maximum number of messages to retain in context"},"memory_id":{"type":"string","description":"Identifier for persistent memory across agent invocations"}},"required":["kind"]},"MemoryMessage":{"type":"object","description":"A single message in conversation history","properties":{"role":{"type":"string","enum":["user","assistant","system"]},"content":{"type":"string"}},"required":["role","content"]},"MemoryManual":{"type":"object","description":"Explicit message history","properties":{"kind":{"type":"string","enum":["manual"]},"messages":{"type":"array","items":{"$ref":"#/components/schemas/MemoryMessage"}}},"required":["kind","messages"]},"MemoryConfig":{"description":"Conversation memory configuration","oneOf":[{"$ref":"#/components/schemas/MemoryOff"},{"$ref":"#/components/schemas/MemoryAuto"},{"$ref":"#/components/schemas/MemoryManual"}],"discriminator":{"propertyName":"kind","mapping":{"off":"#/components/schemas/MemoryOff","auto":"#/components/schemas/MemoryAuto","manual":"#/components/schemas/MemoryManual"}}},"StaticMemoryTransform":{"type":"object","description":"Static memory configuration passed directly to the AI agent","properties":{"value":{"$ref":"#/components/schemas/MemoryConfig"},"type":{"type":"string","enum":["static"]}},"required":["type","value"]},"MemoryTransform":{"description":"Memory configuration - can be static (MemoryConfig), JavaScript expression, or AI-determined","oneOf":[{"$ref":"#/components/schemas/StaticMemoryTransform"},{"$ref":"#/components/schemas/JavascriptTransform"},{"$ref":"#/components/schemas/AiTransform"}],"discriminator":{"propertyName":"type","mapping":{"static":"#/components/schemas/StaticMemoryTransform","javascript":"#/components/schemas/JavascriptTransform","ai":"#/components/schemas/AiTransform"}}},"FlowModuleValue":{"description":"The actual implementation of a flow step. Can be a script (inline or referenced), subflow, loop, branch, or special module type","oneOf":[{"$ref":"#/components/schemas/RawScript"},{"$ref":"#/components/schemas/PathScript"},{"$ref":"#/components/schemas/PathFlow"},{"$ref":"#/components/schemas/ForloopFlow"},{"$ref":"#/components/schemas/WhileloopFlow"},{"$ref":"#/components/schemas/BranchOne"},{"$ref":"#/components/schemas/BranchAll"},{"$ref":"#/components/schemas/Identity"},{"$ref":"#/components/schemas/AiAgent"}],"discriminator":{"propertyName":"type","mapping":{"rawscript":"#/components/schemas/RawScript","script":"#/components/schemas/PathScript","flow":"#/components/schemas/PathFlow","forloopflow":"#/components/schemas/ForloopFlow","whileloopflow":"#/components/schemas/WhileloopFlow","branchone":"#/components/schemas/BranchOne","branchall":"#/components/schemas/BranchAll","identity":"#/components/schemas/Identity","aiagent":"#/components/schemas/AiAgent"}}},"RawScript":{"type":"object","description":"Inline script with code defined directly in the flow. Use 'bun' as default language if unspecified. The script receives arguments from input_transforms","properties":{"input_transforms":{"type":"object","description":"Map of parameter names to their values (static or JavaScript expressions). These become the script's input arguments","additionalProperties":{"$ref":"#/components/schemas/InputTransform"}},"content":{"type":"string","description":"The script source code. Should export a 'main' function"},"language":{"type":"string","description":"Programming language for this script","enum":["deno","bun","bunnative","python3","go","bash","powershell","postgresql","mysql","bigquery","snowflake","mssql","oracledb","graphql","nativets","php","rust","ansible","csharp","nu","java","ruby","rlang","duckdb"]},"path":{"type":"string","description":"Optional path for saving this script"},"lock":{"type":"string","description":"Lock file content for dependencies"},"type":{"type":"string","enum":["rawscript"]},"tag":{"type":"string","description":"Worker group tag for execution routing"},"concurrent_limit":{"type":"number","description":"Maximum concurrent executions of this script"},"concurrency_time_window_s":{"type":"number","description":"Time window for concurrent_limit"},"custom_concurrency_key":{"type":"string","description":"Custom key for grouping concurrent executions"},"is_trigger":{"type":"boolean","description":"If true, this script is a trigger that can start the flow"},"assets":{"type":"array","description":"External resources this script accesses (S3 objects, resources, etc.)","items":{"type":"object","required":["path","kind"],"properties":{"path":{"type":"string","description":"Path to the asset"},"kind":{"type":"string","description":"Type of asset","enum":["s3object","resource","ducklake","datatable","volume"]},"access_type":{"type":"string","nullable":true,"description":"Access level for this asset","enum":["r","w","rw"]},"alt_access_type":{"type":"string","nullable":true,"description":"Alternative access level","enum":["r","w","rw"]}}}}},"required":["type","content","language","input_transforms"]},"PathScript":{"type":"object","description":"Reference to an existing script by path. Use this when calling a previously saved script instead of writing inline code","properties":{"input_transforms":{"type":"object","description":"Map of parameter names to their values (static or JavaScript expressions). These become the script's input arguments","additionalProperties":{"$ref":"#/components/schemas/InputTransform"}},"path":{"type":"string","description":"Path to the script in the workspace (e.g., 'f/scripts/send_email')"},"hash":{"type":"string","description":"Optional specific version hash of the script to use"},"type":{"type":"string","enum":["script"]},"tag_override":{"type":"string","description":"Override the script's default worker group tag"},"is_trigger":{"type":"boolean","description":"If true, this script is a trigger that can start the flow"}},"required":["type","path","input_transforms"]},"PathFlow":{"type":"object","description":"Reference to an existing flow by path. Use this to call another flow as a subflow","properties":{"input_transforms":{"type":"object","description":"Map of parameter names to their values (static or JavaScript expressions). These become the subflow's input arguments","additionalProperties":{"$ref":"#/components/schemas/InputTransform"}},"path":{"type":"string","description":"Path to the flow in the workspace (e.g., 'f/flows/process_user')"},"type":{"type":"string","enum":["flow"]}},"required":["type","path","input_transforms"]},"ForloopFlow":{"type":"object","description":"Executes nested modules in a loop over an iterator. Inside the loop, use 'flow_input.iter.value' to access the current iteration value, and 'flow_input.iter.index' for the index. Supports parallel execution for better performance on I/O-bound operations","properties":{"modules":{"type":"array","description":"Steps to execute for each iteration. These can reference the iteration value via 'flow_input.iter.value'","items":{"$ref":"#/components/schemas/FlowModule"}},"iterator":{"description":"JavaScript expression that returns an array to iterate over. Can reference 'results.step_id' or 'flow_input'","$ref":"#/components/schemas/InputTransform"},"skip_failures":{"type":"boolean","description":"If true, iteration failures don't stop the loop. Failed iterations return null"},"type":{"type":"string","enum":["forloopflow"]},"parallel":{"type":"boolean","description":"If true, iterations run concurrently (faster for I/O-bound operations). Use with parallelism to control concurrency"},"parallelism":{"description":"Maximum number of concurrent iterations when parallel=true. Limits resource usage. Can be static number or expression","$ref":"#/components/schemas/InputTransform"},"squash":{"type":"boolean"}},"required":["modules","iterator","skip_failures","type"]},"WhileloopFlow":{"type":"object","description":"Executes nested modules repeatedly while a condition is true. The loop checks the condition after each iteration. Use stop_after_if on modules to control loop termination","properties":{"modules":{"type":"array","description":"Steps to execute in each iteration. Use stop_after_if to control when the loop ends","items":{"$ref":"#/components/schemas/FlowModule"}},"skip_failures":{"type":"boolean","description":"If true, iteration failures don't stop the loop. Failed iterations return null"},"type":{"type":"string","enum":["whileloopflow"]},"parallel":{"type":"boolean","description":"If true, iterations run concurrently (use with caution in while loops)"},"parallelism":{"description":"Maximum number of concurrent iterations when parallel=true","$ref":"#/components/schemas/InputTransform"},"squash":{"type":"boolean"}},"required":["modules","skip_failures","type"]},"BranchOne":{"type":"object","description":"Conditional branching where only the first matching branch executes. Branches are evaluated in order, and the first one with a true expression runs. If no branches match, the default branch executes","properties":{"branches":{"type":"array","description":"Array of branches to evaluate in order. The first branch with expr evaluating to true executes","items":{"type":"object","properties":{"summary":{"type":"string","description":"Short description of this branch condition"},"expr":{"type":"string","description":"JavaScript expression that returns boolean. Can use 'results.step_id' or 'flow_input'. First true expr wins"},"modules":{"type":"array","description":"Steps to execute if this branch's expr is true","items":{"$ref":"#/components/schemas/FlowModule"}}},"required":["modules","expr"]}},"default":{"type":"array","description":"Steps to execute if no branch expressions match","items":{"$ref":"#/components/schemas/FlowModule"}},"type":{"type":"string","enum":["branchone"]}},"required":["branches","default","type"]},"BranchAll":{"type":"object","description":"Parallel branching where all branches execute simultaneously. Unlike BranchOne, all branches run regardless of conditions. Useful for executing independent tasks concurrently","properties":{"branches":{"type":"array","description":"Array of branches that all execute (either in parallel or sequentially)","items":{"type":"object","properties":{"summary":{"type":"string","description":"Short description of this branch's purpose"},"skip_failure":{"type":"boolean","description":"If true, failure in this branch doesn't fail the entire flow"},"modules":{"type":"array","description":"Steps to execute in this branch","items":{"$ref":"#/components/schemas/FlowModule"}}},"required":["modules"]}},"type":{"type":"string","enum":["branchall"]},"parallel":{"type":"boolean","description":"If true, all branches execute concurrently. If false, they execute sequentially"}},"required":["branches","type"]},"AgentTool":{"type":"object","description":"A tool available to an AI agent. Can be a flow module or an external MCP (Model Context Protocol) tool","properties":{"id":{"type":"string","description":"Unique identifier for this tool. Cannot contain spaces - use underscores instead (e.g., 'get_user_data' not 'get user data')"},"summary":{"type":"string","description":"Short description of what this tool does (shown to the AI)"},"description":{"type":"string","description":"Free-text description of the tool given to the AI to decide when and how to call it. Overrides the description auto-derived from the underlying script."},"value":{"$ref":"#/components/schemas/ToolValue"}},"required":["id","value"]},"ToolValue":{"description":"The implementation of a tool. Can be a flow module (script/flow) or an MCP tool reference","oneOf":[{"$ref":"#/components/schemas/FlowModuleTool"},{"$ref":"#/components/schemas/McpToolValue"},{"$ref":"#/components/schemas/WebsearchToolValue"}],"discriminator":{"propertyName":"tool_type","mapping":{"flowmodule":"#/components/schemas/FlowModuleTool","mcp":"#/components/schemas/McpToolValue","websearch":"#/components/schemas/WebsearchToolValue"}}},"FlowModuleTool":{"description":"A tool implemented as a flow module (script, flow, etc.). The AI can call this like any other flow module","allOf":[{"type":"object","properties":{"tool_type":{"type":"string","enum":["flowmodule"]}},"required":["tool_type"]},{"$ref":"#/components/schemas/FlowModuleValue"}]},"WebsearchToolValue":{"type":"object","description":"A tool implemented as a websearch tool. The AI can call this like any other websearch tool","properties":{"tool_type":{"type":"string","enum":["websearch"]}},"required":["tool_type"]},"McpToolValue":{"type":"object","description":"Reference to an external MCP (Model Context Protocol) tool. The AI can call tools from MCP servers","properties":{"tool_type":{"type":"string","enum":["mcp"]},"resource_path":{"type":"string","description":"Path to the MCP resource/server configuration"},"include_tools":{"type":"array","description":"Whitelist of specific tools to include from this MCP server","items":{"type":"string"}},"exclude_tools":{"type":"array","description":"Blacklist of tools to exclude from this MCP server","items":{"type":"string"}}},"required":["tool_type","resource_path"]},"AiAgent":{"type":"object","description":"AI agent step that can use tools to accomplish tasks. The agent receives inputs and can call any of its configured tools to complete the task","properties":{"input_transforms":{"type":"object","description":"Input parameters for the AI agent mapped to their values","properties":{"provider":{"$ref":"#/components/schemas/ProviderTransform"},"output_type":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"Output format type.\\nValid values: 'text' (default) - plain text response, 'image' - image generation\\n"},"user_message":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"The user's prompt/message to the AI agent. Supports variable interpolation with flow.input syntax."},"system_prompt":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"System instructions that guide the AI's behavior, persona, and response style. Optional."},"streaming":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"Boolean. If true, stream the AI response incrementally.\\nStreaming events include: token_delta, reasoning_token_delta, tool_call, tool_call_arguments, tool_execution, tool_result\\n"},"memory":{"$ref":"#/components/schemas/MemoryTransform"},"output_schema":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"JSON Schema object defining structured output format. Used when you need the AI to return data in a specific shape.\\nSupports standard JSON Schema properties: type, properties, required, items, enum, pattern, minLength, maxLength, minimum, maximum, etc.\\nExample: { type: 'object', properties: { name: { type: 'string' }, age: { type: 'integer' } }, required: ['name'] }\\n"},"user_attachments":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"Array of file references (images or PDFs) for the AI agent.\\nFormat: Array<{ bucket: string, key: string }> - S3 object references\\nExample: [{ bucket: 'my-bucket', key: 'documents/report.pdf' }]\\n"},"max_completion_tokens":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"Integer. Maximum number of tokens the AI will generate in its response.\\nRange: 1 to 4,294,967,295. Typical values: 256-4096 for most use cases.\\n"},"temperature":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"Float. Controls randomness/creativity of responses.\\nRange: 0.0 to 2.0 (provider-dependent)\\n- 0.0 = deterministic, focused responses\\n- 0.7 = balanced (common default)\\n- 1.0+ = more creative/random\\n"},"max_iterations":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"Number. Limits how many times the agent can loop through reasoning and tool use.\\nRange: 1-1000.\\n"}},"required":["provider","user_message","output_type"]},"tools":{"type":"array","description":"Array of tools the agent can use. The agent decides which tools to call based on the task","items":{"$ref":"#/components/schemas/AgentTool"}},"type":{"type":"string","enum":["aiagent"]},"tag":{"type":"string","description":"Worker group tag for execution routing. If not set, the AI agent step runs on the flow's tag (default \`flow\`)"},"omit_output_from_conversation":{"type":"boolean","default":false,"description":"If true, this AI agent step does not persist its assistant or tool messages to the flow conversation when chat mode is enabled."},"parallel":{"type":"boolean","description":"If true, the agent can execute multiple tool calls in parallel"}},"required":["tools","type","input_transforms"]},"Identity":{"type":"object","description":"Pass-through module that returns its input unchanged. Useful for flow structure or as a placeholder","properties":{"type":{"type":"string","enum":["identity"]},"flow":{"type":"boolean","description":"If true, marks this as a flow identity (special handling)"}},"required":["type"]},"FlowStatus":{"type":"object","properties":{"step":{"type":"integer"},"modules":{"type":"array","items":{"$ref":"#/components/schemas/FlowStatusModule"}},"user_states":{"additionalProperties":true},"preprocessor_module":{"allOf":[{"$ref":"#/components/schemas/FlowStatusModule"}]},"failure_module":{"allOf":[{"$ref":"#/components/schemas/FlowStatusModule"},{"type":"object","properties":{"parent_module":{"type":"string"}}}]},"retry":{"type":"object","properties":{"fail_count":{"type":"integer"},"failed_jobs":{"type":"array","items":{"type":"string","format":"uuid"}}}}},"required":["step","modules","failure_module"]},"FlowStatusModule":{"type":"object","properties":{"type":{"type":"string","enum":["WaitingForPriorSteps","WaitingForEvents","WaitingForExecutor","InProgress","Success","Failure"]},"id":{"type":"string"},"job":{"type":"string","format":"uuid"},"count":{"type":"integer"},"progress":{"type":"integer"},"iterator":{"type":"object","properties":{"index":{"type":"integer"},"itered":{"type":"array","items":{}},"itered_len":{"type":"integer"},"args":{}}},"flow_jobs":{"type":"array","items":{"type":"string"}},"flow_jobs_success":{"type":"array","items":{"type":"boolean"}},"flow_jobs_duration":{"type":"object","properties":{"started_at":{"type":"array","items":{"type":"string"}},"duration_ms":{"type":"array","items":{"type":"integer"}}}},"branch_chosen":{"type":"object","properties":{"type":{"type":"string","enum":["branch","default"]},"branch":{"type":"integer"}},"required":["type"]},"branchall":{"type":"object","properties":{"branch":{"type":"integer"},"len":{"type":"integer"}},"required":["branch","len"]},"approvers":{"type":"array","items":{"type":"object","properties":{"resume_id":{"type":"integer"},"approver":{"type":"string"}},"required":["resume_id","approver"]}},"failed_retries":{"type":"array","items":{"type":"string","format":"uuid"}},"skipped":{"type":"boolean"},"agent_actions":{"type":"array","items":{"type":"object","oneOf":[{"type":"object","properties":{"job_id":{"type":"string","format":"uuid"},"function_name":{"type":"string"},"type":{"type":"string","enum":["tool_call"]},"module_id":{"type":"string"}},"required":["job_id","function_name","type","module_id"]},{"type":"object","properties":{"call_id":{"type":"string","format":"uuid"},"function_name":{"type":"string"},"resource_path":{"type":"string"},"type":{"type":"string","enum":["mcp_tool_call"]},"arguments":{"type":"object"}},"required":["call_id","function_name","resource_path","type"]},{"type":"object","properties":{"type":{"type":"string","enum":["web_search"]}},"required":["type"]},{"type":"object","properties":{"type":{"type":"string","enum":["message"]}},"required":["content","type"]}]}},"agent_actions_success":{"type":"array","items":{"type":"boolean"}}},"required":["type"]}}`,
  "raw-app": `---
name: raw-app
description: MUST use when creating raw apps.
---

# Windmill Raw Apps — CLI workflow

This guide covers raw apps from the terminal: scaffolding via \`wmill app new\`, the on-disk layout, and the file-based conventions the CLI uses to represent backend runnables and data table configuration. The platform shape (how a raw app behaves at runtime — frontend bundling, runnable types, datatable SDK calls) is covered in the companion authoring guide.

## Creating a Raw App

**You — the AI agent — create the app yourself by running \`wmill app new\` with the right flags. Do NOT tell the user to "run \`wmill app new\` and follow the prompts" or wait for them to do it.** The bare \`wmill app new\` is an interactive wizard that hangs waiting for stdin in any non-TTY context (which includes you). Always pass flags.

### Step 1 — Gather the three required values by asking the user

You need three things to run the command:

1. **summary** — a short description of the app
2. **path** — the windmill path, e.g. \`f/folder/my_app\` or \`u/username/my_app\`
3. **framework** — one of \`react19\` (recommended), \`react18\`, \`svelte5\`, \`vue\`

If the user's request did not supply *every* one of these explicitly, ask. Do not guess values, do not invent paths, do not pick a framework on the user's behalf, do not "just use react19 because it's the default".

Use whichever interactive question facility your runtime provides — a structured multi-choice tool if available, otherwise plain chat — and group all missing fields into a single round-trip so the user answers them at once:

- For \`framework\` — multiple-choice with the four allowed values; mark \`react19\` as \`(Recommended)\` and put it first.
- For \`summary\` and \`path\` — provide one or two example values as multiple-choice options (the user can pick "Other" to type a free-form answer).

Only proceed once you have concrete values for all three. If the user replies with something ambiguous, ask again rather than guessing.

### Step 2 — Run the command yourself

Once you have summary + path + framework, run it:

\`\`\`bash
wmill app new \\
  --summary "Customer dashboard" \\
  --path f/sales/dashboard \\
  --framework react19
\`\`\`

That's the minimum. The datatable wizard and the "Open in Claude Desktop?" prompt are skipped silently because passing any of \`--summary\`/\`--path\`/\`--framework\` puts the command in non-interactive mode.

### Optional flags

Layer these in only when the user asked for them:

| Flag | When to add it |
|---|---|
| \`--datatable <name>\` | The user wants this app wired to a specific Windmill datatable. Without it, the app is created with no datatable. |
| \`--schema <name>\` | Together with \`--datatable\`. Creates the schema with \`CREATE SCHEMA IF NOT EXISTS\` if it doesn't already exist. |
| \`--overwrite\` | The target directory already exists and the user said it's OK to replace. Without it, non-interactive mode aborts with an error so you don't clobber existing work. |
| \`--no-open-in-desktop\` | Already implied in non-interactive mode; only needed if you're somehow running interactively. |

### Step 3 — Offer the visual preview

After \`wmill app new\` and any initial edits to \`App.tsx\` / \`index.tsx\`, **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 when an embedded preview tool is in play) the user should consent to.

For apps the preview command runs from the app folder (\`cd <app_path>__raw_app && wmill app dev …\`); the \`preview\` skill picks the proxy vs direct branch based on whether the runtime exposes a tool that can embed a localhost URL. If the user already asked to see/preview/visualize the app in their original request, skip the offer and just invoke the skill.

### Anti-patterns to avoid

- ❌ Running \`wmill app new\` with no flags (the prompt will hang).
- ❌ Telling the user to "run \`wmill app new\` and follow the prompts" — that's a step backwards from what you can do directly.
- ❌ Inventing a path/summary/framework instead of asking the user.
- ❌ Defaulting to \`react19\` because the user didn't say — even sensible defaults must be confirmed.
- ❌ Passing \`--overwrite\` automatically when the directory exists — confirm with the user first.

### Interactive (only when a human is at the terminal)

\`\`\`bash
wmill app new
\`\`\`

This is the wizard. It only works when run by a human in a real terminal. Don't call it this way from an agent.

## On-disk app layout

\`\`\`
my_app{{RAW_APP_SUFFIX}}/
├── AGENTS.md              # AI agent instructions (auto-generated)
├── DATATABLES.md          # Database schemas (run 'wmill app generate-agents' to refresh)
├── raw_app.yaml           # App configuration (summary, path, data settings)
├── index.tsx              # Frontend entry point
├── App.tsx                # Main React/Svelte/Vue component
├── index.css              # Styles
├── package.json           # Frontend dependencies
├── wmill.ts               # Auto-generated backend type definitions (DO NOT EDIT)
├── backend/               # Backend runnables (server-side scripts)
│   ├── <id>.<ext>         # Code file (e.g., get_user.ts)
│   ├── <id>.yaml          # Optional: config for fields, or to reference existing scripts
│   └── <id>.lock          # Lock file (run 'wmill generate-metadata' to create/update)
└── sql_to_apply/          # SQL migrations (dev only, not synced)
    └── *.sql              # SQL files to apply via dev server
\`\`\`

## Backend runnables on disk

Add a code file to the \`backend/\` folder:

\`\`\`
backend/<id>.<ext>
\`\`\`

The runnable ID is the filename without extension. For example, \`get_user.ts\` creates a runnable with ID \`get_user\`.

### Supported languages (extension-driven)

| Language         | Extension    | Example          |
|------------------|--------------|------------------|
| TypeScript       | \`.ts\`        | \`myFunc.ts\`      |
| TypeScript (Bun) | \`.bun.ts\`    | \`myFunc.bun.ts\`  |
| TypeScript (Deno)| \`.deno.ts\`   | \`myFunc.deno.ts\` |
| Python           | \`.py\`        | \`myFunc.py\`      |
| Go               | \`.go\`        | \`myFunc.go\`      |
| Bash             | \`.sh\`        | \`myFunc.sh\`      |
| PowerShell       | \`.ps1\`       | \`myFunc.ps1\`     |
| PostgreSQL       | \`.pg.sql\`    | \`myFunc.pg.sql\`  |
| MySQL            | \`.my.sql\`    | \`myFunc.my.sql\`  |
| BigQuery         | \`.bq.sql\`    | \`myFunc.bq.sql\`  |
| Snowflake        | \`.sf.sql\`    | \`myFunc.sf.sql\`  |
| MS SQL           | \`.ms.sql\`    | \`myFunc.ms.sql\`  |
| GraphQL          | \`.gql\`       | \`myFunc.gql\`     |
| PHP              | \`.php\`       | \`myFunc.php\`     |
| Rust             | \`.rs\`        | \`myFunc.rs\`      |
| C#               | \`.cs\`        | \`myFunc.cs\`      |
| Java             | \`.java\`      | \`myFunc.java\`    |

After creating or editing a backend runnable — especially when its imports or arguments changed — its local lock and \`wmill-lock.yaml\` go stale. Offer to run \`wmill generate-metadata\` and run it once the user agrees (or automatically if the project's \`AGENTS.md\` opts into that) — YOU run it, don't just name it and wait. It writes local files only (not a deploy), and keeping the lock current avoids noise in git-sync/CI:
\`\`\`bash
wmill generate-metadata
\`\`\`
After it runs, check the regenerated \`.lock\` diff and tell the user which dependency versions changed (e.g. \`requests 2.31.0 → 2.32.0\`), so they can catch an unwanted bump before deploying.

### Optional YAML configuration

Add a \`<id>.yaml\` file alongside the code to configure fields or static values:

**backend/get_user.yaml:**
\`\`\`yaml
type: inline
fields:
  user_id:
    type: static
    value: "default_user"
\`\`\`

### Referencing existing scripts

To use an existing Windmill script instead of inline code:

**backend/existing_script.yaml:**
\`\`\`yaml
type: script
path: f/my_folder/existing_script
\`\`\`

For flows:
\`\`\`yaml
type: flow
path: f/my_folder/my_flow
\`\`\`

## Data tables — \`raw_app.yaml\` config

The \`data\` block in \`raw_app.yaml\` controls which tables the app can query.

\`\`\`yaml
data:
  datatable: main           # Default datatable
  schema: app_schema        # Default schema (optional)
  tables:
    - main/users            # Table in public schema
    - main/app_schema:items # Table in specific schema
\`\`\`

**Table reference formats:**
- \`<datatable>\` — All tables in the datatable
- \`<datatable>/<table>\` — Specific table in public schema
- \`<datatable>/<schema>:<table>\` — Table in specific schema

## SQL Migrations (sql_to_apply/)

The \`sql_to_apply/\` folder is for creating/modifying database tables during development.

### Workflow

1. Create \`.sql\` files in \`sql_to_apply/\`
2. Run \`wmill app dev\` — the dev server watches this folder
3. When SQL files change, a modal appears in the browser to confirm execution
4. After creating tables, **add them to \`data.tables\`** in \`raw_app.yaml\`

### Example migration

**sql_to_apply/001_create_users.sql:**
\`\`\`sql
CREATE TABLE IF NOT EXISTS users (
    id SERIAL PRIMARY KEY,
    email TEXT NOT NULL UNIQUE,
    name TEXT,
    created_at TIMESTAMP DEFAULT NOW()
);
\`\`\`

After applying, add to \`raw_app.yaml\`:
\`\`\`yaml
data:
  tables:
    - main/users
\`\`\`

### Migration best practices

- **Use idempotent SQL**: \`CREATE TABLE IF NOT EXISTS\`, etc.
- **Number files**: \`001_\`, \`002_\` for ordering
- **Always whitelist tables** after creation
- This folder is NOT synced — it's for local development only

## CLI Commands

Two commands you run yourself, not the user:
- \`wmill app new\` — run it with flags, per the "Creating a Raw App" section above.
- \`wmill generate-metadata\` — (re)generates local lock files and refreshes \`wmill-lock.yaml\` content hashes; writes local files only (not a deploy). After adding or editing a runnable, offer it and run it on agreement — or automatically if the project's \`AGENTS.md\` opts into that (see "After creating a runnable" above).

For the rest, tell the user which command fits their intent and let them run it — these deploy to the workspace, overwrite local files, or launch a long-running server, so the user should consent each time:

| Command | Description |
|---------|-------------|
| \`wmill app dev\` | Start dev server with live reload (see the \`preview\` skill for the full open-the-app-in-the-IDE-pane procedure). |
| \`wmill app generate-agents\` | Refresh AGENTS.md and DATATABLES.md |
| \`wmill sync push\` | Deploy app to Windmill |
| \`wmill sync pull\` | Pull latest from Windmill |



# Windmill Raw Apps

Raw apps let you build custom frontends with React, Svelte, or Vue that connect to Windmill backend runnables and datatables.

## App shape

A raw app has three logical parts:

- **Frontend** — bundled with esbuild from \`index.tsx\` as the entrypoint. Files include the entrypoint, components (\`App.tsx\`), styles, etc.
- **Backend runnables** — server-side scripts the frontend calls, each addressed by a unique key.
- **Data** — optional whitelisted datatables (managed PostgreSQL) that the backend runnables can query. The frontend never queries the database directly; backend runnables are the only bridge.

## Frontend

### Entrypoint

The entrypoint is \`index.tsx\` for React and \`index.ts\` for Svelte and Vue. It is both the bundling entrypoint (the bundler is esbuild) and the **mount** entrypoint: the preview executes the bundle against an empty \`<div id="root">\` and auto-renders nothing, so the entrypoint must mount a top-level \`App\` itself. Keep the UI in \`App.tsx\` / \`App.svelte\` / \`App.vue\` and keep the entrypoint as the mount shim.

React (\`index.tsx\`):

\`\`\`tsx
import React from 'react'
import { createRoot } from 'react-dom/client'
import App from './App'

createRoot(document.getElementById('root')!).render(<App />)
\`\`\`

Svelte (\`index.ts\`): \`mount(App, { target: document.getElementById('root')! })\`. Vue (\`index.ts\`): \`createApp(App).mount('#root')\`.

**Never replace the entrypoint with a bare component** (\`export default function App() { ... }\` and no mount call). A component that is defined but never mounted renders a blank screen with **no error thrown** — it never executes, so nothing reaches the console or the error overlay. If an app renders blank, check that the entrypoint still mounts \`App\` into \`#root\`.

**Always begin every React file (\`.tsx\`/\`.jsx\`) that uses JSX with \`import React from 'react'\`.** esbuild uses the classic JSX transform, so \`React\` must be in scope wherever JSX appears — a missing import compiles fine but throws \`React is not defined\` at runtime, leaving a blank screen.

### Generated bindings (\`wmill.d.ts\` / \`wmill.ts\`)

The frontend imports a generated module that mirrors the backend runnables. **Never write to it directly** — it gets regenerated whenever backend runnables change. Modifying it by hand will be overwritten.

### Calling backend runnables

Import the generated bindings and call the runnable like a function:

\`\`\`typescript
import { backend } from './wmill';

// Call a backend runnable
const user = await backend.get_user({ user_id: '123' });
\`\`\`

The frontend cannot reach datatables, workspace items, or external services on its own — it goes through \`backend.<key>(args)\` for everything server-side.

## Backend runnables

Each runnable has a unique key (used to call it from the frontend) and one of four types:

| Type | What it is |
|---|---|
| \`inline\` | Custom code stored on the app itself. Most common for app-specific logic. |
| \`script\` | Reference to an existing workspace script by path. |
| \`flow\` | Reference to an existing workspace flow by path. |
| \`hubscript\` | Reference to a hub script by path. |

### Inline runnables

Inline runnables carry their own source code. For file-based raw apps, the runnable language is determined by the backend file extension. The script must expose a \`main\` function as its entrypoint.

**TypeScript example** (\`backend/get_user.ts\`):

\`\`\`typescript
import * as wmill from 'windmill-client';

export async function main(user_id: string) {
  const sql = wmill.datatable();
  const user = await sql\`SELECT * FROM users WHERE id = \${user_id}\`.fetchOne();
  return user;
}
\`\`\`

**Python example** (\`backend/get_user.py\`):

\`\`\`python
import wmill

def main(user_id: str):
    db = wmill.datatable()
    user = db.query('SELECT * FROM users WHERE id = $1', user_id).fetch_one()
    return user
\`\`\`

### Path runnables (script / flow / hubscript)

When \`type\` is \`script\`, \`flow\`, or \`hubscript\`, the runnable just stores a \`path\` to an existing workspace or hub item — no inline code. The referenced item's input/output schema becomes the runnable's surface.

### Static inputs

\`staticInputs\` is an optional \`Record<string, any>\` for arguments not overridable from the frontend. Useful with path runnables to pre-fill some args while leaving the rest to the frontend caller.

## Data Tables

Data tables are PostgreSQL databases managed by Windmill. Backend runnables query them via the \`wmill\` client; the frontend never queries them directly.

### Critical rules

1. **Whitelisted tables only**: a runnable can only query tables listed in the app's \`data.tables\` config. Tables not in this list are not accessible.
2. **Add tables before using**: queries against unlisted tables fail at runtime. When you introduce a new table, register it in \`data.tables\` first.
3. **Use the configured datatable/schema**: the app's \`data\` config sets the default datatable and schema; reference them consistently across runnables.

### Querying in TypeScript (Bun/Deno)

\`\`\`typescript
import * as wmill from 'windmill-client';

export async function main(user_id: string) {
  const sql = wmill.datatable();  // Or: wmill.datatable('other_datatable')

  // Parameterized queries (safe from SQL injection)
  const user = await sql\`SELECT * FROM users WHERE id = \${user_id}\`.fetchOne();
  const users = await sql\`SELECT * FROM users WHERE active = \${true}\`.fetch();

  // Insert/Update
  await sql\`INSERT INTO users (name, email) VALUES (\${name}, \${email})\`;
  await sql\`UPDATE users SET name = \${newName} WHERE id = \${user_id}\`;

  return user;
}
\`\`\`

### Querying in Python

\`\`\`python
import wmill

def main(user_id: str):
    db = wmill.datatable()  # Or: wmill.datatable('other_datatable')

    # Use $1, $2, etc. for parameters
    user = db.query('SELECT * FROM users WHERE id = $1', user_id).fetch_one()
    users = db.query('SELECT * FROM users WHERE active = $1', True).fetch()

    # Insert/Update
    db.query('INSERT INTO users (name, email) VALUES ($1, $2)', name, email)
    db.query('UPDATE users SET name = $1 WHERE id = $2', new_name, user_id)

    return user
\`\`\`

## Best Practices

1. **Check existing tables** before creating new ones — reuse beats schema growth.
2. **Use parameterized queries** — never concatenate user input into SQL.
3. **Keep runnables focused** — one function per runnable; small surface area.
4. **Use descriptive keys** — \`get_user\`, not \`a\`.
5. **Always whitelist tables** — adding a runnable that queries a new table requires the table to be in \`data.tables\` first.
`,
  "triggers": `---
name: triggers
description: MUST use when configuring triggers.
---

# Windmill Triggers

Triggers allow external events to invoke your scripts and flows.

## File Naming

Trigger configuration files use the pattern: \`{path}.{trigger_type}_trigger.yaml\`

Examples:
- \`u/user/webhook.http_trigger.yaml\`
- \`f/data/kafka_consumer.kafka_trigger.yaml\`
- \`f/sync/postgres_cdc.postgres_trigger.yaml\`
- \`f/inbound/orders.email_trigger.yaml\`

## Email Triggers

An email trigger routes incoming emails to a script or flow. Each trigger reserves a local-part: emails sent to \`<local_part>@<windmill_email_domain>\` are delivered to the configured runnable. Set \`workspaced_local_part: true\` to namespace it per workspace (the actual recipient becomes \`<workspace_id>-<local_part>@…\`); on Windmill Cloud this is required.

Senders may append URL-style extras to the local-part with \`+\`: \`mytrigger+foo=bar+baz=qux@…\`. They flow through to the script as \`email_extra_args\` (see below).

### Payload

The runnable receives:

- \`parsed_email\` — \`{ headers, text_body, html_body, attachments[] }\`. Each \`attachment\` has \`{ headers, body }\`.
- \`raw_email\` — the raw RFC 822 message as a string, **or** an S3 object (\`{ s3: "windmill_emails/<job_id>/raw.eml" }\`) if the message exceeds 1 MiB.
- \`email_extra_args\` (optional, only when sender appended \`+key=value\` extras) — a flat object of the parsed extras.

With a preprocessor, all of the above are nested under \`event\` along with \`event.kind = "email"\` and \`event.trigger_path\` (the trigger's path). Without a preprocessor, \`trigger_path\` is **not** exposed — add a preprocessor if you need it.

### Attachments are S3 objects

Binary attachments are uploaded to the workspace S3 bucket and surface in \`parsed_email.attachments[i].body\` as:

\`\`\`json
{ "s3": "windmill_emails/<job_id>/attachments/<filename>" }
\`\`\`

To read the bytes inside a script, use the wmill SDK:

\`\`\`ts
// TypeScript
import * as wmill from "windmill-client"
const file = await wmill.loadS3File(parsed_email.attachments[0].body)
\`\`\`

\`\`\`python
# Python
import wmill
data = wmill.load_s3_file(parsed_email["attachments"][0]["body"])
\`\`\`

If the workspace has no S3 resource configured (Workspace Settings → Object storage), \`body\` falls back to the string \`"configure s3 in the workspace settings to handle attachments"\`. The same applies to large \`raw_email\` bodies. Email attachment storage requires the server to be built with the \`parquet\` feature.

Text/HTML/inline parts are placed inline in \`body\` as strings.

## CLI Commands

Deploying local changes to the workspace can be destructive to remote state — only suggest/run a deploy when the user explicitly asks to deploy/publish/push, not when they say "run", "try", or "test". Deploy via \`git push\` or \`wmill sync push\` depending on how the repo is wired (see the **Deploying** section in \`AGENTS.wmill.md\`). \`sync pull\` is safe to run yourself — it never mutates remote state, though it does overwrite local files to match the remote (use \`sync pull --dry-run\` to only preview).

\`\`\`bash
# Push trigger configuration — only when the user explicitly asks to deploy
wmill sync push

# Pull triggers from Windmill
wmill sync pull
\`\`\`
`,
  "schedules": `---
name: schedules
description: MUST use when configuring schedules.
---

# Windmill Schedules

Schedules run scripts and flows automatically on a cron schedule.

## File Naming

Schedule files use the pattern: \`{path}.schedule.yaml\`

Example: \`f/folder/daily_sync.schedule.yaml\`

Note: The path is derived from the filename, not stored in the file content.

## Cron Expression Format

Windmill uses 6-field cron expressions (includes seconds):

\`\`\`
 ┌───────────── second (0-59)
 │ ┌───────────── minute (0-59)
 │ │ ┌───────────── hour (0-23)
 │ │ │ ┌───────────── day of month (1-31)
 │ │ │ │ ┌───────────── month (1-12 or jan-dec)
 │ │ │ │ │ ┌───────────── day of week (0-6, 0=Sunday, or sun-sat)
 │ │ │ │ │ │
 * * * * * *
\`\`\`

**Common Examples:**
- \`0 0 0 * * *\` - Daily at midnight
- \`0 0 12 * * *\` - Daily at noon
- \`0 */5 * * * *\` - Every 5 minutes
- \`0 0 9 * * 1-5\` - Weekdays at 9 AM
- \`0 0 0 1 * *\` - First day of each month

## CLI Commands

Deploying local changes to the workspace can be destructive to remote state — only suggest/run a deploy when the user explicitly asks to deploy/publish/push, not when they say "run", "try", or "test". Deploy via \`git push\` or \`wmill sync push\` depending on how the repo is wired (see the **Deploying** section in \`AGENTS.wmill.md\`). The commands below never mutate remote state, so they're safe to run yourself — note that \`sync pull\` does overwrite local files to match the remote (use \`sync pull --dry-run\` to only preview), while \`schedule\` just lists.

\`\`\`bash
# Push schedules to Windmill — only when the user explicitly asks to deploy
wmill sync push

# Pull schedules from Windmill
wmill sync pull

# List schedules
wmill schedule
\`\`\`
`,
  "resources": `---
name: resources
description: MUST use when managing resources.
---

# Windmill Resources

Resources store credentials and configuration for external services.

## File Format

Resource files use the pattern: \`{path}.resource.json\`

Example: \`f/databases/postgres_prod.resource.json\`

## Resource Structure

\`\`\`json
{
  "value": {
    "host": "db.example.com",
    "port": 5432,
    "user": "admin",
    "password": "$var:g/all/db_password",
    "dbname": "production"
  },
  "description": "Production PostgreSQL database",
  "resource_type": "postgresql"
}
\`\`\`

## Required Fields

- \`value\` - Object containing the resource configuration
- \`resource_type\` - Name of the resource type (e.g., "postgresql", "slack")

## Variable References

Reference variables in resource values:

\`\`\`json
{
  "value": {
    "api_key": "$var:g/all/api_key",
    "secret": "$var:u/admin/secret"
  }
}
\`\`\`

**Reference formats:**
- \`$var:g/all/name\` - Global variable
- \`$var:u/username/name\` - User variable
- \`$var:f/folder/name\` - Folder variable

## Resource References

Reference other resources:

\`\`\`json
{
  "value": {
    "database": "$res:f/databases/postgres"
  }
}
\`\`\`

## Common Resource Types

### PostgreSQL
\`\`\`json
{
  "resource_type": "postgresql",
  "value": {
    "host": "localhost",
    "port": 5432,
    "user": "postgres",
    "password": "$var:g/all/pg_password",
    "dbname": "windmill",
    "sslmode": "prefer"
  }
}
\`\`\`

### MySQL
\`\`\`json
{
  "resource_type": "mysql",
  "value": {
    "host": "localhost",
    "port": 3306,
    "user": "root",
    "password": "$var:g/all/mysql_password",
    "database": "myapp"
  }
}
\`\`\`

### Slack
\`\`\`json
{
  "resource_type": "slack",
  "value": {
    "token": "$var:g/all/slack_token"
  }
}
\`\`\`

### AWS S3
\`\`\`json
{
  "resource_type": "s3",
  "value": {
    "bucket": "my-bucket",
    "region": "us-east-1",
    "accessKeyId": "$var:g/all/aws_access_key",
    "secretAccessKey": "$var:g/all/aws_secret_key"
  }
}
\`\`\`

### HTTP/API
\`\`\`json
{
  "resource_type": "http",
  "value": {
    "baseUrl": "https://api.example.com",
    "headers": {
      "Authorization": "Bearer $var:g/all/api_token"
    }
  }
}
\`\`\`

### Kafka
\`\`\`json
{
  "resource_type": "kafka",
  "value": {
    "brokers": "broker1:9092,broker2:9092",
    "sasl_mechanism": "PLAIN",
    "security_protocol": "SASL_SSL",
    "username": "$var:g/all/kafka_user",
    "password": "$var:g/all/kafka_password"
  }
}
\`\`\`

### NATS
\`\`\`json
{
  "resource_type": "nats",
  "value": {
    "servers": ["nats://localhost:4222"],
    "user": "$var:g/all/nats_user",
    "password": "$var:g/all/nats_password"
  }
}
\`\`\`

### MQTT
\`\`\`json
{
  "resource_type": "mqtt",
  "value": {
    "host": "mqtt.example.com",
    "port": 8883,
    "username": "$var:g/all/mqtt_user",
    "password": "$var:g/all/mqtt_password",
    "tls": true
  }
}
\`\`\`

## Custom Resource Types

Create custom resource types with JSON Schema:

\`\`\`json
{
  "name": "custom_api",
  "schema": {
    "type": "object",
    "properties": {
      "base_url": {"type": "string", "format": "uri"},
      "api_key": {"type": "string"},
      "timeout": {"type": "integer", "default": 30}
    },
    "required": ["base_url", "api_key"]
  },
  "description": "Custom API connection"
}
\`\`\`

Save as: \`custom_api.resource-type.json\`

## OAuth Resources

OAuth resources are managed through the Windmill UI and marked:

\`\`\`json
{
  "is_oauth": true,
  "account": 123
}
\`\`\`

OAuth tokens are automatically refreshed by Windmill.

## Using Resources in Scripts

### TypeScript (Bun/Deno)
\`\`\`typescript
export async function main(db: RT.Postgresql) {
  // db contains the resource values
  const { host, port, user, password, dbname } = db;
}
\`\`\`

### Python
\`\`\`python
class postgresql(TypedDict):
    host: str
    port: int
    user: str
    password: str
    dbname: str

def main(db: postgresql):
    # db contains the resource values
    pass
\`\`\`

## CLI Commands

\`\`\`bash
# List resources
wmill resource list

# List resource types with schemas
wmill resource-type list --schema

# Get specific resource type schema
wmill resource-type get postgresql

# Deploy resources to the workspace — destructive to remote state, so only run when
# the user explicitly asks to deploy/publish/push. Depending on how the repo is wired,
# deploy via \`git push\` or \`wmill sync push\` (see the Deploying section in AGENTS.wmill.md).
wmill sync push
\`\`\`
`,
  "write-workflow-as-code": `---
name: write-workflow-as-code
description: MUST use when writing or modifying Windmill Workflow-as-Code scripts using workflow, task, step, sleep, approvals, taskScript, taskFlow, task_script, or task_flow.
---

## CLI Commands

Place scripts in a folder.

After writing, tell the user which command fits what they want to do:

- \`wmill script preview <script_path>\` — **default when iterating on a local script.** Runs the local file without deploying.
- \`wmill script run <path>\` — runs the script **already deployed** in the workspace. Use only when the user explicitly wants to test the deployed version, not local edits.
- \`wmill generate-metadata\` — regenerate the local \`.script.yaml\` (input schema) and \`.lock\` (resolved dependencies) for scripts you changed, and refresh their content hashes in \`wmill-lock.yaml\`. Local files only — **not** a deploy. See "Keep metadata in sync" below.
- 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 script", "try it", "test it", "does it work" while there are **local edits to the script file**, use \`script preview\`. Do NOT push the script to then \`script run\` it — pushing is a deploy, and deploying just to test overwrites the workspace version with untested changes.

Only use \`script run\` when:
- The user explicitly says "run the deployed version" / "run what's on the server".
- There is no local script being edited (you're just invoking an existing script).

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.

### Keep metadata in sync after editing

\`wmill-lock.yaml\` tracks a content hash for each item. Editing a script's content — most importantly **adding or removing an import** or **changing \`main\`'s arguments** — invalidates that hash and leaves the \`.lock\`, the \`.script.yaml\` input schema, and the hash row out of date. Run \`wmill generate-metadata\` (scoped to what you touched) after such edits so the resolved lock, the auto-generated args UI (driven by \`.script.yaml\`), and \`wmill-lock.yaml\` all match the code. Leaving them stale produces spurious diffs in git-sync and CI.

This only writes local files (it is **not** a deploy), but it re-resolves dependencies, so it can bump unpinned versions (the same as deploying from the UI; expected, not a bug). So by default offer it and run it once the user agrees, rather than running it silently after every edit — unless the project's \`AGENTS.md\` opts into running metadata automatically (see the "Keeping metadata in sync" preference there). Either way YOU run the command, not the user. After running it, diff the regenerated \`.lock\` / \`.script.lock\` files and tell the user which dependency versions changed (e.g. \`requests 2.31.0 → 2.32.0\`), so they can catch an unwanted bump before deploying — even under \`Metadata: auto\`, since it's information, not a confirmation gate. Pin versions in code to keep them fixed.

With no path argument, \`generate-metadata\` regenerates only the items whose content hash drifted — not everything. Imports propagate: editing a script that others import marks every importer stale too, so a one-line change to a shared module can regenerate many locks (by design — their locks must reflect the imported code). If it touches more than you expect, run \`wmill generate-metadata --dry-run\` — it lists each stale item with a reason (\`content changed\` or \`depends on <path>\`) without changing anything — then narrow with a path argument (\`wmill generate-metadata f/foo\`) or \`--strict-folder-boundaries\`.

If the on-disk \`.lock\` and \`.script.yaml\` are already correct and only \`wmill-lock.yaml\` needs its hashes refreshed (hash drift, or bootstrapping missing entries), use \`wmill generate-metadata rehash\` — it re-records hashes from disk with no backend round-trip and no dependency changes.

### After writing — offer to test, don't wait passively

If the user hasn't already told you to run/test/preview the script, offer it as a one-sentence next step (e.g. "Want me to run \`wmill script preview\` with sample args?"). Do not present a multi-option menu.

If the user already asked to test/run/try the script in their original request, skip the offer and just execute \`wmill script preview <path> -d '<args>'\` directly — pick plausible args from the script's declared parameters. The shape varies by language: \`main(...)\` for code languages, the SQL dialect's own placeholder syntax (\`$1\` for PostgreSQL, \`?\` for MySQL/Snowflake, \`@P1\` for MSSQL, \`@name\` for BigQuery, etc.), positional \`$1\`, \`$2\`, … for Bash, \`param(...)\` for PowerShell.

\`wmill script preview\` does not deploy, but it still executes script code and may cause side effects; run it yourself when the user asked to test/preview (or after confirming that execution is intended). \`wmill generate-metadata\` does not deploy either — it only writes local files (locks, schemas, hashes) — but offer it before running (or run automatically if the project's \`AGENTS.md\` opts in), per "Keep metadata in sync" above. Deploying to the workspace (\`git push\` or \`wmill sync push\` depending on how the repo is wired — see the **Deploying** section) is the only step that mutates remote state — do it only when the user explicitly asks to deploy/publish/push.

For a **visual** open-the-script-in-the-dev-page preview (rather than \`script preview\`'s run-and-print-result), use the \`preview\` skill.

Use \`wmill resource-type list --schema\` to discover available resource types.

Workflow-as-Code files use the normal script CLI workflow. There are no separate WAC deploy commands.

# Windmill Workflow-as-Code Writing Guide

## Scope

Use this guide when writing or modifying Windmill Workflow-as-Code (WAC) scripts.
WAC is authored as a Windmill script and deployed with the normal script workflow. It is not an OpenFlow YAML flow.

Supported WAC authoring targets:
- Bun TypeScript scripts that import from \`windmill-client\`
- Python 3 scripts that import from \`wmill\`

## File Shape

Bun TypeScript:

\`\`\`typescript
import {
  task,
  taskScript,
  taskFlow,
  step,
  sleep,
  waitForApproval,
  getResumeUrls,
  parallel,
  workflow,
} from "windmill-client";

const process = task(async (x: string): Promise<string> => {
  return \`processed: \${x}\`;
});

export const main = workflow(async (x: string) => {
  const result = await process(x);
  return { result };
});
\`\`\`

Python:

\`\`\`python
from wmill import task, task_script, task_flow, step, sleep, wait_for_approval, get_resume_urls, parallel, workflow

@task()
async def process(x: str) -> str:
    return f"processed: {x}"

@workflow
async def main(x: str):
    result = await process(x)
    return {"result": result}
\`\`\`

Rules:
- Do not call \`main\`.
- Bun TypeScript should export the workflow entrypoint, preferably \`export const main = workflow(async (...) => { ... })\`.
- Python must use \`@workflow\` on an async top-level function, usually \`main\`.
- Define task functions and \`taskScript\`/\`task_script\` or \`taskFlow\`/\`task_flow\` assignments at module top level with stable names.
- Use the exact SDK names. Do not alias \`workflow\`, \`task\`, \`taskScript\`, \`taskFlow\`, \`step\`, \`sleep\`, \`waitForApproval\`, \`task_script\`, \`task_flow\`, or \`wait_for_approval\`; the WAC parser recognizes these names directly.

## Checkpoint And Replay Model

The parent workflow may rerun from the top after any suspension, retry, approval, or child task completion. Completed durable steps are replayed from the checkpoint.

Put every side effect or non-deterministic value behind a durable WAC boundary:
- Use \`task()\` / \`@task()\` for substantial work that should run as its own child job.
- Use \`taskScript()\` / \`task_script()\` for an existing script or a relative module file.
- Use \`taskFlow()\` / \`task_flow()\` for an existing Windmill flow.
- Use \`step(name, fn)\` for lightweight inline work whose result must be checkpointed.
- Use \`sleep(seconds)\` for server-side sleeps that do not hold a worker.
- Use \`waitForApproval()\` / \`wait_for_approval()\` for external approval suspension.

Never put API calls, database writes, notifications, random values, timestamps, or irreversible changes directly in the top-level workflow body. The workflow body can be rerun. Put those operations in a task or in \`step()\`.

Branching on task or step results is safe because those results are checkpointed. Branching on current time, random data, environment reads, or external state is unsafe unless the value is first captured with \`step()\`.

## Tasks

Use \`task()\` / \`@task()\` for inline functions that become workflow steps:

\`\`\`typescript
const enrich = task(async (customerId: string) => {
  return await fetchCustomer(customerId);
});
\`\`\`

\`\`\`python
@task(timeout=600, tag="etl")
async def enrich(customer_id: str):
    return await fetch_customer(customer_id)
\`\`\`

In TypeScript, prefer assigning each task to a named top-level const. In Python, prefer top-level async functions decorated with \`@task()\` or \`@task\`.

For existing scripts:

\`\`\`typescript
const helper = taskScript("./helper.ts");
const existing = taskScript("f/data/extract", { timeout: 600 });
const value = await helper({ input: x });
\`\`\`

\`\`\`python
helper = task_script("./helper.py")
existing = task_script("f/data/extract", timeout=600)
value = await helper(input=x)
\`\`\`

For existing flows:

\`\`\`typescript
const pipeline = taskFlow("f/etl/pipeline");
const output = await pipeline({ input: data });
\`\`\`

\`\`\`python
pipeline = task_flow("f/etl/pipeline")
output = await pipeline(input=data)
\`\`\`

## Inline Steps

Use \`step()\` for lightweight inline values that must not change during replay:

\`\`\`typescript
const urls = await step("get_urls", () => getResumeUrls());
const startedAt = await step("started_at", () => new Date().toISOString());
\`\`\`

\`\`\`python
urls = await step("get_urls", lambda: get_resume_urls())
\`\`\`

Use stable, descriptive step names. Do not generate step names dynamically.

## Parallelism

To run independent work in parallel, start task promises/coroutines before awaiting them together:

\`\`\`typescript
const [a, b] = await Promise.all([process("a"), process("b")]);
const many = await parallel(items, process, { concurrency: 5 });
\`\`\`

\`\`\`python
import asyncio

a, b = await asyncio.gather(process("a"), process("b"))
many = await parallel(items, process, concurrency=5)
\`\`\`

Only parallelize independent steps. Do not read the result of a task before it is awaited.

## Approvals

Generate resume URLs inside \`step()\` before sending them:

\`\`\`typescript
const urls = await step("get_urls", () => getResumeUrls());
await step("notify", () => sendApprovalEmail(urls.approvalPage));
const approval = await waitForApproval({ timeout: 3600 });
\`\`\`

\`\`\`python
urls = await step("get_urls", lambda: get_resume_urls())
await step("notify", lambda: send_approval_email(urls["approvalPage"]))
approval = await wait_for_approval(timeout=3600)
\`\`\`

\`selfApproval: false\` and \`self_approval=False\` are Enterprise-only approval behavior. Do not use them unless the user asks for that behavior.

## Error Handling

Let task errors fail the workflow unless the user asks for recovery logic.

Python: \`except Exception\` is safe around WAC calls because internal suspension inherits from \`BaseException\`. Avoid bare \`except:\` in workflow code. If the user asks for recovery logic around failed child work, catch \`TaskError\` from \`wmill\` for task failures.

TypeScript: avoid broad \`try/catch\` around WAC SDK calls. The SDK uses an internal suspension error during initial dispatch; catching it can break workflow suspension. If a broad catch is unavoidable, rethrow internal suspension errors before handling business errors.


## TypeScript Workflow-as-Code API (windmill-client)

Import: \`import { workflow, task, taskScript, taskFlow, step, sleep, waitForApproval, getResumeUrls, parallel } from "windmill-client"\`

\`\`\`typescript
export interface TaskOptions {
  timeout?: number;
  tag?: string;
  cache_ttl?: number;
  priority?: number;
  concurrency_limit?: number;
  concurrency_key?: string;
  concurrency_time_window_s?: number;
}

/**
 * Get URLs needed for resuming a flow after this step
 * @param approver approver name
 * @param flowLevel if true, generate resume URLs for the parent flow instead of the specific step.
 *                  This allows pre-approvals that can be consumed by any later suspend step in the same flow.
 * @returns approval page UI URL, resume and cancel API URLs for resuming the flow
 */
export async function getResumeUrls(approver?: string, flowLevel?: boolean): Promise<{ approvalPage: string; resume: string; cancel: string; }>

/**
 * Wrap an async function as a workflow task.
 *
 * @example
 * const extract_data = task(async (url: string) => { ... });
 * const run_external = task("f/external_script", async (x: number) => { ... });
 *
 * Inside a \`workflow()\`, calling a task dispatches it as a step.
 * Outside a workflow, the function body executes directly.
 */
export function task<T extends (...args: any[]) => Promise<any>>(fnOrPath: T | string, maybeFnOrOptions?: T | TaskOptions, maybeOptions?: TaskOptions,): T

/**
 * Create a task that dispatches to a separate Windmill script.
 *
 * @example
 * const extract = taskScript("f/data/extract");
 * // inside workflow: await extract({ url: "https://..." })
 */
export function taskScript(path: string, options?: TaskOptions): (...args: any[]) => PromiseLike<any>

/**
 * Create a task that dispatches to a separate Windmill flow.
 *
 * @example
 * const pipeline = taskFlow("f/etl/pipeline");
 * // inside workflow: await pipeline({ input: data })
 */
export function taskFlow(path: string, options?: TaskOptions): (...args: any[]) => PromiseLike<any>

/**
 * Mark an async function as a workflow-as-code entry point.
 *
 * The function must be **deterministic**: given the same inputs it must call
 * tasks in the same order on every replay. Branching on task results is fine
 * (results are replayed from checkpoint), but branching on external state
 * (current time, random values, external API calls) must use \`step()\` to
 * checkpoint the value so replays see the same result.
 */
export function workflow<T>(fn: (...args: any[]) => Promise<T>)

export async function step<T>(name: string, fn: () => T | Promise<T>): Promise<T>

export async function sleep(seconds: number): Promise<void>

/**
 * Suspend the workflow and wait for an external approval.
 *
 * Use \`getResumeUrls()\` (wrapped in \`step()\`) to obtain resume/cancel/approvalPage
 * URLs before calling this function.
 *
 * @example
 * const urls = await step("urls", () => getResumeUrls());
 * await step("notify", () => sendEmail(urls.approvalPage));
 * const { value, approver } = await waitForApproval({ timeout: 3600 });
 */
export function waitForApproval(options?: { timeout?: number; form?: object; selfApproval?: boolean; }): PromiseLike<{ value: any; approver: string; approved: boolean }>

/**
 * Process items in parallel with optional concurrency control.
 *
 * Each item is processed by calling \`fn(item)\`, which should be a task().
 * Items are dispatched in batches of \`concurrency\` (default: all at once).
 *
 * @example
 * const process = task(async (item: string) => { ... });
 * const results = await parallel(items, process, { concurrency: 5 });
 */
export async function parallel<T, R>(items: T[], fn: (item: T) => PromiseLike<R> | R, options?: { concurrency?: number },): Promise<R[]>
\`\`\`


## Python Workflow-as-Code API (wmill)

Import: \`from wmill import workflow, task, task_script, task_flow, step, sleep, wait_for_approval, get_resume_urls, parallel, TaskError\`

\`\`\`python
# Raised when a WAC task step failed.
#
# Attributes:
#     step_key: The checkpoint key of the failed step.
#     child_job_id: The UUID of the failed child job.
#     result: The error result from the child job.
class TaskError(Exception):
    def __init__(self, message: str, *, step_key: str = '', child_job_id: str = '', result = None)

# Get URLs needed for resuming a flow after suspension.
#
# Args:
#     approver: Optional approver name
#     flow_level: If True, generate resume URLs for the parent flow instead of the
#         specific step. This allows pre-approvals that can be consumed by any later
#         suspend step in the same flow.
#
# Returns:
#     Dictionary with approvalPage, resume, and cancel URLs
def get_resume_urls(approver: str = None, flow_level: bool = None) -> dict

# Decorator that marks a function as a workflow task.
#
# Works in both WAC v1 (sync, HTTP-based dispatch) and WAC v2
# (async, checkpoint/replay) modes:
#
# - **v2 (inside @workflow)**: dispatches as a checkpoint step.
# - **v1 (WM_JOB_ID set, no @workflow)**: dispatches via HTTP API.
# - **Standalone**: executes the function body directly.
#
# Usage::
#
#     @task
#     async def extract_data(url: str): ...
#
#     @task(path="f/external_script", timeout=600, tag="gpu")
#     async def run_external(x: int): ...
def task(_func = None, *, path: Optional[str] = None, tag: Optional[str] = None, timeout: Optional[int] = None, cache_ttl: Optional[int] = None, priority: Optional[int] = None, concurrency_limit: Optional[int] = None, concurrency_key: Optional[str] = None, concurrency_time_window_s: Optional[int] = None)

# Create a task that dispatches to a separate Windmill script.
#
# Usage::
#
#     extract = task_script("f/data/extract", timeout=600)
#
#     @workflow
#     async def main():
#         data = await extract(url="https://...")
def task_script(path: str, *, timeout: Optional[int] = None, tag: Optional[str] = None, cache_ttl: Optional[int] = None, priority: Optional[int] = None, concurrency_limit: Optional[int] = None, concurrency_key: Optional[str] = None, concurrency_time_window_s: Optional[int] = None)

# Create a task that dispatches to a separate Windmill flow.
#
# Usage::
#
#     pipeline = task_flow("f/etl/pipeline", priority=10)
#
#     @workflow
#     async def main():
#         result = await pipeline(input=data)
def task_flow(path: str, *, timeout: Optional[int] = None, tag: Optional[str] = None, cache_ttl: Optional[int] = None, priority: Optional[int] = None, concurrency_limit: Optional[int] = None, concurrency_key: Optional[str] = None, concurrency_time_window_s: Optional[int] = None)

# Decorator marking an async function as a workflow-as-code entry point.
#
# The function must be **deterministic**: given the same inputs it must call
# tasks in the same order on every replay. Branching on task results is fine
# (results are replayed from checkpoint), but branching on external state
# (current time, random values, external API calls) must use \`\`step()\`\` to
# checkpoint the value so replays see the same result.
def workflow(func)

# Execute \`\`fn\`\` inline and checkpoint the result.
#
# On replay the cached value is returned without re-executing \`\`fn\`\`.
# Use for lightweight deterministic operations (timestamps, random IDs,
# config reads) that should not incur the overhead of a child job.
async def step(name: str, fn)

# Server-side sleep — suspend the workflow for the given duration without holding a worker.
#
# Inside a @workflow, the parent job suspends and auto-resumes after \`\`seconds\`\`.
# Outside a workflow, falls back to \`\`asyncio.sleep\`\`.
async def sleep(seconds: int)

# Suspend the workflow and wait for an external approval.
#
# Use \`\`get_resume_urls()\`\` (wrapped in \`\`step()\`\`) to obtain
# resume/cancel/approval URLs before calling this function.
#
# Returns a dict with \`\`value\`\` (form data), \`\`approver\`\`, and \`\`approved\`\`.
#
# Args:
#     timeout: Approval timeout in seconds (default 1800).
#     form: Optional form schema for the approval page.
#     self_approval: Whether the user who triggered the flow can approve it (default True).
#
# Example::
#
#     urls = await step("urls", lambda: get_resume_urls())
#     await step("notify", lambda: send_email(urls["approvalPage"]))
#     result = await wait_for_approval(timeout=3600)
async def wait_for_approval(timeout: int = 1800, form: dict | None = None, self_approval: bool = True) -> dict

# Process items in parallel with optional concurrency control.
#
# Each item is processed by calling \`\`fn(item)\`\`, which should be a @task.
# Items are dispatched in batches of \`\`concurrency\`\` (default: all at once).
#
# Example::
#
#     @task
#     async def process(item: str):
#         ...
#
#     results = await parallel(items, process, concurrency=5)
async def parallel(items, fn, *, concurrency: Optional[int] = None)
\`\`\`
`,
  "cli-commands": `---
name: cli-commands
description: MUST use when using the CLI, including debugging job failures and inspecting run history via \`wmill job\`.
---

# Windmill CLI Commands

The Windmill CLI (\`wmill\`) provides commands for managing scripts, flows, apps, and other resources.

## Global Options

- \`--workspace <workspace:string>\` - Specify the target workspace. This overrides the default workspace.
- \`--debug --verbose\` - Show debug/verbose logs
- \`--show-diffs\` - Show diff informations when syncing (may show sensitive informations)
- \`--token <token:string>\` - Specify an API token. This will override any stored token.
- \`--base-url <baseUrl:string>\` - Specify the base URL of the API. If used, --token and --workspace are required and no local remote/workspace already set will be used.
- \`--config-dir <configDir:string>\` - Specify a custom config directory. Overrides WMILL_CONFIG_DIR environment variable and default ~/.config location.

## Commands

### app

app related commands

**Options:**
- \`--json\` - Output as JSON (for piping to jq)

**Subcommands:**

- \`app list\` - list all apps
  - \`--json\` - Output as JSON (for piping to jq)
- \`app get <path:string>\` - get an app's details
  - \`--json\` - Output as JSON (for piping to jq)
- \`app push [file_path:string] [remote_path:string]\` - push a local app. With no args, infers the app from the current directory and the remote path from its location relative to wmill.yaml.
- \`app dev [app_folder:string]\` - Start a development server for building apps with live reload and hot module replacement
  - \`--port <port:number>\` - Port to run the dev server on (will find next available port if occupied)
  - \`--host <host:string>\` - Host to bind the dev server to
  - \`--entry <entry:string>\` - Entry point file (default: index.ts for Svelte/Vue, index.tsx otherwise)
  - \`--no-open\` - Don't automatically open the browser
- \`app lint [app_folder:string]\` - Lint a raw app folder to validate structure and buildability
  - \`--fix\` - Attempt to fix common issues (not implemented yet)
- \`app new\` - create a new raw app from a template
  - \`--summary <summary:string>\` - App summary (short description). Skips the prompt when provided. Triggers non-interactive mode.
  - \`--path <path:string>\` - App path (e.g., f/folder/my_app or u/username/my_app). Skips the prompt when provided. Triggers non-interactive mode.
  - \`--framework <framework:string>\` - Framework template: react19 | react18 | svelte5 | vue. Skips the prompt when provided. Triggers non-interactive mode.
  - \`--datatable <datatable:string>\` - Datatable to wire up. Without this flag in non-interactive mode, no datatable is configured.
  - \`--schema <schema:string>\` - Schema to use with --datatable. Created (CREATE SCHEMA IF NOT EXISTS) if it doesn't already exist.
  - \`--overwrite\` - Overwrite the target directory if it already exists, without prompting.
  - \`--no-open-in-desktop\` - Do not prompt to open the new app in Claude Desktop.
- \`app generate-agents [app_folder:string]\` - regenerate AGENTS.md and DATATABLES.md from remote workspace
- \`app set-permissioned-as <path:string> <email:string>\` - Set the on_behalf_of_email for an app (requires admin or wm_deployers group)

### audit

View audit logs (requires admin)

**Subcommands:**

- \`audit list\` - List audit log entries
- \`audit get <id:string>\` - Get a specific audit log entry
  - \`--json\` - Output as JSON (for piping to jq)

### config

Show all available wmill.yaml configuration options

**Options:**
- \`--json\` - Output as JSON for programmatic consumption

**Subcommands:**

- \`config migrate\` - Migrate wmill.yaml from gitBranches/environments to workspaces format

### datatable

datatable related commands

**Subcommands:**

- \`datatable list\` - list all datatables in the workspace
  - \`--json\` - Output as JSON (for piping to jq)
- \`datatable run <sql:string>\` - run a SQL query on a datatable
  - \`-n --name <name:string>\` - Datatable name (default: main)
  - \`-s --silent\` - Output only the final result as JSON. Useful for scripting.
- \`datatable migrate\` - manage datatable migrations
  - \`datatable migrate new <name:string>\` - scaffold a new migration (.up.sql / .down.sql files)
    - \`-d --datatable <datatable:string>\` - Target datatable (default: main)
  - \`datatable migrate up\` - apply all pending migrations to the main datatable (or one via --datatable)
    - \`-d --datatable <datatable:string>\` - Target datatable (default: main)
  - \`datatable migrate down\` - roll back the most recent migration on the main datatable (or one via --datatable)
    - \`-d --datatable <datatable:string>\` - Target datatable (default: main)
- \`datatable create [name:string]\` - register a datatable database in the workspace (default: instance-backed 'main') so scripts can use datatable://<name>
  - \`--resource <resource:string>\` - Back the datatable with an existing postgresql resource path instead of the instance database
  - \`--force\` - Allow adding to a workspace that already has datatables (fork metadata on existing ones is not preserved)
- \`datatable serve\` - Serve all datatables as a Postgres-wire endpoint (psql, DBeaver, pgAdmin); the client picks the datatable via the database name in its connection string
  - \`--port <port:number>\` - Port to listen on (default: first free port in 5433-5500)
  - \`--host <host:string>\` - Bind address (default: 127.0.0.1)
  - \`--password <password:string>\` - Password for Postgres clients (default: generate a random password at startup)
- \`datatable psql\` - Start a serve listener and launch psql connected to it
  - \`-n --name <name:string>\` - Datatable to connect psql to (default: main)
  - \`--port <port:number>\` - Port the proxy listens on (default: first free port in 5433-5500)
  - \`--host <host:string>\` - Bind address for the proxy (default: 127.0.0.1)
  - \`--password <password:string>\` - Password for the temporary Postgres proxy (default: generate a random password at startup)

### dependencies

workspace dependencies related commands

**Alias:** \`deps\`

**Subcommands:**

- \`dependencies push <file_path:string>\` - Push workspace dependencies from a local file

### dev

Watch local file changes and live-reload the dev page for preview. Does NOT deploy to the remote workspace — use wmill sync push for that.

**Options:**
- \`--includes <pattern...:string>\` - Filter paths given a glob pattern or path
- \`--proxy-port <port:number>\` - Port for a localhost reverse proxy to the remote Windmill server
- \`--path <path:string>\` - Watch a specific windmill path (e.g., u/admin/my_script or f/my_flow)
- \`--no-open\` - Do not open the browser automatically

### docs

Search Windmill documentation.

**Arguments:** \`<query:string>\`

**Options:**
- \`--json\` - Output results as JSON.

### ducklake

ducklake related commands

**Subcommands:**

- \`ducklake list\` - list all ducklakes in the workspace
  - \`--json\` - Output as JSON (for piping to jq)
- \`ducklake run <sql:string>\` - run a SQL query on a ducklake
  - \`-n --name <name:string>\` - Ducklake name (default: main)
  - \`-s --silent\` - Output only the final result as JSON. Useful for scripting.

### flow

flow related commands

**Options:**
- \`--show-archived\` - Enable archived flows in output
- \`--json\` - Output as JSON (for piping to jq)

**Subcommands:**

- \`flow list\` - list all flows
  - \`--show-archived\` - Enable archived flows in output
  - \`--json\` - Output as JSON (for piping to jq)
- \`flow get <path:string>\` - get a flow's details
  - \`--json\` - Output as JSON (for piping to jq)
- \`flow push <file_path:string> <remote_path:string>\` - push a local flow spec. This overrides any remote versions.
  - \`--message <message:string>\` - Deployment message
- \`flow run <path:string>\` - run a flow by path.
  - \`-d --data <data:string>\` - Inputs specified as a JSON string or a file using @<filename> or stdin using @-.
  - \`-s --silent\` - Do not ouput anything other then the final output. Useful for scripting.
  - \`--tag <tag:string>\` - Override the worker tag the run is dispatched to (e.g. to route it to dev workers instead of the flow's default tag).
- \`flow preview <flow_path:string>\` - preview a local flow without deploying it. Runs the flow definition from local files and uses local PathScripts by default. Pass --step <id> to run only one module in isolation (resolves nested steps inside branchone/branchall/forloopflow/whileloopflow plus the special preprocessor/failure modules; supported step types: rawscript, script, flow).
  - \`-d --data <data:string>\` - Inputs specified as a JSON string or a file using @<filename> or stdin using @-.
  - \`-s --silent\` - Do not output anything other then the final output. Useful for scripting.
  - \`--remote\` - Use deployed workspace scripts for PathScript steps instead of local files.
  - \`--step <step_id:string>\` - Run only the named step instead of the whole flow. Honors --data as the step's args and --remote / local-PathScript resolution the same way the full-flow preview does.
  - \`--tag <tag:string>\` - Override the worker tag the preview is dispatched to (e.g. to route it to dev workers instead of the flow's default tag).
- \`flow new <flow_path:string>\` - create a new empty flow
  - \`--summary <summary:string>\` - flow summary
  - \`--description <description:string>\` - flow description
- \`flow bootstrap <flow_path:string>\` - create a new empty flow (alias for new)
  - \`--summary <summary:string>\` - flow summary
  - \`--description <description:string>\` - flow description
- \`flow history <path:string>\` - Show version history for a flow
  - \`--json\` - Output as JSON (for piping to jq)
- \`flow show-version <path:string> <version:string>\` - Show a specific version of a flow
  - \`--json\` - Output as JSON (for piping to jq)
- \`flow set-permissioned-as <path:string> <email:string>\` - Set the on_behalf_of_email for a flow (requires admin or wm_deployers group)

### folder

folder related commands

**Options:**
- \`--json\` - Output as JSON (for piping to jq)

**Subcommands:**

- \`folder list\` - list all folders
  - \`--json\` - Output as JSON (for piping to jq)
- \`folder get <name:string>\` - get a folder's details
  - \`--json\` - Output as JSON (for piping to jq)
- \`folder new <name:string>\` - create a new folder locally
  - \`--summary <summary:string>\` - folder summary
- \`folder push <name:string>\` - push a local folder to the remote by name. This overrides any remote versions.
- \`folder add-missing\` - create default folder.meta.yaml for all subdirectories of f/ that are missing one
  - \`-y, --yes\` - skip confirmation prompt
- \`folder show-rules <name:string>\` - Show default_permissioned_as rules for a folder. Use --test-path to see which rule matches a given item path.
  - \`--test-path <path:string>\` - Test which rule matches this item path (e.g. f/prod/jobs/my_script)
  - \`--json\` - Output as JSON

### generate-metadata

Regenerate stale local locks and script schemas and refresh wmill-lock.yaml content hashes (scripts, flows, apps). Writes local files only, not a deploy. Run it after edits that add or remove imports or change a script's arguments, so the lock, the auto-generated UI schema, and wmill-lock.yaml stay in sync.

**Arguments:** \`[folder:string]\`

**Options:**
- \`--yes\` - Skip confirmation prompt
- \`--dry-run\` - Show what would be updated without making changes
- \`--lock-only\` - Re-generate only the lock files
- \`--schema-only\` - Re-generate only script schemas (skips flows and apps)
- \`--skip-scripts\` - Skip processing scripts
- \`--skip-flows\` - Skip processing flows
- \`--skip-apps\` - Skip processing apps
- \`--strict-folder-boundaries\` - Only update items inside the specified folder (requires folder argument)
- \`--parallel <n:number>\` - Number of items to process in parallel
- \`-i --includes <patterns:file[]>\` - Comma separated patterns to specify which files to include
- \`-e --excludes <patterns:file[]>\` - Comma separated patterns to specify which files to exclude

**Subcommands:**

- \`generate-metadata rehash [folder:string]\` - Refresh wmill-lock.yaml content hashes from the on-disk .lock and .script.yaml without re-resolving dependencies or hitting the backend. Use when those files are already correct and only the hashes need updating: bootstrapping missing entries or recovering from hash drift.
  - \`--skip-scripts\` - Skip processing scripts
  - \`--skip-flows\` - Skip processing flows
  - \`--skip-apps\` - Skip processing apps
  - \`--parallel <n:number>\` - Number of items to process in parallel
  - \`-i --includes <patterns:file[]>\` - Comma separated patterns to specify which files to include
  - \`-e --excludes <patterns:file[]>\` - Comma separated patterns to specify which files to exclude

### gitsync-settings

Manage git-sync settings between local wmill.yaml and Windmill backend

**Subcommands:**

- \`gitsync-settings pull\` - Pull git-sync settings from Windmill backend to local wmill.yaml
  - \`--repository <repo:string>\` - Specify repository path (e.g., u/user/repo)
  - \`--default\` - Write settings to top-level defaults instead of overrides
  - \`--replace\` - Replace existing settings (non-interactive mode)
  - \`--override\` - Add branch-specific override (non-interactive mode)
  - \`--diff\` - Show differences without applying changes
  - \`--json-output\` - Output in JSON format
  - \`--with-backend-settings <json:string>\` - Use provided JSON settings instead of querying backend (for testing)
  - \`--yes\` - Skip interactive prompts and use default behavior
  - \`--promotion <branch:string>\` - Use promotionOverrides from the specified branch instead of regular overrides
- \`gitsync-settings push\` - Push git-sync settings from local wmill.yaml to Windmill backend
  - \`--repository <repo:string>\` - Specify repository path (e.g., u/user/repo)
  - \`--diff\` - Show what would be pushed without applying changes
  - \`--json-output\` - Output in JSON format
  - \`--with-backend-settings <json:string>\` - Use provided JSON settings instead of querying backend (for testing)
  - \`--yes\` - Skip interactive prompts and use default behavior
  - \`--promotion <branch:string>\` - Use promotionOverrides from the specified branch instead of regular overrides
- \`gitsync-settings status\` - Report how local changes deploy to the workspace (git push vs wmill sync push)
  - \`--json-output\` - Output in JSON format

### group

Manage workspace groups

**Options:**
- \`--json\` - Output as JSON (for piping to jq)

**Subcommands:**

- \`group list\` - List all groups in the workspace
  - \`--json\` - Output as JSON (for piping to jq)
- \`group get <name:string>\` - Get group details and members
  - \`--json\` - Output as JSON (for piping to jq)
- \`group create <name:string>\` - Create a new group
  - \`--summary <summary:string>\` - Group summary/description
- \`group delete <name:string>\` - Delete a group
- \`group add-user <name:string> <username:string>\` - Add a user to a group
- \`group remove-user <name:string> <username:string>\` - Remove a user from a group

### hub

Hub related commands. EXPERIMENTAL. INTERNAL USE ONLY.

**Subcommands:**

- \`hub pull\` - pull any supported definitions. EXPERIMENTAL.

### init

Bootstrap a windmill project with a wmill.yaml file

**Options:**
- \`--use-default\` - Use default settings without checking backend
- \`--use-backend\` - Use backend git-sync settings if available
- \`--repository <repo:string>\` - Specify repository path (e.g., u/user/repo) when using backend settings
- \`--bind-profile\` - Automatically bind active workspace profile to current Git branch
- \`--no-bind-profile\` - Skip workspace profile binding prompt

### instance

sync local with a remote instance or the opposite (push or pull)

**Subcommands:**

- \`instance add [instance_name:string] [remote:string] [token:string]\` - Add a new instance
- \`instance remove <instance:string:instance>\` - Remove an instance
- \`instance switch <instance:string:instance>\` - Switch the current instance
- \`instance pull\` - Pull instance settings, users, configs, instance groups and overwrite local
  - \`--yes\` - Pull without needing confirmation
  - \`--dry-run\` - Perform a dry run without making changes
  - \`--skip-users\` - Skip pulling users
  - \`--skip-settings\` - Skip pulling settings
  - \`--skip-configs\` - Skip pulling configs (worker groups)
  - \`--skip-groups\` - Skip pulling instance groups
  - \`--include-workspaces\` - Also pull workspaces
  - \`--folder-per-instance\` - Create a folder per instance
  - \`--instance <instance:string>\` - Name of the instance to pull from, override the active instance
  - \`--prefix <prefix:string>\` - Prefix of the local workspaces to pull, used to create the folders when using --include-workspaces
  - \`--prefix-settings\` - Store instance yamls inside prefixed folders when using --prefix and --folder-per-instance
- \`instance push\` - Push instance settings, users, configs, group and overwrite remote
  - \`--yes\` - Push without needing confirmation
  - \`--dry-run\` - Perform a dry run without making changes
  - \`--skip-users\` - Skip pushing users
  - \`--skip-settings\` - Skip pushing settings
  - \`--skip-configs\` - Skip pushing configs (worker groups)
  - \`--skip-groups\` - Skip pushing instance groups
  - \`--include-workspaces\` - Also push workspaces
  - \`--folder-per-instance\` - Create a folder per instance
  - \`--instance <instance:string>\` - Name of the instance to push to, override the active instance
  - \`--prefix <prefix:string>\` - Prefix of the local workspaces folders to push
  - \`--prefix-settings\` - Store instance yamls inside prefixed folders when using --prefix and --folder-per-instance
- \`instance whoami\` - Display information about the currently logged-in user
- \`instance get-config\` - Dump the current instance config (global settings + worker configs) as YAML
  - \`-o, --output-file <file:string>\` - Write YAML to a file instead of stdout
  - \`--show-secrets\` - Include sensitive fields (license key, JWT secret) without prompting
  - \`--instance <instance:string>\` - Name of the instance, override the active instance
- \`instance connect-slack\` - Non-interactively connect Slack at the instance level using a pre-minted bot token (xoxb-...). Produces the same artifacts as the UI OAuth flow: global_settings 'slack' row + encrypted f/slack_bot/global_bot_token variable and resource in the admins workspace.
  - \`--bot-token <bot_token:string>\` - Slack bot token (xoxb-...)
  - \`--team-id <team_id:string>\` - Slack team id
  - \`--team-name <team_name:string>\` - Slack team name
  - \`--instance <instance:string>\` - Instance profile to connect against (defaults to the active instance)

### job

Manage jobs (list, inspect, cancel)

**Subcommands:**

- \`job list\` - List recent jobs
- \`job get <id:string>\` - Get job details. For flows: shows step tree with sub-job IDs
  - \`--json\` - Output as JSON (for piping to jq)
- \`job result <id:string>\` - Get the result of a completed job (machine-friendly)
- \`job logs <id:string>\` - Get job logs. For flows: aggregates all step logs
- \`job cancel <id:string>\` - Cancel a running or queued job
  - \`--reason <reason:string>\` - Reason for cancellation
- \`job rerun <id:string>\` - Re-run a completed job with the same args. Prints the new job UUID on stdout.
- \`job restart <id:string>\` - Restart a completed flow at a given top-level step. Prints the new flow job UUID on stdout.
  - \`--step <stepId:string>\` - Top-level step id to restart the flow from
  - \`--iteration <n:number>\` - For a top-level branchall or for-loop step, the iteration to restart at

### jobs

Manage jobs (import/export)

**Subcommands:**

- \`jobs pull [workspace:string]\` - Pull completed and queued jobs from workspace
  - \`-c, --completed-output <file:string>\` - Completed jobs output file (default: completed_jobs.json)
  - \`-q, --queued-output <file:string>\` - Queued jobs output file (default: queued_jobs.json)
  - \`--skip-worker-check\` - Skip checking for active workers before export
- \`jobs push [workspace:string]\` - Push completed and queued jobs to workspace
  - \`-c, --completed-file <file:string>\` - Completed jobs input file (default: completed_jobs.json)
  - \`-q, --queued-file <file:string>\` - Queued jobs input file (default: queued_jobs.json)
  - \`--skip-worker-check\` - Skip checking for active workers before import

### lint

Validate Windmill flow, schedule, and trigger YAML files in a directory

**Arguments:** \`[directory:string]\`

**Options:**
- \`--json\` - Output results in JSON format
- \`--fail-on-warn\` - Exit with code 1 when warnings are emitted
- \`--locks-required\` - Fail if scripts or flow inline scripts that need locks have no locks
- \`-w, --watch\` - Watch for file changes and re-lint automatically

### object-storage

Object storage (S3) related commands. Operates on the workspace's default object storage; use --storage to target a configured secondary storage.

**Alias:** \`s3\`

**Subcommands:**

- \`object-storage list\` - List configured object storages for the workspace (default + secondary).
  - \`--json\` - Output as JSON (for piping to jq)
- \`object-storage files [prefix:string]\` - List files in an object storage. Optionally filter by prefix.
  - \`--json\` - Output as JSON (for piping to jq)
  - \`--max-keys <maxKeys:number>\` - Page size (default 100)
  - \`--marker <marker:string>\` - Pagination marker from a previous response
  - \`--storage <storage:string>\` - Secondary storage name (omit for the workspace default)
- \`object-storage upload <local_path:string> <file_key:string>\` - Upload a local file to object storage at the given file key.
  - \`--storage <storage:string>\` - Secondary storage name
  - \`--content-type <contentType:string>\` - Content-Type header to set on the object
  - \`--content-disposition <contentDisposition:string>\` - Content-Disposition header to set on the object
- \`object-storage download <file_key:string> [output_path:string]\` - Download an object to a local file (or stdout). Default output path is the basename of the file key in the current directory.
  - \`--storage <storage:string>\` - Secondary storage name
  - \`--stdout\` - Write file contents to stdout instead of a file
- \`object-storage delete <file_key:string>\` - Delete an object from object storage. Prompts for confirmation unless --yes is set.
  - \`--storage <storage:string>\` - Secondary storage name
  - \`--yes\` - Skip the confirmation prompt
- \`object-storage move <src_file_key:string> <dest_file_key:string>\` - Move an object within the same storage (rename or relocate by key).
  - \`--storage <storage:string>\` - Secondary storage name
- \`object-storage info <file_key:string>\` - Show metadata (size, mime, last-modified) for an object.
  - \`--json\` - Output as JSON (for piping to jq)
  - \`--storage <storage:string>\` - Secondary storage name
- \`object-storage preview <file_key:string>\` - Preview the contents of an object (text/CSV). Use --bytes-from / --bytes-length to peek at a slice of binary files.
  - \`--storage <storage:string>\` - Secondary storage name
  - \`--mime <mime:string>\` - Override the detected mime type (e.g. text/csv)
  - \`--bytes-from <bytesFrom:number>\` - Start offset in bytes
  - \`--bytes-length <bytesLength:number>\` - Number of bytes to read
  - \`--csv-separator <csvSeparator:string>\` - CSV column separator (default ,)
  - \`--csv-header\` - Treat the first CSV row as a header

### pipeline

inspect asset-driven pipelines (scripts marked \`// pipeline\`, wired by \`// on <spec>\` annotations)

**Subcommands:**

- \`pipeline list\` - list pipeline folders in the workspace
  - \`--json\` - Output as JSON (for piping to jq)
- \`pipeline show <folder:string>\` - render a pipeline folder's DAG (sources, lineage, subscriptions) in the terminal
  - \`--json\` - Output the raw asset graph as JSON
  - \`--local\` - Build the graph from local working-tree files (// pipeline scripts) instead of the deployed workspace — no deploy needed.
- \`pipeline run <folder:string>\` - run a cascade: from --from (a root OR any mid-DAG model), fan downstream up to the --to end node(s)
  - \`--from <script:string>\` - Start script (short name or path). May be any node, including a mid-DAG model — that node plus its transitive downstream runs, upstream is NOT re-run (dbt \`--select model+\`). Defaults to the folder's sole schedule/manual root.
  - \`--to <node:string>\` - End node(s) to stop at — script names/paths or asset URIs (e.g. datatable://main/staged). Repeatable or comma-separated. Omit to run the full downstream.
  - \`--dry-run\` - Print the topological run plan without executing.
  - \`--json\` - Output the plan as JSON (for piping to jq).
  - \`--local\` - Run the local working-tree scripts via preview (no deploy) instead of the deployed versions; the graph is built from local files.
  - \`--upload <binding:string>\` - Bind an object to a data_upload/webhook entry point so it runs in the cascade, as SCRIPT[:PARAM]=SOURCE (SOURCE is a local file or an s3://key). Local files are uploaded to the workspace store; the S3Object param is inferred when the script has exactly one. Repeatable.
  - \`--arg <binding:string>\` - Pass a plain run arg to a script in the cascade, as SCRIPT:PARAM=VALUE (VALUE is parsed as JSON when possible, else taken as a string — e.g. daily_report:partition=2026-07-02). Repeatable.
  - \`--partition <value:string>\` - Partition value for \`// partitioned\` scripts in the run (e.g. 2026-06-30) — use it to backfill a past slice. With --local, time kinds (daily/hourly/weekly/monthly) default to the current UTC period when omitted; \`dynamic\` always needs it. Deployed runs without it defer to backend run-start resolution.
- \`pipeline docs <folder:string>\` - generate PIPELINE.md (+ AGENTS.md pointer) describing a folder's pipeline graph and datatable schemas, for an editor / agentic loop
  - \`--local\` - Build the graph from local working-tree files instead of the deployed workspace.
- \`pipeline dev [folder:string]\` - Live-preview a data pipeline from local files: watch an \`f/<folder>\` of \`// pipeline\` scripts, push the working-tree graph to the dev page, and run the cascade via preview (no deploy).
  - \`--port <port:number>\` - Port for the dev WebSocket server.
  - \`--no-open\` - Do not open the browser automatically.
  - \`--frontend <origin:string>\` - Origin serving the /pipeline_dev page (e.g. http://localhost:3000 for a locally-run frontend). Defaults to the workspace remote; use it when the remote's deployed frontend predates the dev page.

### protection-rules

Sync workspace protection rules between protection-rules.yaml and Windmill. The file is keyed by workspace name; keys must match wmill.yaml 'workspaces'.

**Subcommands:**

- \`protection-rules pull [workspace:string]\` - Pull protection rules from Windmill into protection-rules.yaml for a workspace
  - \`--all\` - Pull every workspace defined in wmill.yaml
  - \`--dry-run\` - Show what would change without writing the file
  - \`--json-output\` - Output in JSON format
- \`protection-rules push [workspace:string]\` - Push protection rules from protection-rules.yaml to Windmill for a workspace (full reconcile: creates, updates, and deletes)
  - \`--all\` - Push every workspace defined in protection-rules.yaml
  - \`--dry-run\` - Show what would change without applying
  - \`--json-output\` - Output in JSON format
  - \`--yes\` - Skip the confirmation prompt (including deletions)

### queues

List all queues with their metrics

**Arguments:** \`[workspace:string] the optional workspace to filter by (default to all workspaces)\`

**Options:**
- \`--instance [instance]\` - Name of the instance to push to, override the active instance
- \`--base-url [baseUrl]\` - If used with --token, will be used as the base url for the instance

### refresh

Refresh wmill-managed project files (AGENTS.wmill.md, skills, tsconfig.wmill.json)

**Subcommands:**

- \`refresh prompts\` - Refresh AGENTS.wmill.md and managed skills. User-owned AGENTS.md and CLAUDE.md are never overwritten unless you opt in.
  - \`--yes\` - Non-interactive: append the @AGENTS.wmill.md include to an existing AGENTS.md / CLAUDE.md without prompting. Without it, a non-interactive run leaves an unlinked file untouched.
- \`refresh tsconfig\` - Refresh the wmill-managed tsconfig.wmill.json (and Deno import map for Deno projects)
  - \`--yes\` - Non-interactive: wire an existing custom tsconfig.json/deno.json to the managed file without prompting (a previously-generated config is always migrated automatically).

### resource

resource related commands

**Options:**
- \`--json\` - Output as JSON (for piping to jq)

**Subcommands:**

- \`resource list\` - list all resources
  - \`--json\` - Output as JSON (for piping to jq)
- \`resource get <path:string>\` - get a resource's details
  - \`--json\` - Output as JSON (for piping to jq)
- \`resource new <path:string>\` - create a new resource locally
- \`resource push <file_path:string> <remote_path:string>\` - push a local resource spec. This overrides any remote versions.

### resource-type

resource type related commands

**Options:**
- \`--json\` - Output as JSON (for piping to jq)

**Subcommands:**

- \`resource-type list\` - list all resource types
  - \`--schema\` - Show schema in the output
  - \`--json\` - Output as JSON (for piping to jq)
- \`resource-type get <path:string>\` - get a resource type's details
  - \`--json\` - Output as JSON (for piping to jq)
- \`resource-type new <name:string>\` - create a new resource type locally
- \`resource-type push <file_path:string> <name:string>\` - push a local resource spec. This overrides any remote versions.
- \`resource-type generate-namespace\` - Create a TypeScript definition file with the RT namespace generated from the resource types

### schedule

schedule related commands

**Options:**
- \`--json\` - Output as JSON (for piping to jq)

**Subcommands:**

- \`schedule list\` - list all schedules
  - \`--json\` - Output as JSON (for piping to jq)
- \`schedule get <path:string>\` - get a schedule's details
  - \`--json\` - Output as JSON (for piping to jq)
- \`schedule new <path:string>\` - create a new schedule locally
- \`schedule push <file_path:string> <remote_path:string>\` - push a local schedule spec. This overrides any remote versions.
- \`schedule enable <path:string>\` - Enable a schedule
  - \`--force\` - Bypass the fork-conflict warning when the parent workspace has the same schedule (acknowledges that both crons will fire)
- \`schedule disable <path:string>\` - Disable a schedule
- \`schedule set-permissioned-as <path:string> <email:string>\` - Set the email (run-as user) for a schedule (requires admin or wm_deployers group)

### script

script related commands

**Options:**
- \`--show-archived\` - Show archived scripts instead of active ones
- \`--json\` - Output as JSON (for piping to jq)

**Subcommands:**

- \`script list\` - list all scripts
  - \`--show-archived\` - Show archived scripts instead of active ones
  - \`--json\` - Output as JSON (for piping to jq)
- \`script push <path:file>\` - push a local script spec. This overrides any remote versions. Use the script file (.ts, .js, .py, .sh)
  - \`--message <message:string>\` - Deployment message
- \`script get <path:file>\` - get a script's details
  - \`--json\` - Output as JSON (for piping to jq)
- \`script show <path:file>\` - show a script's content (alias for get)
- \`script run <path:file>\` - run a script by path
  - \`-d --data <data:file>\` - Inputs specified as a JSON string or a file using @<filename> or stdin using @-.
  - \`-s --silent\` - Do not output anything other then the final output. Useful for scripting.
  - \`--tag <tag:string>\` - Override the worker tag the run is dispatched to (e.g. to route it to dev workers instead of the script's default tag).
- \`script preview <path:file>\` - preview a local script without deploying it. Supports both regular and codebase scripts.
  - \`-d --data <data:file>\` - Inputs specified as a JSON string or a file using @<filename> or stdin using @-.
  - \`-s --silent\` - Do not output anything other than the final output. Useful for scripting.
  - \`--tag <tag:string>\` - Override the worker tag the preview is dispatched to (e.g. to route it to dev workers instead of the script's default tag).
- \`script new <path:file> <language:string>\` - create a new script
  - \`--summary <summary:string>\` - script summary
  - \`--description <description:string>\` - script description
- \`script bootstrap <path:file> <language:string>\` - create a new script (alias for new)
  - \`--summary <summary:string>\` - script summary
  - \`--description <description:string>\` - script description
- \`script set-permissioned-as <path:string> <email:string>\` - Set the on_behalf_of_email for a script (requires admin or wm_deployers group)
- \`script history <path:string>\` - show version history for a script
  - \`--json\` - Output as JSON (for piping to jq)

### sync

sync local with a remote workspaces or the opposite (push or pull)

**Subcommands:**

- \`sync pull\` - Pull any remote changes and apply them locally.
  - \`--yes\` - Pull without needing confirmation
  - \`--dry-run\` - Show changes that would be pulled without actually pushing
  - \`--plain-secrets\` - Pull secrets as plain text
  - \`--json\` - Use JSON instead of YAML
  - \`--skip-variables\` - Skip syncing variables (including secrets)
  - \`--skip-secrets\` - Skip syncing only secrets variables
  - \`--include-secrets\` - Include secrets in sync (overrides skipSecrets in wmill.yaml)
  - \`--skip-resources\` - Skip syncing  resources
  - \`--skip-resource-types\` - Skip syncing  resource types
  - \`--skip-scripts\` - Skip syncing scripts
  - \`--skip-flows\` - Skip syncing flows
  - \`--skip-apps\` - Skip syncing apps
  - \`--skip-folders\` - Skip syncing folders
  - \`--skip-workspace-dependencies\` - Skip syncing workspace dependencies
  - \`--skip-scripts-metadata\` - Skip syncing scripts metadata, focus solely on logic
  - \`--include-schedules\` - Include syncing  schedules
  - \`--include-triggers\` - Include syncing triggers
  - \`--include-users\` - Include syncing users
  - \`--include-groups\` - Include syncing groups
  - \`--include-settings\` - Include syncing workspace settings
  - \`--include-key\` - Include workspace encryption key
  - \`--skip-branch-validation\` - Skip git branch validation and prompts
  - \`--json-output\` - Output results in JSON format
  - \`-i --includes <patterns:file[]>\` - Comma separated patterns to specify which file to take into account (among files that are compatible with windmill). Patterns can include * (any string until '/') and ** (any string). Overrides wmill.yaml includes
  - \`-e --excludes <patterns:file[]>\` - Comma separated patterns to specify which file to NOT take into account. Overrides wmill.yaml excludes
  - \`--extra-includes <patterns:file[]>\` - Comma separated patterns to specify which file to take into account (among files that are compatible with windmill). Patterns can include * (any string until '/') and ** (any string). Useful to still take wmill.yaml into account and act as a second pattern to satisfy
  - \`--repository <repo:string>\` - Specify repository path (e.g., u/user/repo) when multiple repositories exist
  - \`--promotion <branch:string>\` - Use promotionOverrides from the specified branch instead of regular overrides
  - \`--branch, --env <branch:string>\` - [Deprecated: use --workspace] Override the current git branch/environment
- \`sync push\` - Push any local changes and apply them remotely.
  - \`--yes\` - Push without needing confirmation
  - \`--dry-run\` - Show changes that would be pushed without actually pushing
  - \`--plain-secrets\` - Push secrets as plain text
  - \`--json\` - Use JSON instead of YAML
  - \`--skip-variables\` - Skip syncing variables (including secrets)
  - \`--skip-secrets\` - Skip syncing only secrets variables
  - \`--include-secrets\` - Include secrets in sync (overrides skipSecrets in wmill.yaml)
  - \`--skip-resources\` - Skip syncing  resources
  - \`--skip-resource-types\` - Skip syncing  resource types
  - \`--skip-scripts\` - Skip syncing scripts
  - \`--skip-flows\` - Skip syncing flows
  - \`--skip-apps\` - Skip syncing apps
  - \`--skip-folders\` - Skip syncing folders
  - \`--skip-workspace-dependencies\` - Skip syncing workspace dependencies
  - \`--skip-scripts-metadata\` - Skip syncing scripts metadata, focus solely on logic
  - \`--include-schedules\` - Include syncing schedules
  - \`--include-triggers\` - Include syncing triggers
  - \`--include-users\` - Include syncing users
  - \`--include-groups\` - Include syncing groups
  - \`--include-settings\` - Include syncing workspace settings
  - \`--include-key\` - Include workspace encryption key
  - \`--skip-reencrypt-on-key-change\` - When the pushed encryption key differs from the remote, do NOT re-encrypt existing remote secrets. Only safe if they are already encrypted with the new key (e.g. workspace/instance migration). Default is to re-encrypt.
  - \`--skip-branch-validation\` - Skip git branch validation and prompts
  - \`--json-output\` - Output results in JSON format
  - \`-i --includes <patterns:file[]>\` - Comma separated patterns to specify which file to take into account (among files that are compatible with windmill). Patterns can include * (any string until '/') and ** (any string)
  - \`-e --excludes <patterns:file[]>\` - Comma separated patterns to specify which file to NOT take into account.
  - \`--extra-includes <patterns:file[]>\` - Comma separated patterns to specify which file to take into account (among files that are compatible with windmill). Patterns can include * (any string until '/') and ** (any string). Useful to still take wmill.yaml into account and act as a second pattern to satisfy
  - \`--message <message:string>\` - Include a message that will be added to all scripts/flows/apps updated during this push
  - \`--parallel <number>\` - Number of changes to process in parallel
  - \`--repository <repo:string>\` - Specify repository path (e.g., u/user/repo) when multiple repositories exist
  - \`--branch, --env <branch:string>\` - [Deprecated: use --workspace] Override the current git branch/environment
  - \`--lint\` - Run lint validation before pushing
  - \`--locks-required\` - Fail if scripts or flow inline scripts that need locks have no locks
  - \`--auto-metadata\` - Automatically regenerate stale metadata (locks and schemas) before pushing
  - \`--accept-overriding-permissioned-as-with-self\` - Accept that items with a different permissioned_as will be updated with your own user

### token

Manage API tokens

**Options:**
- \`--json\` - Output as JSON (for piping to jq)

**Subcommands:**

- \`token list\` - List API tokens
  - \`--json\` - Output as JSON (for piping to jq)
- \`token create\` - Create a new API token
  - \`--label <label:string>\` - Token label
  - \`--expiration <expiration:string>\` - Token expiration (ISO 8601 timestamp)
- \`token delete <token_prefix:string>\` - Delete a token by its prefix

### trigger

trigger related commands

**Options:**
- \`--json\` - Output as JSON (for piping to jq)

**Subcommands:**

- \`trigger list\` - list all triggers
  - \`--json\` - Output as JSON (for piping to jq)
- \`trigger get <path:string>\` - get a trigger's details
  - \`--json\` - Output as JSON (for piping to jq)
  - \`--kind <kind:string>\` - Trigger kind (http, websocket, kafka, nats, postgres, mqtt, amqp, sqs, gcp, azure, email). Recommended for faster lookup
- \`trigger new <path:string>\` - create a new trigger locally
  - \`--kind <kind:string>\` - Trigger kind (required: http, websocket, kafka, nats, postgres, mqtt, amqp, sqs, gcp, azure, email)
- \`trigger push <file_path:string> <remote_path:string>\` - push a local trigger spec. This overrides any remote versions.
- \`trigger set-permissioned-as <path:string> <email:string>\` - Set the email (run-as user) for a trigger (requires admin or wm_deployers group)
  - \`--kind <kind:string>\` - Trigger kind (required: http, websocket, kafka, nats, postgres, mqtt, amqp, sqs, gcp, azure, email)

### user

user related commands

**Subcommands:**

- \`user add <email:string> [password:string]\` - Create a user
  - \`--superadmin\` - Specify to make the new user superadmin.
  - \`--company <company:string>\` - Specify to set the company of the new user.
  - \`--name <name:string>\` - Specify to set the name of the new user.
- \`user remove <email:string>\` - Delete a user
- \`user create-token\` - Create a new API token for the authenticated user
  - \`--email <email:string>\` - Specify credentials to use for authentication. This will not be stored. It will only be used to exchange for a token with the API server, which will not be stored either.
  - \`--password <password:string>\` - Specify credentials to use for authentication. This will not be stored. It will only be used to exchange for a token with the API server, which will not be stored either.

### variable

variable related commands

**Options:**
- \`--json\` - Output as JSON (for piping to jq)

**Subcommands:**

- \`variable list\` - list all variables
  - \`--json\` - Output as JSON (for piping to jq)
- \`variable get <path:string>\` - get a variable's details
  - \`--json\` - Output as JSON (for piping to jq)
- \`variable new <path:string>\` - create a new variable locally
- \`variable push <file_path:string> <remote_path:string>\` - Push a local variable spec. This overrides any remote versions.
  - \`--plain-secrets\` - Push secrets as plain text
- \`variable add <value:string> <remote_path:string>\` - Create a new variable on the remote. This will update the variable if it already exists.
  - \`--yes\` - Skip confirmation prompt when updating an existing variable
  - \`--secret\` - Mark the variable as secret (default when creating a new variable)
  - \`--no-secret\` - Mark the variable as non-secret (when updating, the existing setting is preserved if neither --secret nor --no-secret is passed)
  - \`--description <description:string>\` - Set the variable description (when updating, the existing description is preserved if not passed)
  - \`--plain-secrets\` - Push secrets as plain text
  - \`--public\` - Legacy option, use --no-secret instead

### version

Show version information

### worker-groups

display worker groups, pull and push worker groups configs

**Subcommands:**

- \`worker-groups pull\` - Pull worker groups (similar to \`wmill instance pull --skip-users --skip-settings --skip-groups\`)
  - \`--instance\` - Name of the instance to push to, override the active instance
  - \`--base-url\` - Base url to be passed to the instance settings instead of the local one
  - \`--yes\` - Pull without needing confirmation
- \`worker-groups push\` - Push worker groups (similar to \`wmill instance push --skip-users --skip-settings --skip-groups\`)
  - \`--instance [instance]\` - Name of the instance to push to, override the active instance
  - \`--base-url [baseUrl]\` - If used with --token, will be used as the base url for the instance
  - \`--yes\` - Push without needing confirmation

### workers

List all workers grouped by worker groups

**Options:**
- \`--instance [instance]\` - Name of the instance to push to, override the active instance
- \`--base-url [baseUrl]\` - If used with --token, will be used as the base url for the instance

### workspace

workspace related commands

**Alias:** \`profile\`

**Subcommands:**

- \`workspace switch <workspace_name:string:workspace>\` - Switch to another workspace
- \`workspace add [workspace_name:string] [workspace_id:string] [remote:string]\` - Add a workspace
  - \`-c --create\` - Create the workspace if it does not exist
  - \`--create-workspace-name <workspace_name:string>\` - Specify the workspace name. Ignored if --create is not specified or the workspace already exists. Will default to the workspace id.
  - \`--create-username <username:string>\` - Specify your own username in the newly created workspace. Ignored if --create is not specified, the workspace already exists or automatic username creation is enabled on the instance.
- \`workspace remove <workspace_name:string>\` - Remove a workspace
- \`workspace whoami\` - Show the currently active user
- \`workspace list\` - List local workspace profiles
- \`workspace list-remote\` - List workspaces on the remote server that you have access to
  - \`--as-superadmin\` - List ALL workspaces on the instance (requires the token to belong to a superadmin/devops user)
- \`workspace list-forks\` - List forked workspaces on the remote server
- \`workspace bind\` - Create or update a workspace entry in wmill.yaml from the active profile
  - \`--workspace <name:string>\` - Workspace name (default: current branch or workspaceId)
  - \`--branch <branch:string>\` - Git branch to associate (default: workspace name)
- \`workspace unbind\` - Remove baseUrl and workspaceId from a workspace entry
  - \`--workspace <name:string>\` - Workspace to unbind
- \`workspace fork [workspace_name:string] [workspace_id:string]\` - Create a forked workspace from its parent workspace.

The parent is resolved from your current git branch, not from the active profile: run this from a git repo checked out on the branch mapped to the parent workspace in wmill.yaml's \`workspaces:\` section (a fork branch of it resolves to the same parent). \`wmill workspace switch\` does not change which workspace is forked.

Arguments (omit both to be prompted interactively):
  [workspace_name]  Friendly display name for the fork, shown in the UI. May contain spaces, so quote it in the shell (e.g. "My Fork"). Max 50 chars. Defaults to "<parent workspace name>'s fork".
  [workspace_id]    Id for the fork. Must be a slug (no spaces or special characters) and is automatically prefixed with \`wm-fork-\`, so pass just the bare slug (e.g. \`my-fork\` becomes \`wm-fork-my-fork\`). This id also determines the fork's git branch name. Defaults to a slug derived from the name — or, when you are converting an existing branch into the fork branch, from that branch.
  - \`--create-workspace-name <workspace_name:string>\` - Specify the workspace name. Ignored if --create is not specified or the workspace already exists. Will default to the workspace id.
  - \`--color <color:string>\` - Workspace color (hex code, e.g. #ff0000)
  - \`--datatable-behavior <behavior:string>\` - How to handle datatables: skip, schema_only, or schema_and_data (default: interactive prompt)
  - \`--from-branch <branch:string>\` - Non-interactive override for the 'turn my current working branch into the fork' workflow: base the fork on <branch> (its bound workspace is the parent) and rename the current branch onto wm-fork/<branch>/<id>. Usually unneeded — from a working branch \`wmill workspace fork\` offers this interactively; from a base branch it creates a fresh fork branch.
  - \`-y --yes\` - Skip interactive prompts (defaults datatable behavior to 'skip'). On a non-base branch, requires --from-branch since the base branch can't be prompted for.
- \`workspace delete-fork <fork_name:string>\` - Delete a forked workspace
  - \`-y --yes\` - Skip confirmation prompt
- \`workspace merge\` - Compare and deploy changes between a fork and its parent workspace
  - \`--direction <direction:string>\` - Deploy direction: to-parent or to-fork
  - \`--all\` - Deploy all changed items including conflicts
  - \`--skip-conflicts\` - Skip items modified in both workspaces
  - \`--include <items:string>\` - Comma-separated kind:path items to include (e.g. script:f/test/main,flow:f/my/flow)
  - \`--exclude <items:string>\` - Comma-separated kind:path items to exclude
  - \`--preserve-on-behalf-of\` - Preserve original on_behalf_of/permissioned_as values
  - \`-y --yes\` - Non-interactive mode (deploy without prompts)
- \`workspace connect-slack\` - Non-interactively connect Slack to the active workspace using a pre-minted bot token (xoxb-...). Produces the same artifacts as the UI OAuth flow: workspace_settings fields, g/slack group, f/slack_bot folder, and the encrypted bot token variable + resource at f/slack_bot/bot_token.
  - \`--bot-token <bot_token:string>\` - Slack bot token (xoxb-...)
  - \`--team-id <team_id:string>\` - Slack team id
  - \`--team-name <team_name:string>\` - Slack team name
- \`workspace disconnect-slack\` - Clear slack_team_id / slack_name on the active workspace (marks the workspace as disconnected). Does NOT remove the bot token variable/resource/folder/group — delete those from the local sync folder and run 'wmill sync push' to tear them down. Does NOT remove the workspace-level OAuth override — set slack_oauth_client_id/_secret to '' in settings.yaml and push.



# Object Storage CLI

\`wmill object-storage\` (alias \`wmill s3\`) exposes the workspace's object storage (S3-compatible: AWS S3, MinIO, GCS, R2, Azure Blob) over the per-workspace \`/job_helpers/*\` endpoints.

## Key concepts (not obvious from per-command --help)

- **\`file_key\` is the path inside the bucket** (e.g. \`reports/2026-05/orders.csv\`), not a Windmill path. Do NOT pass \`u/...\` or \`f/...\` here — those are Windmill paths to scripts/flows/resources, unrelated to objects in the bucket.
- **Scope is the active workspace.** Object storage is configured per-workspace (default storage + optional secondary storages). Switching workspaces switches which bucket the commands target.
- **\`--storage <name>\` targets a secondary storage** configured on the workspace. Omit it to use the workspace's default object storage. Use \`wmill object-storage list\` to discover configured storages.
- **\`preview\` vs \`download\`**: \`preview\` returns a peek (CSV first rows, text content, or a byte slice via \`--bytes-from\`/\`--bytes-length\`) without writing to disk. Use \`download\` when you want the full file on disk.

## Choosing a subcommand

- Look at what's there: \`wmill object-storage files [prefix]\` (alias \`ls\`) — paginated, use \`--marker\` to continue.
- Inspect one file: \`wmill object-storage info <file_key>\` for size/mime/last-modified, \`wmill object-storage preview <file_key>\` for content peek.
- Move data in: \`wmill object-storage upload <local_path> <file_key>\` — set \`--content-type\` if the receiver cares (e.g. \`text/csv\`).
- Move data out: \`wmill object-storage download <file_key> [output_path]\` — \`--stdout\` to pipe.
- Reorganize: \`wmill object-storage move <src> <dest>\` (same storage), \`wmill object-storage delete <file_key>\` (interactive confirm unless \`--yes\`).
`,
  "preview": `---
name: preview
description: MUST use when opening the Windmill dev page / visual preview of a flow, script, or app. Triggers on words like preview, open, navigate to, visualize, see the flow/app/script, and after writing a flow/script/app for visual verification.
---

# Windmill Preview Workflow

Use this skill any time the user wants to **see**, **open**, **navigate to**, **visualize**, or **preview** a flow, script, or app — and any time you've just finished writing one and want to offer visual verification.

The Windmill dev page renders the flow graph / script editor, lets the user step through steps, and live-reloads on every save. It runs locally via \`wmill dev\` and is reached on a localhost port.

## Two independent decisions

### 1. Mode: proxy or direct?

\`wmill dev\` runs in two modes; pick by asking what kind of URL whatever will display the preview needs.

- **Proxy** (\`--proxy-port <port>\`) — exposes the dev page on \`http://localhost:<port>/\`. Use it when the embedder you'll hand the URL to **only accepts localhost URLs** (most in-IDE / in-chat preview embedders do, because they sandbox cross-origin loads).
- **Direct** (default) — the user's browser loads the dev page from the remote workspace's HTTPS URL; the local \`wmill dev\` only runs the WebSocket back-channel for live reload. Use it when the URL will be opened in a regular browser tab.

Default to **direct** unless you have a specific embedder that needs localhost.

### 2. Who starts the server?

- **You start it** in the background. Spawn \`wmill dev …\` (or \`wmill app dev …\`) yourself, capture the URL it prints, do whatever's next (open a tab, hand the URL to an embedder).
- **The runtime starts it from \`.claude/launch.json\`.** Some runtimes (currently the Claude Desktop / Claude Code MCP preview integration — tools prefixed with \`mcp__Claude_Preview__\`) can read a \`launch.json\` configuration and launch the dev server on demand when you invoke their preview tool. **Only take this path if you actually have such a tool** — otherwise nothing reads the file and \`wmill dev\` never starts.

The two decisions compose. The common cases:

| Embedder | Needs localhost? | launch.json runtime? | What to do |
|---|---|---|---|
| Regular browser tab | No | n/a | Direct mode, you start it, give URL to user |
| IDE / chat preview pane that takes any URL | No | No | Direct mode, you start it, point the embedder at the printed URL |
| IDE / chat preview pane that only accepts localhost | Yes | No | Proxy mode, you start it, point the embedder at \`http://localhost:<port>/\` |
| Claude Desktop / Code MCP preview | Yes | Yes | Proxy mode, write a \`launch.json\` entry, invoke the MCP tool |

Never start the proxy "just in case" — it adds the localhost hop for no benefit when no embedder needs it.

## Starting the server yourself

Use this when no \`launch.json\`-aware runtime is available, regardless of mode.

For flows / scripts:
\`\`\`bash
# Direct mode — gives you the remote dev-page URL
wmill dev --path <wmill_path> --no-open

# Proxy mode — gives you a localhost URL that 302s to the remote dev page
wmill dev --proxy-port 4000 --path <wmill_path> --no-open
\`\`\`

For apps:
\`\`\`bash
cd <app_path>__raw_app && wmill app dev --no-open --port 4000
\`\`\`

Each command prints the URL on stdout. Line shapes differ:

- \`wmill dev --no-open\` (direct) prints \`Go to <url>\` with the full remote URL (workspace, token, path baked in).
- \`wmill dev --proxy-port\` prints \`Dev proxy listening on http://localhost:<port>\` — the URL to hand to an embedder is \`http://localhost:<port>/\`.
- \`wmill app dev --no-open\` prints \`🚀 Dev server running at <url>\` — the local app server.

Capture the URL with a loose match (the first \`https?://…\` token after startup) and either hand it to your embedder or relay it to the user: *"Preview is running — open \`<url>\` in your browser."* Don't construct the URL yourself; you don't have the workspace ID or auth token.

These commands are long-running — start them in the background, don't block waiting.

## Letting \`launch.json\` start the server (Claude Desktop / Code MCP only)

Take this path when **and only when** an \`mcp__Claude_Preview__*\` MCP tool is exposed in your tool list. Skip it otherwise — without an MCP tool reading the file, \`wmill dev\` never starts.

**Each flow / script / app gets its own named entry** in the user's \`.claude/launch.json\` so multiple previews coexist without colliding — each entry pins a different port + path. Never reuse a generic "windmill" entry for different targets.

### Step 1 — Reuse or add a per-target entry in \`.claude/launch.json\`

Convention: name the entry \`windmill: <wmill_path>\` (e.g. \`windmill: f/test/my_flow\`).

- **Entry already exists** → reuse it; note its \`port\` for the next step.
- **Not there** → add one. Pick a port not already taken by another entry (start at 4000 and bump). Shape:

For flows / scripts:
\`\`\`json
{
  "name": "windmill: f/test/my_flow",
  "runtimeExecutable": "bash",
  "runtimeArgs": ["-c", "wmill dev --proxy-port \${PORT:-4000} --path f/test/my_flow --no-open"],
  "port": 4000,
  "autoPort": true
}
\`\`\`

For apps (\`*__raw_app/\`), \`wmill app dev\` is the equivalent — runs from the app folder, no \`--path\`:
\`\`\`json
{
  "name": "windmill: f/test/my_app",
  "runtimeExecutable": "bash",
  "runtimeArgs": ["-c", "cd f/test/my_app__raw_app && wmill app dev --no-open --port \${PORT:-4001}"],
  "port": 4001,
  "autoPort": true
}
\`\`\`

If \`.claude/launch.json\` doesn't exist yet, create it with the standard shell \`{ "version": "0.0.1", "configurations": [...] }\`.

### Step 2 — Invoke the MCP preview tool

Point it at the entry you just added/found. Use \`http://localhost:<port>/\` as the URL — the proxy's redirect at \`/\` is what appends the workspace ID, the auth token, and the path. Do **NOT** construct a \`/dev?...\` URL yourself.

The MCP tool launches the configuration on demand, so you don't need to start the \`wmill dev\` process manually.

## Non-visual alternative

If the user wants a programmatic test rather than a visual one:
- Flow: \`wmill flow preview <path> -d '<args>'\`
- Script: \`wmill script preview <path> -d '<args>'\`

Both print the job result, are safe to run yourself, and don't deploy.

## Anti-patterns to avoid

- ❌ Writing a \`.claude/launch.json\` entry when no \`mcp__Claude_Preview__*\` tool is in your tool list. Nothing will read the file; the server never starts. Spawn \`wmill dev\` yourself instead.
- ❌ Starting the proxy when no embedder needs a localhost URL. Direct mode is the right choice — the proxy is overhead with no purpose.
- ❌ Reusing a single generic \`launch.json\` entry for every preview target. Each flow/script/app gets its own named entry on its own port — that's how multiple sessions coexist without one preview clobbering another.
- ❌ Mutating an existing entry's \`--path\` to retarget it. Add a new entry instead.
- ❌ Constructing \`http://localhost:<port>/dev?path=<X>\` yourself. The proxy's \`/\` redirect is what appends the workspace ID and auth token; bypassing it gives a broken page. Always use \`http://localhost:<port>/\`.
- ❌ Starting \`wmill dev\` in the foreground (you'll hang). Always background.
- ❌ Listing both "open in IDE pane" and "open in browser" as a menu — pick one based on context.
`,
};

// YAML schema content for triggers and schedules
export const SCHEMAS: Record<string, string> = {
  "amqp_trigger": `type: object
properties:
  script_path:
    type: string
    description: Path to the script or flow to execute when triggered
  permissioned_as:
    type: string
    description: The user or group this trigger runs as (permissioned_as)
  is_flow:
    type: boolean
    description: True if script_path points to a flow, false if it points to a script
  labels:
    type: array
    items:
      type: string
  draft_only:
    type: boolean
    description: 'True when this row is a per-user draft with no deployed

      trigger at the same path. Set by list endpoints when

      \`include_draft_only=true\` synthesizes the row from the

      draft. Frontend renders a "Draft" badge.

      '
  is_draft:
    type: boolean
    description: 'True when the authed user has a per-user draft at this path

      (over a deployed row or a synthesized draft-only row).

      Frontend appends a \`*\` to the displayed name.

      '
  amqp_resource_path:
    type: string
    description: Path to the AMQP resource containing broker connection configuration
  queue_name:
    type: string
    description: Name of the queue to consume messages from
  exchange:
    type: object
    properties:
      exchange_name:
        type: string
        description: Name of the exchange to bind the consumed queue to
      routing_keys:
        type: array
        items:
          type: string
        description: Routing keys used to bind the queue to the exchange
  options:
    type: object
    properties:
      declare_queue:
        type: boolean
        description: Declare the queue (durable) before consuming; when false the
          queue is declared passively and must already exist
      prefetch_count:
        type: integer
        format: int32
        minimum: 1
        maximum: 65535
        description: Maximum number of unacknowledged messages the broker delivers
          at once (1-65535)
  error_handler_path:
    type: string
    description: Path to a script or flow to run when the triggered job fails
  error_handler_args:
    type: object
    description: The arguments to pass to the script or flow
  retry:
    type: object
    properties:
      constant:
        type: object
        description: Retry with constant delay between attempts
        properties:
          attempts:
            type: integer
            description: Number of retry attempts
          seconds:
            type: integer
            description: Seconds to wait between retries
      exponential:
        type: object
        description: Retry with exponential backoff (delay doubles each time)
        properties:
          attempts:
            type: integer
            description: Number of retry attempts
          multiplier:
            type: integer
            description: Multiplier for exponential backoff
          seconds:
            type: integer
            minimum: 1
            description: Initial delay in seconds
          random_factor:
            type: integer
            minimum: 0
            maximum: 100
            description: Random jitter percentage (0-100) to avoid thundering herd
      retry_if:
        $ref: '#/components/schemas/RetryIf'
    description: Retry configuration for failed module executions
required:
- script_path
- permissioned_as
- is_flow
- amqp_resource_path
- queue_name
`,
  "azure_trigger": `type: object
properties:
  script_path:
    type: string
    description: Path to the script or flow to execute when triggered
  permissioned_as:
    type: string
    description: The user or group this trigger runs as (permissioned_as)
  is_flow:
    type: boolean
    description: True if script_path points to a flow, false if it points to a script
  labels:
    type: array
    items:
      type: string
  draft_only:
    type: boolean
    description: 'True when this row is a per-user draft with no deployed

      trigger at the same path. Set by list endpoints when

      \`include_draft_only=true\` synthesizes the row from the

      draft. Frontend renders a "Draft" badge.

      '
  is_draft:
    type: boolean
    description: 'True when the authed user has a per-user draft at this path

      (over a deployed row or a synthesized draft-only row).

      Frontend appends a \`*\` to the displayed name.

      '
  azure_resource_path:
    type: string
  azure_mode:
    type: string
    enum:
    - basic_push
    - namespace_push
    - namespace_pull
    description: Azure Event Grid trigger mode.
  scope_resource_id:
    type: string
    description: ARM resource ID of the topic (basic) or namespace (namespace modes).
  topic_name:
    type: string
    description: Topic name within the namespace (namespace modes only).
  subscription_name:
    type: string
  event_type_filters:
    type: array
    items:
      type: string
  error_handler_path:
    type: string
  error_handler_args:
    type: object
    description: The arguments to pass to the script or flow
  retry:
    type: object
    properties:
      constant:
        type: object
        description: Retry with constant delay between attempts
        properties:
          attempts:
            type: integer
            description: Number of retry attempts
          seconds:
            type: integer
            description: Seconds to wait between retries
      exponential:
        type: object
        description: Retry with exponential backoff (delay doubles each time)
        properties:
          attempts:
            type: integer
            description: Number of retry attempts
          multiplier:
            type: integer
            description: Multiplier for exponential backoff
          seconds:
            type: integer
            minimum: 1
            description: Initial delay in seconds
          random_factor:
            type: integer
            minimum: 0
            maximum: 100
            description: Random jitter percentage (0-100) to avoid thundering herd
      retry_if:
        $ref: '#/components/schemas/RetryIf'
    description: Retry configuration for failed module executions
required:
- script_path
- permissioned_as
- is_flow
- azure_resource_path
- azure_mode
- scope_resource_id
- subscription_name
`,
  "email_trigger": `type: object
properties:
  script_path:
    type: string
    description: Path to the script or flow to execute when triggered
  permissioned_as:
    type: string
    description: The user or group this trigger runs as (permissioned_as)
  is_flow:
    type: boolean
    description: True if script_path points to a flow, false if it points to a script
  labels:
    type: array
    items:
      type: string
  draft_only:
    type: boolean
    description: 'True when this row is a per-user draft with no deployed

      trigger at the same path. Set by list endpoints when

      \`include_draft_only=true\` synthesizes the row from the

      draft. Frontend renders a "Draft" badge.

      '
  is_draft:
    type: boolean
    description: 'True when the authed user has a per-user draft at this path

      (over a deployed row or a synthesized draft-only row).

      Frontend appends a \`*\` to the displayed name.

      '
  local_part:
    type: string
  workspaced_local_part:
    type: boolean
  error_handler_path:
    type: string
  error_handler_args:
    type: object
    description: The arguments to pass to the script or flow
  retry:
    type: object
    properties:
      constant:
        type: object
        description: Retry with constant delay between attempts
        properties:
          attempts:
            type: integer
            description: Number of retry attempts
          seconds:
            type: integer
            description: Seconds to wait between retries
      exponential:
        type: object
        description: Retry with exponential backoff (delay doubles each time)
        properties:
          attempts:
            type: integer
            description: Number of retry attempts
          multiplier:
            type: integer
            description: Multiplier for exponential backoff
          seconds:
            type: integer
            minimum: 1
            description: Initial delay in seconds
          random_factor:
            type: integer
            minimum: 0
            maximum: 100
            description: Random jitter percentage (0-100) to avoid thundering herd
      retry_if:
        $ref: '#/components/schemas/RetryIf'
    description: Retry configuration for failed module executions
required:
- script_path
- permissioned_as
- is_flow
- local_part
`,
  "gcp_trigger": `type: object
properties:
  script_path:
    type: string
    description: Path to the script or flow to execute when triggered
  permissioned_as:
    type: string
    description: The user or group this trigger runs as (permissioned_as)
  is_flow:
    type: boolean
    description: True if script_path points to a flow, false if it points to a script
  labels:
    type: array
    items:
      type: string
  draft_only:
    type: boolean
    description: 'True when this row is a per-user draft with no deployed

      trigger at the same path. Set by list endpoints when

      \`include_draft_only=true\` synthesizes the row from the

      draft. Frontend renders a "Draft" badge.

      '
  is_draft:
    type: boolean
    description: 'True when the authed user has a per-user draft at this path

      (over a deployed row or a synthesized draft-only row).

      Frontend appends a \`*\` to the displayed name.

      '
  gcp_resource_path:
    type: string
    description: Path to the GCP resource containing service account credentials for
      authentication.
  topic_id:
    type: string
    description: Google Cloud Pub/Sub topic ID to subscribe to.
  subscription_id:
    type: string
    description: Google Cloud Pub/Sub subscription ID.
  delivery_type:
    type: string
    enum:
    - push
    - pull
    description: Delivery mode for messages. 'push' for HTTP push delivery where messages
      are sent to a webhook endpoint, 'pull' for polling where the trigger actively
      fetches messages.
  delivery_config:
    type: object
    properties:
      audience:
        type: string
        description: The audience claim for OIDC tokens used in push authentication.
      authenticate:
        type: boolean
        description: If true, push messages will include OIDC authentication tokens.
    description: Configuration for push delivery mode.
  subscription_mode:
    type: string
    enum:
    - existing
    - create_update
    description: The mode of subscription. 'existing' means using an existing GCP
      subscription, while 'create_update' involves creating or updating a new subscription.
  error_handler_path:
    type: string
    description: Path to a script or flow to run when the triggered job fails.
  error_handler_args:
    type: object
    description: The arguments to pass to the script or flow
  retry:
    type: object
    properties:
      constant:
        type: object
        description: Retry with constant delay between attempts
        properties:
          attempts:
            type: integer
            description: Number of retry attempts
          seconds:
            type: integer
            description: Seconds to wait between retries
      exponential:
        type: object
        description: Retry with exponential backoff (delay doubles each time)
        properties:
          attempts:
            type: integer
            description: Number of retry attempts
          multiplier:
            type: integer
            description: Multiplier for exponential backoff
          seconds:
            type: integer
            minimum: 1
            description: Initial delay in seconds
          random_factor:
            type: integer
            minimum: 0
            maximum: 100
            description: Random jitter percentage (0-100) to avoid thundering herd
      retry_if:
        $ref: '#/components/schemas/RetryIf'
    description: Retry configuration for failed module executions
required:
- script_path
- permissioned_as
- is_flow
- gcp_resource_path
- topic_id
- subscription_id
- delivery_type
- subscription_mode
`,
  "http_trigger": `type: object
properties:
  script_path:
    type: string
    description: Path to the script or flow to execute when triggered
  permissioned_as:
    type: string
    description: The user or group this trigger runs as (permissioned_as)
  is_flow:
    type: boolean
    description: True if script_path points to a flow, false if it points to a script
  labels:
    type: array
    items:
      type: string
  draft_only:
    type: boolean
    description: 'True when this row is a per-user draft with no deployed

      trigger at the same path. Set by list endpoints when

      \`include_draft_only=true\` synthesizes the row from the

      draft. Frontend renders a "Draft" badge.

      '
  is_draft:
    type: boolean
    description: 'True when the authed user has a per-user draft at this path

      (over a deployed row or a synthesized draft-only row).

      Frontend appends a \`*\` to the displayed name.

      '
  route_path:
    type: string
    description: The URL route path that will trigger this endpoint (e.g., 'api/myendpoint').
      Must NOT start with a /.
  static_asset_config:
    type: object
    properties:
      s3:
        type: string
        description: S3 bucket path for static assets
      storage:
        type: string
        description: Storage path for static assets
      filename:
        type: string
        description: Filename for the static asset
    description: Configuration for serving static assets (s3 bucket, storage path,
      filename)
  http_method:
    type: string
    enum:
    - get
    - post
    - put
    - delete
    - patch
  authentication_resource_path:
    type: string
    description: Path to the resource containing authentication configuration (for
      api_key, basic_http, custom_script, signature methods)
  summary:
    type: string
    description: Short summary describing the purpose of this trigger
  description:
    type: string
    description: Detailed description of what this trigger does
  request_type:
    type: string
    enum:
    - sync
    - async
    - sync_sse
  authentication_method:
    type: string
    enum:
    - none
    - windmill
    - api_key
    - basic_http
    - custom_script
    - signature
  is_static_website:
    type: boolean
    description: If true, serves static files from S3/storage instead of running a
      script
  workspaced_route:
    type: boolean
    description: If true, the route includes the workspace ID in the path
  wrap_body:
    type: boolean
    description: If true, wraps the request body in a 'body' parameter
  raw_string:
    type: boolean
    description: If true, passes the request body as a raw string instead of parsing
      as JSON
  error_handler_path:
    type: string
    description: Path to a script or flow to run when the triggered job fails
  error_handler_args:
    type: object
    description: The arguments to pass to the script or flow
  retry:
    type: object
    properties:
      constant:
        type: object
        description: Retry with constant delay between attempts
        properties:
          attempts:
            type: integer
            description: Number of retry attempts
          seconds:
            type: integer
            description: Seconds to wait between retries
      exponential:
        type: object
        description: Retry with exponential backoff (delay doubles each time)
        properties:
          attempts:
            type: integer
            description: Number of retry attempts
          multiplier:
            type: integer
            description: Multiplier for exponential backoff
          seconds:
            type: integer
            minimum: 1
            description: Initial delay in seconds
          random_factor:
            type: integer
            minimum: 0
            maximum: 100
            description: Random jitter percentage (0-100) to avoid thundering herd
      retry_if:
        $ref: '#/components/schemas/RetryIf'
    description: Retry configuration for failed module executions
required:
- script_path
- permissioned_as
- is_flow
- route_path
- request_type
- authentication_method
- http_method
- is_static_website
- workspaced_route
- wrap_body
- raw_string
`,
  "kafka_trigger": `type: object
properties:
  script_path:
    type: string
    description: Path to the script or flow to execute when triggered
  permissioned_as:
    type: string
    description: The user or group this trigger runs as (permissioned_as)
  is_flow:
    type: boolean
    description: True if script_path points to a flow, false if it points to a script
  labels:
    type: array
    items:
      type: string
  draft_only:
    type: boolean
    description: 'True when this row is a per-user draft with no deployed

      trigger at the same path. Set by list endpoints when

      \`include_draft_only=true\` synthesizes the row from the

      draft. Frontend renders a "Draft" badge.

      '
  is_draft:
    type: boolean
    description: 'True when the authed user has a per-user draft at this path

      (over a deployed row or a synthesized draft-only row).

      Frontend appends a \`*\` to the displayed name.

      '
  kafka_resource_path:
    type: string
    description: Path to the Kafka resource containing connection configuration
  group_id:
    type: string
    description: Kafka consumer group ID for this trigger
  topics:
    type: array
    items:
      type: string
    description: Array of Kafka topic names to subscribe to
  filters:
    type: array
    items:
      type: object
      properties:
        key:
          type: string
        value: {}
  filter_logic:
    type: string
    enum:
    - and
    - or
    description: Logic to apply when evaluating filters. 'and' requires all filters
      to match, 'or' requires any filter to match.
  auto_offset_reset:
    type: string
    enum:
    - latest
    - earliest
    description: Initial offset behavior when consumer group has no committed offset.
      'latest' starts from new messages only, 'earliest' starts from the beginning.
  auto_commit:
    type: boolean
    description: When true (default), offsets are committed automatically after receiving
      each message. When false, you must manually commit offsets using the commit_offsets
      endpoint.
  error_handler_path:
    type: string
    description: Path to a script or flow to run when the triggered job fails
  error_handler_args:
    type: object
    description: The arguments to pass to the script or flow
  retry:
    type: object
    properties:
      constant:
        type: object
        description: Retry with constant delay between attempts
        properties:
          attempts:
            type: integer
            description: Number of retry attempts
          seconds:
            type: integer
            description: Seconds to wait between retries
      exponential:
        type: object
        description: Retry with exponential backoff (delay doubles each time)
        properties:
          attempts:
            type: integer
            description: Number of retry attempts
          multiplier:
            type: integer
            description: Multiplier for exponential backoff
          seconds:
            type: integer
            minimum: 1
            description: Initial delay in seconds
          random_factor:
            type: integer
            minimum: 0
            maximum: 100
            description: Random jitter percentage (0-100) to avoid thundering herd
      retry_if:
        $ref: '#/components/schemas/RetryIf'
    description: Retry configuration for failed module executions
required:
- script_path
- permissioned_as
- is_flow
- kafka_resource_path
- group_id
- topics
- filters
`,
  "mqtt_trigger": `type: object
properties:
  script_path:
    type: string
    description: Path to the script or flow to execute when triggered
  permissioned_as:
    type: string
    description: The user or group this trigger runs as (permissioned_as)
  is_flow:
    type: boolean
    description: True if script_path points to a flow, false if it points to a script
  labels:
    type: array
    items:
      type: string
  draft_only:
    type: boolean
    description: 'True when this row is a per-user draft with no deployed

      trigger at the same path. Set by list endpoints when

      \`include_draft_only=true\` synthesizes the row from the

      draft. Frontend renders a "Draft" badge.

      '
  is_draft:
    type: boolean
    description: 'True when the authed user has a per-user draft at this path

      (over a deployed row or a synthesized draft-only row).

      Frontend appends a \`*\` to the displayed name.

      '
  mqtt_resource_path:
    type: string
    description: Path to the MQTT resource containing broker connection configuration
  subscribe_topics:
    type: array
    items:
      type: object
    description: Array of MQTT topics to subscribe to, each with topic name and QoS
      level
  v3_config:
    type: object
    properties:
      clean_session:
        type: boolean
  v5_config:
    type: object
    properties:
      clean_start:
        type: boolean
      topic_alias_maximum:
        type: number
      session_expiry_interval:
        type: number
  client_id:
    type: string
    description: MQTT client ID for this connection
  client_version:
    type: string
    enum:
    - v3
    - v5
  error_handler_path:
    type: string
    description: Path to a script or flow to run when the triggered job fails
  error_handler_args:
    type: object
    description: The arguments to pass to the script or flow
  retry:
    type: object
    properties:
      constant:
        type: object
        description: Retry with constant delay between attempts
        properties:
          attempts:
            type: integer
            description: Number of retry attempts
          seconds:
            type: integer
            description: Seconds to wait between retries
      exponential:
        type: object
        description: Retry with exponential backoff (delay doubles each time)
        properties:
          attempts:
            type: integer
            description: Number of retry attempts
          multiplier:
            type: integer
            description: Multiplier for exponential backoff
          seconds:
            type: integer
            minimum: 1
            description: Initial delay in seconds
          random_factor:
            type: integer
            minimum: 0
            maximum: 100
            description: Random jitter percentage (0-100) to avoid thundering herd
      retry_if:
        $ref: '#/components/schemas/RetryIf'
    description: Retry configuration for failed module executions
required:
- script_path
- permissioned_as
- is_flow
- subscribe_topics
- mqtt_resource_path
`,
  "nats_trigger": `type: object
properties:
  script_path:
    type: string
    description: Path to the script or flow to execute when triggered
  permissioned_as:
    type: string
    description: The user or group this trigger runs as (permissioned_as)
  is_flow:
    type: boolean
    description: True if script_path points to a flow, false if it points to a script
  labels:
    type: array
    items:
      type: string
  draft_only:
    type: boolean
    description: 'True when this row is a per-user draft with no deployed

      trigger at the same path. Set by list endpoints when

      \`include_draft_only=true\` synthesizes the row from the

      draft. Frontend renders a "Draft" badge.

      '
  is_draft:
    type: boolean
    description: 'True when the authed user has a per-user draft at this path

      (over a deployed row or a synthesized draft-only row).

      Frontend appends a \`*\` to the displayed name.

      '
  nats_resource_path:
    type: string
    description: Path to the NATS resource containing connection configuration
  use_jetstream:
    type: boolean
    description: If true, uses NATS JetStream for durable message delivery
  stream_name:
    type: string
    description: JetStream stream name (required when use_jetstream is true)
  consumer_name:
    type: string
    description: JetStream consumer name (required when use_jetstream is true)
  subjects:
    type: array
    items:
      type: string
    description: Array of NATS subjects to subscribe to
  error_handler_path:
    type: string
    description: Path to a script or flow to run when the triggered job fails
  error_handler_args:
    type: object
    description: The arguments to pass to the script or flow
  retry:
    type: object
    properties:
      constant:
        type: object
        description: Retry with constant delay between attempts
        properties:
          attempts:
            type: integer
            description: Number of retry attempts
          seconds:
            type: integer
            description: Seconds to wait between retries
      exponential:
        type: object
        description: Retry with exponential backoff (delay doubles each time)
        properties:
          attempts:
            type: integer
            description: Number of retry attempts
          multiplier:
            type: integer
            description: Multiplier for exponential backoff
          seconds:
            type: integer
            minimum: 1
            description: Initial delay in seconds
          random_factor:
            type: integer
            minimum: 0
            maximum: 100
            description: Random jitter percentage (0-100) to avoid thundering herd
      retry_if:
        $ref: '#/components/schemas/RetryIf'
    description: Retry configuration for failed module executions
required:
- script_path
- permissioned_as
- is_flow
- nats_resource_path
- use_jetstream
- subjects
`,
  "postgres_trigger": `type: object
properties:
  script_path:
    type: string
    description: Path to the script or flow to execute when triggered
  permissioned_as:
    type: string
    description: The user or group this trigger runs as (permissioned_as)
  is_flow:
    type: boolean
    description: True if script_path points to a flow, false if it points to a script
  labels:
    type: array
    items:
      type: string
  draft_only:
    type: boolean
    description: 'True when this row is a per-user draft with no deployed

      trigger at the same path. Set by list endpoints when

      \`include_draft_only=true\` synthesizes the row from the

      draft. Frontend renders a "Draft" badge.

      '
  is_draft:
    type: boolean
    description: 'True when the authed user has a per-user draft at this path

      (over a deployed row or a synthesized draft-only row).

      Frontend appends a \`*\` to the displayed name.

      '
  postgres_resource_path:
    type: string
    description: Path to the PostgreSQL resource containing connection configuration
  publication_name:
    type: string
    description: Name of the PostgreSQL publication to subscribe to for change data
      capture
  replication_slot_name:
    type: string
    description: Name of the PostgreSQL logical replication slot to use
  error_handler_path:
    type: string
    description: Path to a script or flow to run when the triggered job fails
  error_handler_args:
    type: object
    description: The arguments to pass to the script or flow
  retry:
    type: object
    properties:
      constant:
        type: object
        description: Retry with constant delay between attempts
        properties:
          attempts:
            type: integer
            description: Number of retry attempts
          seconds:
            type: integer
            description: Seconds to wait between retries
      exponential:
        type: object
        description: Retry with exponential backoff (delay doubles each time)
        properties:
          attempts:
            type: integer
            description: Number of retry attempts
          multiplier:
            type: integer
            description: Multiplier for exponential backoff
          seconds:
            type: integer
            minimum: 1
            description: Initial delay in seconds
          random_factor:
            type: integer
            minimum: 0
            maximum: 100
            description: Random jitter percentage (0-100) to avoid thundering herd
      retry_if:
        $ref: '#/components/schemas/RetryIf'
    description: Retry configuration for failed module executions
required:
- script_path
- permissioned_as
- is_flow
- postgres_resource_path
- replication_slot_name
- publication_name
`,
  "schedule": `type: object
properties:
  schedule:
    type: string
    description: Cron expression with 6 fields (seconds, minutes, hours, day of month,
      month, day of week). Example '0 0 12 * * *' for daily at noon
  timezone:
    type: string
    description: IANA timezone for the schedule (e.g., 'UTC', 'Europe/Paris', 'America/New_York')
  enabled:
    type: boolean
    description: Whether the schedule is currently active and will trigger jobs
  script_path:
    type: string
    description: Path to the script or flow to execute when triggered
  is_flow:
    type: boolean
    description: True if script_path points to a flow, false if it points to a script
  args:
    type: object
    description: The arguments to pass to the script or flow
  permissioned_as:
    type: string
    description: The user or group this schedule runs as (e.g., 'u/admin' or 'g/mygroup')
  on_failure:
    type: string
    description: Path to a script or flow to run when the scheduled job fails
  on_failure_times:
    type: number
    description: Number of consecutive failures before the on_failure handler is triggered
      (default 1)
  on_failure_exact:
    type: boolean
    description: If true, trigger on_failure handler only on exactly N failures, not
      on every failure after N
  on_failure_extra_args:
    type: object
    description: The arguments to pass to the script or flow
  on_recovery:
    type: string
    description: Path to a script or flow to run when the schedule recovers after
      failures
  on_recovery_times:
    type: number
    description: Number of consecutive successes before the on_recovery handler is
      triggered (default 1)
  on_recovery_extra_args:
    type: object
    description: The arguments to pass to the script or flow
  on_success:
    type: string
    description: Path to a script or flow to run after each successful execution
  on_success_extra_args:
    type: object
    description: The arguments to pass to the script or flow
  ws_error_handler_muted:
    type: boolean
    description: If true, the workspace-level error handler will not be triggered
      for this schedule's failures
  retry:
    type: object
    properties:
      constant:
        type: object
        description: Retry with constant delay between attempts
        properties:
          attempts:
            type: integer
            description: Number of retry attempts
          seconds:
            type: integer
            description: Seconds to wait between retries
      exponential:
        type: object
        description: Retry with exponential backoff (delay doubles each time)
        properties:
          attempts:
            type: integer
            description: Number of retry attempts
          multiplier:
            type: integer
            description: Multiplier for exponential backoff
          seconds:
            type: integer
            minimum: 1
            description: Initial delay in seconds
          random_factor:
            type: integer
            minimum: 0
            maximum: 100
            description: Random jitter percentage (0-100) to avoid thundering herd
      retry_if:
        $ref: '#/components/schemas/RetryIf'
    description: Retry configuration for failed module executions
  summary:
    type: string
    description: Short summary describing the purpose of this schedule
  description:
    type: string
    description: Detailed description of what this schedule does
  no_flow_overlap:
    type: boolean
    description: If true, skip this schedule's execution if the previous run is still
      in progress (prevents concurrent runs)
  tag:
    type: string
    description: Worker tag to route jobs to specific worker groups
  paused_until:
    type: string
    description: ISO 8601 datetime until which the schedule is paused. Schedule resumes
      automatically after this time
  cron_version:
    type: string
    description: Cron parser version. Use 'v2' for extended syntax with additional
      features
  dynamic_skip:
    type: string
    description: Path to a script that validates scheduled datetimes. Receives scheduled_for
      datetime and returns boolean to skip (true) or run (false)
  labels:
    type: array
    items:
      type: string
  draft_only:
    type: boolean
    description: 'True when this row is a per-user draft with no deployed

      schedule at the same path. Frontend renders a "Draft" badge.

      '
  is_draft:
    type: boolean
    description: 'True when the authed user has a per-user draft at this path

      (over a deployed row or a synthesized draft-only row).

      Frontend appends a \`*\` to the displayed name.

      '
  inherited_labels:
    type: array
    items:
      type: string
    description: 'Labels inherited from the parent folder, computed at read time.
      Read-only — edit them on the folder.

      '
required:
- schedule
- script_path
- timezone
- is_flow
- enabled
- permissioned_as
`,
  "sqs_trigger": `type: object
properties:
  script_path:
    type: string
    description: Path to the script or flow to execute when triggered
  permissioned_as:
    type: string
    description: The user or group this trigger runs as (permissioned_as)
  is_flow:
    type: boolean
    description: True if script_path points to a flow, false if it points to a script
  labels:
    type: array
    items:
      type: string
  draft_only:
    type: boolean
    description: 'True when this row is a per-user draft with no deployed

      trigger at the same path. Set by list endpoints when

      \`include_draft_only=true\` synthesizes the row from the

      draft. Frontend renders a "Draft" badge.

      '
  is_draft:
    type: boolean
    description: 'True when the authed user has a per-user draft at this path

      (over a deployed row or a synthesized draft-only row).

      Frontend appends a \`*\` to the displayed name.

      '
  queue_url:
    type: string
    description: The full URL of the AWS SQS queue to poll for messages
  aws_auth_resource_type:
    type: string
    enum:
    - oidc
    - credentials
  aws_resource_path:
    type: string
    description: Path to the AWS resource containing credentials or OIDC configuration
  message_attributes:
    type: array
    items:
      type: string
    description: Array of SQS message attribute names to include with each message
  error_handler_path:
    type: string
    description: Path to a script or flow to run when the triggered job fails
  error_handler_args:
    type: object
    description: The arguments to pass to the script or flow
  retry:
    type: object
    properties:
      constant:
        type: object
        description: Retry with constant delay between attempts
        properties:
          attempts:
            type: integer
            description: Number of retry attempts
          seconds:
            type: integer
            description: Seconds to wait between retries
      exponential:
        type: object
        description: Retry with exponential backoff (delay doubles each time)
        properties:
          attempts:
            type: integer
            description: Number of retry attempts
          multiplier:
            type: integer
            description: Multiplier for exponential backoff
          seconds:
            type: integer
            minimum: 1
            description: Initial delay in seconds
          random_factor:
            type: integer
            minimum: 0
            maximum: 100
            description: Random jitter percentage (0-100) to avoid thundering herd
      retry_if:
        $ref: '#/components/schemas/RetryIf'
    description: Retry configuration for failed module executions
required:
- script_path
- permissioned_as
- is_flow
- queue_url
- aws_resource_path
- aws_auth_resource_type
`,
  "websocket_trigger": `type: object
properties:
  script_path:
    type: string
    description: Path to the script or flow to execute when triggered
  permissioned_as:
    type: string
    description: The user or group this trigger runs as (permissioned_as)
  is_flow:
    type: boolean
    description: True if script_path points to a flow, false if it points to a script
  labels:
    type: array
    items:
      type: string
  draft_only:
    type: boolean
    description: 'True when this row is a per-user draft with no deployed

      trigger at the same path. Set by list endpoints when

      \`include_draft_only=true\` synthesizes the row from the

      draft. Frontend renders a "Draft" badge.

      '
  is_draft:
    type: boolean
    description: 'True when the authed user has a per-user draft at this path

      (over a deployed row or a synthesized draft-only row).

      Frontend appends a \`*\` to the displayed name.

      '
  url:
    type: string
    description: The WebSocket URL to connect to (can be a static URL or computed
      by a runnable)
  filters:
    type: array
    items:
      type: object
      properties:
        key:
          type: string
        value: {}
    description: Array of key-value filters to match incoming messages (only matching
      messages trigger the script)
  filter_logic:
    type: string
    enum:
    - and
    - or
    description: Logic to apply when evaluating filters. 'and' requires all filters
      to match, 'or' requires any filter to match.
  initial_messages:
    type: array
    items:
      type: object
    description: Messages to send immediately after connecting (can be raw strings
      or computed by runnables)
  url_runnable_args:
    type: object
    description: The arguments to pass to the script or flow
  can_return_message:
    type: boolean
    description: If true, the script can return a message to send back through the
      WebSocket
  can_return_error_result:
    type: boolean
    description: If true, error results are sent back through the WebSocket
  heartbeat:
    type: object
    properties:
      interval_secs:
        type: integer
        minimum: 1
        description: Interval in seconds between heartbeat messages
      message:
        type: string
        description: Message to send as heartbeat. Use {{state}} as a placeholder
          for a value extracted from incoming messages (see state_field).
      state_field:
        type: string
        description: Optional. Top-level JSON field to extract from incoming messages.
          The extracted value replaces {{state}} in the heartbeat message.
  error_handler_path:
    type: string
    description: Path to a script or flow to run when the triggered job fails
  error_handler_args:
    type: object
    description: The arguments to pass to the script or flow
  retry:
    type: object
    properties:
      constant:
        type: object
        description: Retry with constant delay between attempts
        properties:
          attempts:
            type: integer
            description: Number of retry attempts
          seconds:
            type: integer
            description: Seconds to wait between retries
      exponential:
        type: object
        description: Retry with exponential backoff (delay doubles each time)
        properties:
          attempts:
            type: integer
            description: Number of retry attempts
          multiplier:
            type: integer
            description: Multiplier for exponential backoff
          seconds:
            type: integer
            minimum: 1
            description: Initial delay in seconds
          random_factor:
            type: integer
            minimum: 0
            maximum: 100
            description: Random jitter percentage (0-100) to avoid thundering herd
      retry_if:
        $ref: '#/components/schemas/RetryIf'
    description: Retry configuration for failed module executions
required:
- script_path
- permissioned_as
- is_flow
- url
- filters
- can_return_message
- can_return_error_result
`,
};

// Maps skill names to their schema types and file patterns
export interface SchemaMapping {
  name: string;
  schemaKey: string;
  filePattern: string;
}

export const SCHEMA_MAPPINGS: Record<string, SchemaMapping[]> = {
  "triggers": [
    { name: "HttpTrigger", schemaKey: "http_trigger", filePattern: "*.http_trigger.yaml" },
    { name: "WebsocketTrigger", schemaKey: "websocket_trigger", filePattern: "*.websocket_trigger.yaml" },
    { name: "KafkaTrigger", schemaKey: "kafka_trigger", filePattern: "*.kafka_trigger.yaml" },
    { name: "NatsTrigger", schemaKey: "nats_trigger", filePattern: "*.nats_trigger.yaml" },
    { name: "PostgresTrigger", schemaKey: "postgres_trigger", filePattern: "*.postgres_trigger.yaml" },
    { name: "MqttTrigger", schemaKey: "mqtt_trigger", filePattern: "*.mqtt_trigger.yaml" },
    { name: "AmqpTrigger", schemaKey: "amqp_trigger", filePattern: "*.amqp_trigger.yaml" },
    { name: "SqsTrigger", schemaKey: "sqs_trigger", filePattern: "*.sqs_trigger.yaml" },
    { name: "GcpTrigger", schemaKey: "gcp_trigger", filePattern: "*.gcp_trigger.yaml" },
    { name: "AzureTrigger", schemaKey: "azure_trigger", filePattern: "*.azure_trigger.yaml" },
    { name: "EmailTrigger", schemaKey: "email_trigger", filePattern: "*.email_trigger.yaml" },
  ],
  "schedules": [
    { name: "Schedule", schemaKey: "schedule", filePattern: "*.schedule.yaml" },
  ],
};
