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:
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:
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
// 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:
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:
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
import * as wmill from "windmill-client";
export async function main(file: wmill.S3Object) {
const content = await wmill.loadS3File(file);
// ...
}
S3 operations
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'
The client configures itself from the job's environment — base URL, token and credentials mode
are all set before your code runs, so there is nothing to initialize and no reason to read
WM_TOKEN or BASE_INTERNAL_URL and build an API URL yourself. Reconstructing that by hand only
reintroduces details the client already handles. Call the SDK for anything Windmill, and use raw
HTTP for third-party APIs.
The helpers below are the surface to prefer. For an endpoint none of them covers, import the
generated service classes (JobService, ScriptService, ...) from 'windmill-client' — they are not
listed here but they do exist. What does not exist is a helper name you guessed at: if it is
neither listed below nor a service method, do not call it.
To know who is running the script, read the contextual variables rather than calling the API:
process.env.WM_END_USER_EMAIL || process.env.WM_EMAIL. WM_END_USER_EMAIL is the app viewer when
the run was triggered from an app and empty otherwise (both variables are always defined), WM_EMAIL
is the user the job is permissioned as. WM_USERNAME is the matching username.
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
/**
- 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
/**
- 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
/**
- 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
/**
- 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): Promise
/**
- 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
/**
- 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
/**
- Get the result of a completed job
- @param jobId - ID of the completed job
- @returns Job result
*/
async getResult(jobId: string): Promise
/**
- 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
/**
- Cancel a queued or running job by ID.
- @param jobId - UUID of the job to cancel
- @param reason - Optional reason for cancellation
- @returns Response message from the cancel endpoint
*/
async cancelJob(jobId: string, reason: string | undefined = undefined): Promise
/**
- 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
/**
- 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
/**
- 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
/**
- 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
/**
- 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
/**
- 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
/**
- 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
/**
- 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
/**
- Get a flow user state
- @param path path of the variable
*/
async getFlowUserState(key: string, errorIfNotPossible?: boolean): Promise
/**
- Get the state shared across executions
- @param path Optional state resource path override. Defaults to
getStatePath().
*/
async getState(path?: string): Promise
/**
- Get a variable by path
- @param path path of the variable
- @returns variable value
*/
async getVariable(path: string): Promise
/**
- 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
/**
- 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
async polarsConnectionSettings(s3_resource_path: string | undefined): Promise
async duckdbConnectionSettings(s3_resource_path: string | undefined): Promise
/**
- 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
/**
- Load the content of a file stored in S3. If the s3ResourcePath is undefined, it will default to the workspace S3 resource.
- 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.
- 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.
- 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
/**
- Permanently delete a file from S3 by key.
- 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
/**
- Sign S3 objects to be used by anonymous users in public apps
- @param s3objects s3 objects to sign
- @param expirySecs how long the signature stays valid, in seconds (default 43200 = 12h, clamped to [60, 604800])
- @returns signed s3 objects
*/
async signS3Objects(s3objects: S3Object[], { expirySecs }: { expirySecs?: number } = {}): Promise<S3Object[]>
/**
- Sign S3 object to be used by anonymous users in public apps
- @param s3object s3 object to sign
- @param expirySecs how long the signature stays valid, in seconds (default 43200 = 12h, clamped to [60, 604800])
- @returns signed s3 object
*/
async signS3Object(s3object: S3Object, { expirySecs }: { expirySecs?: number } = {}): Promise
/**
- 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
- @param expirySecs how long the signature stays valid, in seconds (default 43200 = 12h, clamped to [60, 604800])
- @returns list of signed public URLs
*/
async getPresignedS3PublicUrls(s3Objects: S3Object[], { baseUrl, expirySecs }: { baseUrl?: string; expirySecs?: number } = {}): 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
- @param expirySecs how long the signature stays valid, in seconds (default 43200 = 12h, clamped to [60, 604800])
- @returns signed public URL
*/
async getPresignedS3PublicUrl(s3Objects: S3Object, { baseUrl, expirySecs }: { baseUrl?: string; expirySecs?: number } = {}): Promise
/**
/**
- 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
/**
- 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
/**
- 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.
- @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} 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:
- 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
/**
- 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.
- @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} 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:
- 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
setWorkflowCtx(ctx: WorkflowCtx | null): void
async sleep(seconds: number): Promise
/**
- Execute
fn inline and checkpoint the result. On replay the cached value is
- returned without re-executing
fn.
fn's result is encoded as JSON and decoded back before it is returned, so
- the round that runs the body sees the same types every replay sees: a
Date
- comes back as a string, a
Map as {}. {@link Jsonified} is that shape.
*/
async step(name: string, fn: () => T | Promise,): Promise<Jsonified<Awaited>>
/**
- 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
/**
- 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
/**
- 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(fn: (...args: any[]) => Promise): void
/**
- Suspend the workflow and wait for an external approval.
- Pass
key to name the step, then getApprovalUrls(key) yields the URLs that
- resume exactly this approval — route them through your own channel. Without a
- key the steps are named
approval, approval_2, ...
- @example
- const urls = await step("urls", () => getApprovalUrls("manager"));
- await step("notify", () => sendEmail(urls.resume, urls.cancel));
- const { value, approver } = await waitForApproval({ key: "manager", timeout: 3600 });
*/
waitForApproval(options?: { timeout?: number; form?: object; selfApproval?: boolean; key?: string; }): PromiseLike<{ value: any; approver: string; approved: boolean }>
/**
- Resume/cancel/approval-page URLs bound to one
waitForApproval step.
- Unlike
getResumeUrls(), which signs a random nonce, these address the very
resume_job record the step's built-in approval buttons use, so they are
- stable across replays and safe to embed in a custom notification.
stepKey must match the key given to waitForApproval. Keys must be unique
- within a workflow; reusing one throws rather than silently renaming it. The URL
- only resumes while that step is awaiting approval; used at any other moment it is
- rejected rather than banking a row a different approval would consume. Send it
- ahead of time — approvers just cannot act before the workflow reaches the step.
resume and cancel are step-bound; approvalPage is not — it opens the job's
- approval page, which acts on whichever approval is pending when it is used.
- @example
- const urls = await step("urls", () => getApprovalUrls("manager"));
- await step("notify", () => sendEmail(urls.resume, urls.cancel));
- await waitForApproval({ key: "manager" });
*/
async getApprovalUrls(stepKey: string = "approval", approver?: string): Promise<{
approvalPage: string;
resume: string;
cancel: string;
}>
/**
- 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, 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
/**
- 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
/**
/**
/**
- 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
/**
- 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
1---2name: write-script-deno3description: 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.4---5
6## CLI Commands
7
8Place scripts in a folder.
9
10After writing, tell the user which command fits what they want to do:
11
12- `wmill script preview <script_path>` — **default when iterating on a local script.** Runs the local file without deploying.
13- `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.
14- `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.
15- 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".
16
17### Preview vs run — choose by intent, not habit
18
19If 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.
20
21Only use `script run` when:
22- The user explicitly says "run the deployed version" / "run what's on the server".
23- There is no local script being edited (you're just invoking an existing script).
24
25Only use `sync push` when:
26- The user explicitly asks to deploy, publish, push, or ship.
27- The preview has already validated the change and the user wants it in the workspace.
28
29### Keep metadata in sync after editing
30
31`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.
32
33This 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.
34
35With 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`.
36
37If 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.
38
39### After writing — offer to test, don't wait passively
40
41If 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.
42
43If 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.
44
45`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.
46
47For a **visual** open-the-script-in-the-dev-page preview (rather than `script preview`'s run-and-print-result), use the `preview` skill.
48
49Use `wmill resource-type list --schema` to discover available resource types.
50
51# TypeScript (Deno)
52
53Deno runtime with npm support via `npm:` prefix and native Deno libraries.
54
55**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.
56
57## Structure
58
59Export a single **async** function called `main`:
60
61```typescript
62export async function main(param1: string, param2: number) {
63 // Your code here
64 return { result: param1, count: param2 };
65}
66```
67
68Do not call the main function. Libraries are installed automatically.
69
70## Resource Types
71
72On Windmill, credentials and configuration are stored in resources and passed as parameters to main.
73
74Use the `RT` namespace for resource types:
75
76```typescript
77export async function main(stripe: RT.Stripe) {
78 // stripe contains API key and config from the resource
79}
80```
81
82Only use resource types if you need them to satisfy the instructions. Always use the RT namespace.
83
84Before 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`.
85
86## Imports
87
88```typescript
89// npm packages use npm: prefix
90import Stripe from "npm:stripe";
91import { someFunction } from "npm:some-package";
92
93// Deno standard library
94import { serve } from "https://deno.land/std/http/server.ts";
95```
96
97## Windmill Client
98
99Import the windmill client for platform interactions:
100
101```typescript
102import * as wmill from "windmill-client";
103```
104
105**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.
106
107The 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`.
108
109## Preprocessor Scripts
110
111For preprocessor scripts, the function should be named `preprocessor` and receives an `event` parameter:
112
113```typescript
114type Event = {
115 kind:
116 | "webhook"
117 | "http"
118 | "websocket"
119 | "kafka"
120 | "email"
121 | "nats"
122 | "postgres"
123 | "sqs"
124 | "mqtt"
125 | "gcp";
126 body: any;
127 headers: Record<string, string>;
128 query: Record<string, string>;
129};
130
131export async function preprocessor(event: Event) {
132 return {
133 param1: event.body.field1,
134 param2: event.query.id,
135 };
136}
137```
138
139## S3 Object Operations
140
141Windmill 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.
142
143### Receiving an S3Object as a script parameter
144
145```typescript
146import * as wmill from "windmill-client";
147
148export async function main(file: wmill.S3Object) {
149 const content = await wmill.loadS3File(file);
150 // ...
151}
152```
153
154### S3 operations
155
156```typescript
157import * as wmill from "windmill-client";
158
159// Load file content from S3
160const content: Uint8Array = await wmill.loadS3File(s3object);
161
162// Load file as stream
163const blob: Blob = await wmill.loadS3FileStream(s3object);
164
165// Write file to S3
166const result: wmill.S3Object = await wmill.writeS3File(
167 s3object, // Target path (or undefined to auto-generate)
168 fileContent, // string or Blob
169 s3ResourcePath // Optional: specific S3 resource to use
170);
171```
172
173
174# TypeScript SDK (windmill-client)
175
176Import: import * as wmill from 'windmill-client'
177
178The client configures itself from the job's environment — base URL, token and credentials mode
179are all set before your code runs, so there is nothing to initialize and no reason to read
180WM_TOKEN or BASE_INTERNAL_URL and build an API URL yourself. Reconstructing that by hand only
181reintroduces details the client already handles. Call the SDK for anything Windmill, and use raw
182HTTP for third-party APIs.
183
184The helpers below are the surface to prefer. For an endpoint none of them covers, import the
185generated service classes (JobService, ScriptService, ...) from 'windmill-client' — they are not
186listed here but they do exist. What does not exist is a helper name you guessed at: if it is
187neither listed below nor a service method, do not call it.
188
189To know who is running the script, read the contextual variables rather than calling the API:
190`process.env.WM_END_USER_EMAIL || process.env.WM_EMAIL`. WM_END_USER_EMAIL is the app viewer when
191the run was triggered from an app and empty otherwise (both variables are always defined), WM_EMAIL
192is the user the job is permissioned as. WM_USERNAME is the matching username.
193
194workerHasInternalServer(): boolean
195
196/**
197 * Initialize the Windmill client with authentication token and base URL
198 * @param token - Authentication token (defaults to WM_TOKEN env variable)
199 * @param baseUrl - API base URL (defaults to BASE_INTERNAL_URL or BASE_URL env variable)
200 */
201setClient(token?: string, baseUrl?: string): void
202
203/**
204 * Create a client configuration from env variables
205 * @returns client configuration
206 */
207getWorkspace(): string
208
209/**
210 * Get a resource value by path
211 * @param path path of the resource, default to internal state path
212 * @param undefinedIfEmpty if the resource does not exist, return undefined instead of throwing an error
213 * @returns resource value
214 */
215async getResource(path?: string, undefinedIfEmpty?: boolean): Promise<any>
216
217/**
218 * Get the true root job id
219 * @param jobId job id to get the root job id from (default to current job)
220 * @returns root job id
221 */
222async getRootJobId(jobId?: string): Promise<string>
223
224/**
225 * Run a script synchronously by its path and wait for the result
226 * @param path - Script path in Windmill
227 * @param args - Arguments to pass to the script
228 * @param verbose - Enable verbose logging
229 * @param tag - Override the worker tag the job runs on
230 * @returns Script execution result
231 */
232async runScriptByPath(path: string, args: Record<string, any> | null = null, verbose: boolean = false, tag: string | null = null): Promise<any>
233
234/**
235 * Run a script synchronously by its hash and wait for the result
236 * @param hash_ - Script hash in Windmill
237 * @param args - Arguments to pass to the script
238 * @param verbose - Enable verbose logging
239 * @param tag - Override the worker tag the job runs on
240 * @returns Script execution result
241 */
242async runScriptByHash(hash_: string, args: Record<string, any> | null = null, verbose: boolean = false, tag: string | null = null): Promise<any>
243
244/**
245 * Append a text to the result stream
246 * @param text text to append to the result stream
247 */
248appendToResultStream(text: string): void
249
250/**
251 * Stream to the result stream
252 * @param stream stream to stream to the result stream
253 */
254async streamResult(stream: AsyncIterable<string>): Promise<void>
255
256/**
257 * Run a flow synchronously by its path and wait for the result
258 * @param path - Flow path in Windmill
259 * @param args - Arguments to pass to the flow
260 * @param verbose - Enable verbose logging
261 * @param tag - Override the worker tag the job runs on
262 * @returns Flow execution result
263 */
264async runFlow(path: string | null = null, args: Record<string, any> | null = null, verbose: boolean = false, tag: string | null = null): Promise<any>
265
266/**
267 * Wait for a job to complete and return its result
268 * @param jobId - ID of the job to wait for
269 * @param verbose - Enable verbose logging
270 * @returns Job result when completed
271 */
272async waitJob(jobId: string, verbose: boolean = false): Promise<any>
273
274/**
275 * Get the result of a completed job
276 * @param jobId - ID of the completed job
277 * @returns Job result
278 */
279async getResult(jobId: string): Promise<any>
280
281/**
282 * Get the result of a job if completed, or its current status
283 * @param jobId - ID of the job
284 * @returns Object with started, completed, success, and result properties
285 */
286async getResultMaybe(jobId: string): Promise<any>
287
288/**
289 * Cancel a queued or running job by ID.
290 * @param jobId - UUID of the job to cancel
291 * @param reason - Optional reason for cancellation
292 * @returns Response message from the cancel endpoint
293 */
294async cancelJob(jobId: string, reason: string | undefined = undefined): Promise<string>
295
296/**
297 * Run a script asynchronously by its path
298 * @param path - Script path in Windmill
299 * @param args - Arguments to pass to the script
300 * @param scheduledInSeconds - Schedule execution for a future time (in seconds)
301 * @param tag - Override the worker tag the job runs on
302 * @returns Job ID of the created job
303 */
304async runScriptByPathAsync(path: string, args: Record<string, any> | null = null, scheduledInSeconds: number | null = null, tag: string | null = null): Promise<string>
305
306/**
307 * Run a script asynchronously by its hash
308 * @param hash_ - Script hash in Windmill
309 * @param args - Arguments to pass to the script
310 * @param scheduledInSeconds - Schedule execution for a future time (in seconds)
311 * @param tag - Override the worker tag the job runs on
312 * @returns Job ID of the created job
313 */
314async runScriptByHashAsync(hash_: string, args: Record<string, any> | null = null, scheduledInSeconds: number | null = null, tag: string | null = null): Promise<string>
315
316/**
317 * Run a flow asynchronously by its path
318 * @param path - Flow path in Windmill
319 * @param args - Arguments to pass to the flow
320 * @param scheduledInSeconds - Schedule execution for a future time (in seconds)
321 * @param doNotTrackInParent - If false, tracks state in parent job (only use when fully awaiting the job)
322 * @param tag - Override the worker tag the job runs on
323 * @returns Job ID of the created job
324 */
325async 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>
326
327/**
328 * Resolve a resource value in case the default value was picked because the input payload was undefined
329 * @param obj resource value or path of the resource under the format `$res:path`
330 * @returns resource value
331 */
332async resolveDefaultResource(obj: any): Promise<any>
333
334/**
335 * Get the state file path from environment variables
336 * @returns State path string
337 */
338getStatePath(): string
339
340/**
341 * Set a resource value by path
342 * @param path path of the resource to set, default to state path
343 * @param value new value of the resource to set
344 * @param initializeToTypeIfNotExist if the resource does not exist, initialize it with this type
345 */
346async setResource(value: any, path?: string, initializeToTypeIfNotExist?: string): Promise<void>
347
348/**
349 * Set the state
350 * @param state state to set
351 * @param path Optional state resource path override. Defaults to `getStatePath()`.
352 */
353async setState(state: any, path?: string): Promise<void>
354
355/**
356 * Set the progress
357 * Progress cannot go back and limited to 0% to 99% range
358 * @param percent Progress to set in %
359 * @param jobId? Job to set progress for
360 */
361async setProgress(percent: number, jobId?: any): Promise<void>
362
363/**
364 * Get the progress
365 * @param jobId? Job to get progress from
366 * @returns Optional clamped between 0 and 100 progress value
367 */
368async getProgress(jobId?: any): Promise<number | null>
369
370/**
371 * Set a flow user state
372 * @param key key of the state
373 * @param value value of the state
374 */
375async setFlowUserState(key: string, value: any, errorIfNotPossible?: boolean): Promise<void>
376
377/**
378 * Get a flow user state
379 * @param path path of the variable
380 */
381async getFlowUserState(key: string, errorIfNotPossible?: boolean): Promise<any>
382
383/**
384 * Get the state shared across executions
385 * @param path Optional state resource path override. Defaults to `getStatePath()`.
386 */
387async getState(path?: string): Promise<any>
388
389/**
390 * Get a variable by path
391 * @param path path of the variable
392 * @returns variable value
393 */
394async getVariable(path: string): Promise<string>
395
396/**
397 * Set a variable by path, create if not exist
398 * @param path path of the variable
399 * @param value value of the variable
400 * @param isSecretIfNotExist if the variable does not exist, create it as secret or not (default: false)
401 * @param descriptionIfNotExist if the variable does not exist, create it with this description (default: "")
402 */
403async setVariable(path: string, value: string, isSecretIfNotExist?: boolean, descriptionIfNotExist?: string): Promise<void>
404
405/**
406 * Build a PostgreSQL connection URL from a database resource
407 * @param path - Path to the database resource
408 * @returns PostgreSQL connection URL string
409 */
410async databaseUrlFromResource(path: string): Promise<string>
411
412async polarsConnectionSettings(s3_resource_path: string | undefined): Promise<any>
413
414async duckdbConnectionSettings(s3_resource_path: string | undefined): Promise<any>
415
416/**
417 * Get S3 client settings from a resource or workspace default
418 * @param s3_resource_path - Path to S3 resource (uses workspace default if undefined)
419 * @param workspace - Workspace to read from (defaults to the `WM_WORKSPACE` env var)
420 * @returns S3 client configuration settings
421 */
422async denoS3LightClientSettings(s3_resource_path: string | undefined, workspace: string | undefined = undefined): Promise<DenoS3LightClientSettings>
423
424/**
425 * Load the content of a file stored in S3. If the s3ResourcePath is undefined, it will default to the workspace S3 resource.
426 *
427 * ```typescript
428 * let fileContent = await wmill.loadS3FileContent(inputFile)
429 * // if the file is a raw text file, it can be decoded and printed directly:
430 * const text = new TextDecoder().decode(fileContentStream)
431 * console.log(text);
432 * ```
433 *
434 * @param workspace - Workspace to read from (defaults to the `WM_WORKSPACE` env var)
435 */
436async loadS3File(s3object: S3Object, s3ResourcePath: string | undefined = undefined, workspace: string | undefined = undefined): Promise<Uint8Array | undefined>
437
438/**
439 * 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.
440 *
441 * ```typescript
442 * let fileContentBlob = await wmill.loadS3FileStream(inputFile)
443 * // if the content is plain text, the blob can be read directly:
444 * console.log(await fileContentBlob.text());
445 * ```
446 *
447 * @param workspace - Workspace to read from (defaults to the `WM_WORKSPACE` env var)
448 */
449async loadS3FileStream(s3object: S3Object, s3ResourcePath: string | undefined = undefined, workspace: string | undefined = undefined): Promise<Blob | undefined>
450
451/**
452 * Persist a file to the S3 bucket. If the s3ResourcePath is undefined, it will default to the workspace S3 resource.
453 *
454 * ```typescript
455 * const s3object = await writeS3File(s3Object, "Hello Windmill!")
456 * const fileContentAsUtf8Str = (await s3object.toArray()).toString('utf-8')
457 * console.log(fileContentAsUtf8Str)
458 * ```
459 *
460 * @param workspace - Workspace to write to (defaults to the `WM_WORKSPACE` env var)
461 */
462async 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>
463
464/**
465 * Permanently delete a file from S3 by key.
466 *
467 * ```typescript
468 * await wmill.deleteS3File({ s3: "path/to/file.txt" })
469 * ```
470 *
471 * @param s3object - S3 object identifying the file to delete (must have `s3` set)
472 * @param workspace - Workspace to delete from (defaults to the `WM_WORKSPACE` env var)
473 */
474async deleteS3File(s3object: S3Object, workspace: string | undefined = undefined): Promise<void>
475
476/**
477 * Sign S3 objects to be used by anonymous users in public apps
478 * @param s3objects s3 objects to sign
479 * @param expirySecs how long the signature stays valid, in seconds (default 43200 = 12h, clamped to [60, 604800])
480 * @returns signed s3 objects
481 */
482async signS3Objects(s3objects: S3Object[], { expirySecs }: { expirySecs?: number } = {}): Promise<S3Object[]>
483
484/**
485 * Sign S3 object to be used by anonymous users in public apps
486 * @param s3object s3 object to sign
487 * @param expirySecs how long the signature stays valid, in seconds (default 43200 = 12h, clamped to [60, 604800])
488 * @returns signed s3 object
489 */
490async signS3Object(s3object: S3Object, { expirySecs }: { expirySecs?: number } = {}): Promise<S3Object>
491
492/**
493 * Generate a presigned public URL for an array of S3 objects.
494 * If an S3 object is not signed yet, it will be signed first.
495 * @param s3Objects s3 objects to sign
496 * @param expirySecs how long the signature stays valid, in seconds (default 43200 = 12h, clamped to [60, 604800])
497 * @returns list of signed public URLs
498 */
499async getPresignedS3PublicUrls(s3Objects: S3Object[], { baseUrl, expirySecs }: { baseUrl?: string; expirySecs?: number } = {}): Promise<string[]>
500
501/**
502 * Generate a presigned public URL for an S3 object. If the S3 object is not signed yet, it will be signed first.
503 * @param s3Object s3 object to sign
504 * @param expirySecs how long the signature stays valid, in seconds (default 43200 = 12h, clamped to [60, 604800])
505 * @returns signed public URL
506 */
507async getPresignedS3PublicUrl(s3Objects: S3Object, { baseUrl, expirySecs }: { baseUrl?: string; expirySecs?: number } = {}): Promise<string>
508
509/**
510 * Get URLs needed for resuming a flow after this step
511 * @param approver approver name
512 * @param flowLevel if true, generate resume URLs for the parent flow instead of the specific step.
513 * This allows pre-approvals that can be consumed by any later suspend step in the same flow.
514 * @returns approval page UI URL, resume and cancel API URLs for resuming the flow
515 */
516async getResumeUrls(approver?: string, flowLevel?: boolean): Promise<{
517 approvalPage: string;
518 resume: string;
519 cancel: string;
520}>
521
522/**
523 * Get an OIDC jwt token for auth to external services (e.g: Vault, AWS) (ee only)
524 * @param audience audience of the token
525 * @param expiresIn Optional number of seconds until the token expires
526 * @returns jwt token
527 */
528async getIdToken(audience: string, expiresIn?: number): Promise<string>
529
530/**
531 * Convert a base64-encoded string to Uint8Array
532 * @param data - Base64-encoded string
533 * @returns Decoded Uint8Array
534 */
535base64ToUint8Array(data: string): Uint8Array
536
537/**
538 * Convert a Uint8Array to base64-encoded string
539 * @param arrayBuffer - Uint8Array to encode
540 * @returns Base64-encoded string
541 */
542uint8ArrayToBase64(arrayBuffer: Uint8Array): string
543
544/**
545 * Sends an interactive approval request via Slack, allowing optional customization of the message, approver, and form fields.
546 *
547 * **[Enterprise Edition Only]** To include form fields in the Slack approval request, go to **Advanced -> Suspend -> Form**
548 * and define a form. Learn more at [Windmill Documentation](https://www.windmill.dev/docs/flows/flow_approval#form).
549 *
550 * @param {Object} options - The configuration options for the Slack approval request.
551 * @param {string} options.slackResourcePath - The path to the Slack resource in Windmill.
552 * @param {string} options.channelId - The Slack channel ID where the approval request will be sent.
553 * @param {string} [options.message] - Optional custom message to include in the Slack approval request.
554 * @param {string} [options.approver] - Optional user ID or name of the approver for the request.
555 * @param {DefaultArgs} [options.defaultArgsJson] - Optional object defining or overriding the default arguments to a form field.
556 * @param {Enums} [options.dynamicEnumsJson] - Optional object overriding the enum default values of an enum form field.
557 * @param {string} [options.resumeButtonText] - Optional text for the resume button.
558 * @param {string} [options.cancelButtonText] - Optional text for the cancel button.
559 *
560 * @returns {Promise<void>} Resolves when the Slack approval request is successfully sent.
561 *
562 * @throws {Error} If the function is not called within a flow or flow preview.
563 * @throws {Error} If the `JobService.getSlackApprovalPayload` call fails.
564 *
565 * **Usage Example:**
566 * ```typescript
567 * await requestInteractiveSlackApproval({
568 * slackResourcePath: "/u/alex/my_slack_resource",
569 * channelId: "admins-slack-channel",
570 * message: "Please approve this request",
571 * approver: "approver123",
572 * defaultArgsJson: { key1: "value1", key2: 42 },
573 * dynamicEnumsJson: { foo: ["choice1", "choice2"], bar: ["optionA", "optionB"] },
574 * resumeButtonText: "Resume",
575 * cancelButtonText: "Cancel",
576 * });
577 * ```
578 *
579 * **Note:** This function requires execution within a Windmill flow or flow preview.
580 */
581async requestInteractiveSlackApproval({ slackResourcePath, channelId, message, approver, defaultArgsJson, dynamicEnumsJson, resumeButtonText, cancelButtonText, }: SlackApprovalOptions): Promise<void>
582
583/**
584 * Sends an interactive approval request via Teams, allowing optional customization of the message, approver, and form fields.
585 *
586 * **[Enterprise Edition Only]** To include form fields in the Teams approval request, go to **Advanced -> Suspend -> Form**
587 * and define a form. Learn more at [Windmill Documentation](https://www.windmill.dev/docs/flows/flow_approval#form).
588 *
589 * @param {Object} options - The configuration options for the Teams approval request.
590 * @param {string} options.teamName - The Teams team name where the approval request will be sent.
591 * @param {string} options.channelName - The Teams channel name where the approval request will be sent.
592 * @param {string} [options.message] - Optional custom message to include in the Teams approval request.
593 * @param {string} [options.approver] - Optional user ID or name of the approver for the request.
594 * @param {DefaultArgs} [options.defaultArgsJson] - Optional object defining or overriding the default arguments to a form field.
595 * @param {Enums} [options.dynamicEnumsJson] - Optional object overriding the enum default values of an enum form field.
596 *
597 * @returns {Promise<void>} Resolves when the Teams approval request is successfully sent.
598 *
599 * @throws {Error} If the function is not called within a flow or flow preview.
600 * @throws {Error} If the `JobService.getTeamsApprovalPayload` call fails.
601 *
602 * **Usage Example:**
603 * ```typescript
604 * await requestInteractiveTeamsApproval({
605 * teamName: "admins-teams",
606 * channelName: "admins-teams-channel",
607 * message: "Please approve this request",
608 * approver: "approver123",
609 * defaultArgsJson: { key1: "value1", key2: 42 },
610 * dynamicEnumsJson: { foo: ["choice1", "choice2"], bar: ["optionA", "optionB"] },
611 * });
612 * ```
613 *
614 * **Note:** This function requires execution within a Windmill flow or flow preview.
615 */
616async requestInteractiveTeamsApproval({ teamName, channelName, message, approver, defaultArgsJson, dynamicEnumsJson, }: TeamsApprovalOptions): Promise<void>
617
618setWorkflowCtx(ctx: WorkflowCtx | null): void
619
620async sleep(seconds: number): Promise<void>
621
622/**
623 * Execute `fn` inline and checkpoint the result. On replay the cached value is
624 * returned without re-executing `fn`.
625 *
626 * `fn`'s result is encoded as JSON and decoded back before it is returned, so
627 * the round that runs the body sees the same types every replay sees: a `Date`
628 * comes back as a string, a `Map` as `{}`. {@link Jsonified} is that shape.
629 */
630async step<T>(name: string, fn: () => T | Promise<T>,): Promise<Jsonified<Awaited<T>>>
631
632/**
633 * Create a task that dispatches to a separate Windmill script.
634 *
635 * @example
636 * const extract = taskScript("f/data/extract");
637 * // inside workflow: await extract({ url: "https://..." })
638 */
639taskScript(path: string, options?: TaskOptions): (...args: any[]) => PromiseLike<any>
640
641/**
642 * Create a task that dispatches to a separate Windmill flow.
643 *
644 * @example
645 * const pipeline = taskFlow("f/etl/pipeline");
646 * // inside workflow: await pipeline({ input: data })
647 */
648taskFlow(path: string, options?: TaskOptions): (...args: any[]) => PromiseLike<any>
649
650/**
651 * Mark an async function as a workflow-as-code entry point.
652 *
653 * The function must be **deterministic**: given the same inputs it must call
654 * tasks in the same order on every replay. Branching on task results is fine
655 * (results are replayed from checkpoint), but branching on external state
656 * (current time, random values, external API calls) must use `step()` to
657 * checkpoint the value so replays see the same result.
658 */
659workflow<T>(fn: (...args: any[]) => Promise<T>): void
660
661/**
662 * Suspend the workflow and wait for an external approval.
663 *
664 * Pass `key` to name the step, then `getApprovalUrls(key)` yields the URLs that
665 * resume exactly this approval — route them through your own channel. Without a
666 * key the steps are named `approval`, `approval_2`, ...
667 *
668 * @example
669 * const urls = await step("urls", () => getApprovalUrls("manager"));
670 * await step("notify", () => sendEmail(urls.resume, urls.cancel));
671 * const { value, approver } = await waitForApproval({ key: "manager", timeout: 3600 });
672 */
673waitForApproval(options?: { timeout?: number; form?: object; selfApproval?: boolean; key?: string; }): PromiseLike<{ value: any; approver: string; approved: boolean }>
674
675/**
676 * Resume/cancel/approval-page URLs bound to one `waitForApproval` step.
677 *
678 * Unlike `getResumeUrls()`, which signs a random nonce, these address the very
679 * `resume_job` record the step's built-in approval buttons use, so they are
680 * stable across replays and safe to embed in a custom notification.
681 *
682 * `stepKey` must match the `key` given to `waitForApproval`. Keys must be unique
683 * within a workflow; reusing one throws rather than silently renaming it. The URL
684 * only resumes while that step is awaiting approval; used at any other moment it is
685 * rejected rather than banking a row a different approval would consume. Send it
686 * ahead of time — approvers just cannot act before the workflow reaches the step.
687 *
688 * `resume` and `cancel` are step-bound; `approvalPage` is not — it opens the job's
689 * approval page, which acts on whichever approval is pending when it is used.
690 *
691 * @example
692 * const urls = await step("urls", () => getApprovalUrls("manager"));
693 * await step("notify", () => sendEmail(urls.resume, urls.cancel));
694 * await waitForApproval({ key: "manager" });
695 */
696async getApprovalUrls(stepKey: string = "approval", approver?: string): Promise<{
697 approvalPage: string;
698 resume: string;
699 cancel: string;
700}>
701
702/**
703 * Process items in parallel with optional concurrency control.
704 *
705 * Each item is processed by calling `fn(item)`, which should be a task().
706 * Items are dispatched in batches of `concurrency` (default: all at once).
707 *
708 * @example
709 * const process = task(async (item: string) => { ... });
710 * const results = await parallel(items, process, { concurrency: 5 });
711 */
712async parallel<T, R>(items: T[], fn: (item: T) => PromiseLike<R> | R, options?: { concurrency?: number },): Promise<R[]>
713
714/**
715 * Commit Kafka offsets for a trigger with auto_commit disabled.
716 * @param triggerPath - Path to the Kafka trigger (from event.wm_trigger.trigger_path)
717 * @param topic - Kafka topic name (from event.topic)
718 * @param partition - Partition number (from event.partition)
719 * @param offset - Message offset to commit (from event.offset)
720 */
721async commitKafkaOffsets(triggerPath: string, topic: string, partition: number, offset: number,): Promise<void>
722
723/**
724 * Parse an S3 object from URI string or record format
725 * @param s3Object - S3 object as URI string (`s3://storage/key`, `s3:///key`
726 * for the default storage) or record. Any other string throws rather than
727 * falling back to an auto-generated key: an auto key is requested by
728 * omitting the object, and a fallback would silently misplace the upload
729 * on any typo.
730 * @returns S3 object record with storage and s3 key
731 */
732parseS3Object(s3Object: S3Object): S3ObjectRecord
733
734/**
735 * Create a SQL template function for PostgreSQL/datatable queries
736 * @param name - Database/datatable name (default: "main")
737 * @returns SQL template function for building parameterized queries
738 * @example
739 * let sql = wmill.datatable()
740 * let name = 'Robin'
741 * let age = 21
742 * await sql`
743 * SELECT * FROM friends
744 * WHERE name = ${name} AND age = ${age}::int
745 * `.fetch()
746 */
747datatable(name: string = "main"): DatatableSqlTemplateFunction
748
749/**
750 * Create a SQL template function for DuckDB/ducklake queries
751 * @param name - DuckDB database name, optionally with a schema as `name:schema` (default: "main")
752 * @returns SQL template function for building parameterized queries
753 * @example
754 * let sql = wmill.ducklake()
755 * let name = 'Robin'
756 * let age = 21
757 * await sql`
758 * SELECT * FROM friends
759 * WHERE name = ${name} AND age = ${age}
760 * `.fetch()
761 * @example
762 * // Target a specific schema within the ducklake
763 * let sql = wmill.ducklake("my_lake:analytics")
764 */
765ducklake(name: string = "main"): SqlTemplateFunction
766
767/**
768 * Idempotently materialize `selectSql` into a ducklake table for one
769 * partition (or the whole table when `partition` is omitted) — the client-side
770 * equivalent of the `// materialize` engine.
771 * With `uniqueKey` it upserts the slice (delete-by-key + insert); otherwise it
772 * replaces it (whole table → `CREATE OR REPLACE`; partition → delete + insert).
773 * Safe to re-run for the same partition (backfill / failure-recovery).
774 *
775 * Returns a lazy statement — call `.execute()` to run it:
776 * `await wmill.upsertPartition({ table, selectSql, partition }).execute()`.
777 */
778upsertPartition(opts: DucklakeMaterializeOptions): SqlStatement<any>
779
780/**
781 * INSERT-only materialization (no dedup/replace) for append-only tables.
782 * Re-running the same partition duplicates rows — use only for immutable
783 * event-log sources.
784 *
785 * Returns a lazy statement — call `.execute()` to run it:
786 * `await wmill.appendPartition({ table, selectSql, partition }).execute()`.
787 */
788appendPartition(opts: Omit<DucklakeMaterializeOptions, "uniqueKey">,): SqlStatement<any>