- 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'
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<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>
/**
* 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>
/**
* 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<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
* @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 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
View on GitHub