feat(ai): standardize and improve system prompts (#7346)

* init

* test in frontend

* copy files

* use in cli

* better

* add desc to sdks

* better

* fix ts parsing

* add docs to ts client

* add docs to python client

* use script prompt in frontend

* regen

* use in flow

* rm

* use in cli, create AGENTS.md instead of cursor rules

* remove apply

* better

* better

* simplify cli

* more docs

* cleaning

* update readme

* generate cli file

* better folder names

* fix ts

* fix multiline
This commit is contained in:
centdix
2025-12-12 18:26:19 +01:00
committed by GitHub
parent ce1f673a46
commit af200de3c4
57 changed files with 11811 additions and 1301 deletions
Generated
+4
View File
@@ -65,6 +65,7 @@
"jsr:@windmill-labs/cliffy-table@1.0.0-rc.5": "1.0.0-rc.5",
"jsr:@windmill-labs/cliffy-table@^1.0.0-rc.5": "1.0.0-rc.5",
"jsr:@windmill-labs/shared-utils@1.0.10": "1.0.10",
"jsr:@windmill-labs/shared-utils@1.0.11": "1.0.11",
"jsr:@windmill-labs/shared-utils@1.0.3": "1.0.3",
"jsr:@windmill-labs/shared-utils@1.0.5": "1.0.5",
"jsr:@windmill-labs/shared-utils@1.0.6": "1.0.6",
@@ -363,6 +364,9 @@
},
"@windmill-labs/shared-utils@1.0.10": {
"integrity": "bd1993eb8d693c8ba49da1618f82ff4601eeb59011b2cac13e664291f7a299d8"
},
"@windmill-labs/shared-utils@1.0.11": {
"integrity": "4878a841480ad98213759495d72d40be1aebbbacc693f8aa9fc649127722580b"
}
},
"npm": {
+1 -1
View File
@@ -29,7 +29,7 @@ import {
loadRunnablesFromBackend,
writeRunnableToBackend,
} from "./raw_apps.ts";
import { replaceInlineScripts, AppFile as NormalAppFile } from "./apps.ts";
import { replaceInlineScripts, AppFile as NormalAppFile } from "./app.ts";
import {
newPathAssigner,
newRawAppPathAssigner,
+1 -1
View File
@@ -28,7 +28,7 @@ import * as wmill from "../../../gen/services.gen.ts";
import { resolveWorkspace } from "../../core/context.ts";
import { requireLogin } from "../../core/auth.ts";
import { GLOBAL_CONFIG_OPT } from "../../core/conf.ts";
import { replaceInlineScripts } from "./apps.ts";
import { replaceInlineScripts } from "./app.ts";
import { Runnable } from "./metadata.ts";
import {
APP_BACKEND_FOLDER,
+1 -1
View File
@@ -15,7 +15,7 @@ import path from "node:path";
import { GlobalOptions, isSuperset } from "../../types.ts";
import { replaceInlineScripts, repopulateFields } from "./apps.ts";
import { replaceInlineScripts, repopulateFields } from "./app.ts";
import { createBundle, detectFrameworks } from "./bundle.ts";
import { APP_BACKEND_FOLDER } from "./app_metadata.ts";
import { writeIfChanged } from "../../utils/utils.ts";
@@ -1,2 +1,60 @@
export { pullGitSyncSettings, pushGitSyncSettings } from "./index.ts";
export { default } from "./index.ts";
import { Command } from "../../../deps.ts";
import { pullGitSyncSettings } from "./pull.ts";
import { pushGitSyncSettings } from "./push.ts";
const command = new Command()
.description(
"Manage git-sync settings between local wmill.yaml and Windmill backend",
)
.command("pull")
.description(
"Pull git-sync settings from Windmill backend to local wmill.yaml",
)
.option(
"--repository <repo:string>",
"Specify repository path (e.g., u/user/repo)",
)
.option(
"--default",
"Write settings to top-level defaults instead of overrides",
)
.option("--replace", "Replace existing settings (non-interactive mode)")
.option(
"--override",
"Add branch-specific override (non-interactive mode)",
)
.option("--diff", "Show differences without applying changes")
.option("--json-output", "Output in JSON format")
.option(
"--with-backend-settings <json:string>",
"Use provided JSON settings instead of querying backend (for testing)",
)
.option("--yes", "Skip interactive prompts and use default behavior")
.option(
"--promotion <branch:string>",
"Use promotionOverrides from the specified branch instead of regular overrides"
)
.action(pullGitSyncSettings as any)
.command("push")
.description(
"Push git-sync settings from local wmill.yaml to Windmill backend",
)
.option(
"--repository <repo:string>",
"Specify repository path (e.g., u/user/repo)",
)
.option("--diff", "Show what would be pushed without applying changes")
.option("--json-output", "Output in JSON format")
.option(
"--with-backend-settings <json:string>",
"Use provided JSON settings instead of querying backend (for testing)",
)
.option("--yes", "Skip interactive prompts and use default behavior")
.option(
"--promotion <branch:string>",
"Use promotionOverrides from the specified branch instead of regular overrides"
)
.action(pushGitSyncSettings as any);
export { pullGitSyncSettings, pushGitSyncSettings };
export default command;
+2 -60
View File
@@ -1,60 +1,2 @@
import { Command } from "../../../deps.ts";
import { pullGitSyncSettings } from "./pull.ts";
import { pushGitSyncSettings } from "./push.ts";
const command = new Command()
.description(
"Manage git-sync settings between local wmill.yaml and Windmill backend",
)
.command("pull")
.description(
"Pull git-sync settings from Windmill backend to local wmill.yaml",
)
.option(
"--repository <repo:string>",
"Specify repository path (e.g., u/user/repo)",
)
.option(
"--default",
"Write settings to top-level defaults instead of overrides",
)
.option("--replace", "Replace existing settings (non-interactive mode)")
.option(
"--override",
"Add branch-specific override (non-interactive mode)",
)
.option("--diff", "Show differences without applying changes")
.option("--json-output", "Output in JSON format")
.option(
"--with-backend-settings <json:string>",
"Use provided JSON settings instead of querying backend (for testing)",
)
.option("--yes", "Skip interactive prompts and use default behavior")
.option(
"--promotion <branch:string>",
"Use promotionOverrides from the specified branch instead of regular overrides"
)
.action(pullGitSyncSettings as any)
.command("push")
.description(
"Push git-sync settings from local wmill.yaml to Windmill backend",
)
.option(
"--repository <repo:string>",
"Specify repository path (e.g., u/user/repo)",
)
.option("--diff", "Show what would be pushed without applying changes")
.option("--json-output", "Output in JSON format")
.option(
"--with-backend-settings <json:string>",
"Use provided JSON settings instead of querying backend (for testing)",
)
.option("--yes", "Skip interactive prompts and use default behavior")
.option(
"--promotion <branch:string>",
"Use promotionOverrides from the specified branch instead of regular overrides"
)
.action(pushGitSyncSettings as any);
export { pullGitSyncSettings, pushGitSyncSettings };
export default command;
export { pullGitSyncSettings, pushGitSyncSettings } from "./gitsync-settings.ts";
export { default } from "./gitsync-settings.ts";
+15 -21
View File
@@ -5,6 +5,7 @@ import { SCRIPT_GUIDANCE } from "../../guidance/script_guidance.ts";
import { FLOW_GUIDANCE } from "../../guidance/flow_guidance.ts";
import { getActiveWorkspaceOrFallback } from "../workspace/workspace.ts";
import { generateRTNamespace } from "../resource-type/resource-type.ts";
import { CLI_COMMANDS } from "../../guidance/prompts.ts";
export interface InitOptions {
useDefault?: boolean;
@@ -240,28 +241,12 @@ async function initAction(opts: InitOptions) {
try {
const scriptGuidanceContent = SCRIPT_GUIDANCE;
const flowGuidanceContent = FLOW_GUIDANCE;
const cliCommandsContent = CLI_COMMANDS;
// Create .cursor/rules directory
await Deno.mkdir(".cursor/rules", { recursive: true });
// Create windmill.mdc file
if (!(await Deno.stat(".cursor/rules/script.mdc").catch(() => null))) {
// Create AGENTS.md file
if (!(await Deno.stat("AGENTS.md").catch(() => null))) {
await Deno.writeTextFile(
".cursor/rules/script.mdc",
scriptGuidanceContent
);
log.info(colors.green("Created .cursor/rules/script.mdc"));
}
if (!(await Deno.stat(".cursor/rules/flow.mdc").catch(() => null))) {
await Deno.writeTextFile(".cursor/rules/flow.mdc", flowGuidanceContent);
log.info(colors.green("Created .cursor/rules/flow.mdc"));
}
// Create CLAUDE.md file
if (!(await Deno.stat("CLAUDE.md").catch(() => null))) {
await Deno.writeTextFile(
"CLAUDE.md",
"AGENTS.md",
`
You are a helpful assistant that can help with Windmill scripts and flows creation.
@@ -270,8 +255,17 @@ ${scriptGuidanceContent}
## Flow Guidance
${flowGuidanceContent}
`
## CLI Commands
${cliCommandsContent}
`
);
log.info(colors.green("Created AGENTS.md"));
}
// Create CLAUDE.md file, referencing AGENTS.md
if (!(await Deno.stat("CLAUDE.md").catch(() => null))) {
await Deno.writeTextFile("CLAUDE.md", "Instructions are in @AGENTS.md");
log.info(colors.green("Created CLAUDE.md"));
}
} catch (error) {
+1 -1
View File
@@ -75,7 +75,7 @@ import {
} from "../../../windmill-utils-internal/src/path-utils/path-assigner.ts";
import { extractInlineScripts as extractInlineScriptsForFlows } from "../../../windmill-utils-internal/src/inline-scripts/extractor.ts";
import { generateFlowLockInternal } from "../flow/flow_metadata.ts";
import { isExecutionModeAnonymous } from "../app/apps.ts";
import { isExecutionModeAnonymous } from "../app/app.ts";
import {
APP_BACKEND_FOLDER,
generateAppLocksInternal,
+1 -1
View File
@@ -5,7 +5,7 @@ import { AIConfig, Config, GlobalSetting } from "../../gen/types.gen.ts";
import { compareInstanceObjects, InstanceSyncOptions } from "../commands/instance/instance.ts";
import { isSuperset } from "../types.ts";
import { deepEqual } from "../utils/utils.ts";
import { removeWorkerPrefix } from "../commands/worker-groups/worker_groups.ts";
import { removeWorkerPrefix } from "../commands/worker-groups/worker-groups.ts";
import { decrypt, encrypt } from "../utils/local_encryption.ts";
export interface SimplifiedSettings {
+10 -427
View File
@@ -1,434 +1,17 @@
export const FLOW_GUIDANCE = `
---
alwaysApply: true
---
// CLI Flow Guidance - Uses centralized prompts from system_prompts/
import * as prompts from "./prompts.ts";
# System Prompt: OpenFlow Workflow Generator
You are an expert at creating OpenFlow YAML specifications for Windmill workflows.
// CLI-specific introduction
const CLI_INTRO = `You are an expert at creating OpenFlow YAML specifications for Windmill workflows.
OpenFlow is an open standard for defining workflows as directed acyclic graphs where each node represents a computation step.
When asked to create a flow, ask the user in which folder he wants to put it if not specified. Then create a new folder in the specified folder, that ends with \`.flow\`. It should contain a \`.yaml\` file that contains the flow definition.
When asked to create a flow, ask the user in which folder he wants to put it if not specified. Then create a new folder in the specified folder, that ends with \`.flow\`. It should contain a \`.yaml\` file that contains the flow definition.
For rawscript type module in the flow, the content key should start with "!inline" followed by the path of the script containing the code. It should be put in the same folder as the flow.
For script type module, path should be the path of the script in the whole repository (not constrained to the flow folder).
You do not need to create .lock and .yaml files manually. Instead, you should run \`wmill flow generate-locks --yes\` to create them.
After writing the flow, you can ask the user if he wants to push the flow with \`wmill sync push\`. Both should be run at the root of the repository.
You do not need to create .lock and .yaml files manually. Instead, you should run \`wmill flow generate-locks --yes\` to create them.`;
## OpenFlow Structure
// Assemble complete flow guidance
export const FLOW_GUIDANCE = `
${CLI_INTRO}
Every OpenFlow workflow must follow this root structure:
\`\`\`yaml
summary: "Brief one-line description"
description: "Optional detailed description"
value:
modules: [] # Array of workflow steps
# Optional properties:
failure_module: {} # Error handler
preprocessor_module: {} # Runs before first step
same_worker: false # Force same worker execution
concurrent_limit: 0 # Limit concurrent executions
concurrency_key: "string" # Custom concurrency grouping
concurrency_time_window_s: 0
custom_debounce_key: "key"
debounce_delay_s: 0
skip_expr: "javascript_expression" # Skip workflow condition
cache_ttl: 0 # Cache results duration
priority: 0 # Execution priority
early_return: "javascript_expression" # Early termination condition
schema: # JSON Schema for workflow inputs
type: object
properties: {}
required: []
\`\`\`
## Module Types
### 1. RawScript (Inline Code)
\`\`\`yaml
id: unique_step_id
value:
type: rawscript
content: '!inline inline_script_1.inline_script.ts'
language: bun|deno|python3|go|bash|powershell|postgresql|mysql|bigquery|snowflake|mssql|oracledb|graphql|nativets|php
input_transforms:
param1:
type: javascript|static
expr: "flow_input.name" # or for static: value: "fixed_value"
# Optional properties:
path: "optional/path"
lock: "dependency_lock_content"
tag: "version_tag"
concurrent_limit: 0
concurrency_time_window_s: 0
custom_concurrency_key: "key"
custom_debounce_key: "key"
debounce_delay_s: 0
is_trigger: false
assets: []
\`\`\`
### 2. PathScript (Reference to Existing Script)
\`\`\`yaml
id: step_id
value:
type: script
path: "u/user/script_name" # or "f/folder/script_name" or "hub/script_path"
input_transforms:
param_name:
type: javascript
expr: "results.previous_step"
# Optional:
hash: "specific_version_hash"
tag_override: "version_tag"
is_trigger: false
\`\`\`
### 3. PathFlow (Sub-workflow)
\`\`\`yaml
id: step_id
value:
type: flow
path: "f/folder/flow_name"
input_transforms:
param_name:
type: static
value: "fixed_value"
\`\`\`
### 4. ForLoop
\`\`\`yaml
id: loop_step
value:
type: forloopflow
iterator:
type: javascript
expr: "flow_input.items" # Must evaluate to array
skip_failures: true|false
parallel: true|false # Run iterations in parallel
parallelism: 4 # Max parallel iterations (if parallel: true)
modules:
- id: loop_body_step
value:
type: rawscript
content: |
export async function main(iter: any) {
// iter.value contains current item
// iter.index contains current index
return iter.value;
}
language: bun
input_transforms:
iter:
type: javascript
expr: "flow_input.iter"
\`\`\`
### 5. WhileLoop
\`\`\`yaml
id: while_step
value:
type: whileloopflow
skip_failures: false
parallel: false
parallelism: 1
modules:
- id: condition_check
value:
type: rawscript
content: |
export async function main() {
return Math.random() > 0.5; // Continue condition
}
language: bun
input_transforms: {}
\`\`\`
### 6. Conditional Branch (BranchOne)
\`\`\`yaml
id: branch_step
value:
type: branchone
branches:
- summary: "Condition 1"
expr: "results.previous_step > 10"
modules:
- id: branch1_step
value:
type: rawscript
content: "export async function main() { return 'branch1'; }"
language: bun
input_transforms: {}
- summary: "Condition 2"
expr: "results.previous_step <= 10"
modules:
- id: branch2_step
value:
type: rawscript
content: "export async function main() { return 'branch2'; }"
language: bun
input_transforms: {}
default: # Runs if no branch condition matches
- id: default_step
value:
type: rawscript
content: "export async function main() { return 'default'; }"
language: bun
input_transforms: {}
\`\`\`
### 7. Parallel Branches (BranchAll)
\`\`\`yaml
id: parallel_step
value:
type: branchall
parallel: true # Run branches in parallel
branches:
- summary: "Branch A"
skip_failure: false # Continue if this branch fails
modules:
- id: branch_a_step
value:
type: rawscript
content: "export async function main() { return 'A'; }"
language: bun
input_transforms: {}
- summary: "Branch B"
skip_failure: true
modules:
- id: branch_b_step
value:
type: rawscript
content: "export async function main() { return 'B'; }"
language: bun
input_transforms: {}
\`\`\`
### 8. Identity (Pass-through)
\`\`\`yaml
id: identity_step
value:
type: identity
flow: false # Set to true if this represents a sub-flow
\`\`\`
## Input Transforms & Data Flow
### JavaScript Expressions
Reference data using these variables in \`expr\` fields:
- \`flow_input.property_name\` - Access workflow inputs
- \`results.step_id\` - Access outputs from previous steps
- \`results.step_id.property\` - Access specific properties
- \`flow_input.iter.value\` - Current iteration value (in loops)
- \`flow_input.iter.index\` - Current iteration index (in loops)
### Static Values
\`\`\`yaml
input_transforms:
param_name:
type: static
value: "fixed_string" # Can be string, number, boolean, object, array
\`\`\`
### Resource References
\`\`\`yaml
input_transforms:
database:
type: static
value: "$res:f/folder/my_database" # Reference to stored resource
\`\`\`
## Advanced Module Properties
### Error Handling & Control Flow
\`\`\`yaml
id: step_id
value: # ... module definition
# Control flow options:
stop_after_if:
expr: "results.step_id.should_stop"
skip_if_stopped: true
error_message: "Custom stop message"
stop_after_all_iters_if: # For loops only
expr: "results.step_id.should_stop_loop"
skip_if_stopped: false
skip_if:
expr: "results.step_id.should_skip"
sleep:
type: javascript
expr: "flow_input.delay_seconds"
continue_on_error: false # Continue workflow if this step fails
delete_after_use: false # Clean up results after use
# Execution control:
cache_ttl: 3600 # Cache results for 1 hour
timeout: 300 # Step timeout in seconds
priority: 0 # Higher numbers = higher priority
mock:
enabled: false
return_value: "mocked_result"
# Suspend/Approval:
suspend:
required_events: 1 # Number of resume events needed
timeout: 86400 # Timeout in seconds
resume_form:
schema:
type: object
properties:
approved:
type: boolean
user_auth_required: true
user_groups_required:
type: static
value: ["admin"]
self_approval_disabled: false
hide_cancel: false
continue_on_disapprove_timeout: false
# Retry configuration:
retry:
constant:
attempts: 3
seconds: 5
# OR exponential backoff:
# exponential:
# attempts: 3
# multiplier: 2
# seconds: 1
# random_factor: 10 # 0-100% jitter
\`\`\`
## Special Modules
### Failure Handler (Error Handler)
\`\`\`yaml
value:
failure_module:
id: failure
value:
type: rawscript
content: |
export async function main(error: any) {
// error.message, error.step_id, error.name, error.stack
console.log("Flow failed:", error.message);
return error;
}
language: bun
input_transforms: {}
\`\`\`
### Preprocessor
\`\`\`yaml
value:
preprocessor_module:
id: preprocessor
value:
type: rawscript
content: |
export async function main() {
console.log("Flow starting...");
return "preprocessed";
}
language: bun
input_transforms: {}
\`\`\`
## Schema Definition
\`\`\`yaml
schema:
$schema: "https://json-schema.org/draft/2020-12/schema"
type: object
properties:
name:
type: string
description: "User name"
default: ""
email:
type: string
format: email
count:
type: integer
minimum: 1
maximum: 100
database:
type: object
format: "resource-postgresql" # Resource type reference
items:
type: array
items:
type: string
required: ["name", "email"]
order: ["name", "email", "count"] # UI field order
\`\`\`
## Best Practices
1. **Step IDs**: Use descriptive, unique identifiers (alphanumeric + underscores)
2. **Data Flow**: Chain steps using \`results.step_id\` references
3. **Error Handling**: Add failure_module for critical workflows
4. **Languages**: Use \`bun\` for TypeScript (fastest), \`python3\` for Python
5. **Resources**: Store credentials/configs as resources, reference with \`$res:path\`
6. **Loops**: Prefer \`parallel: true\` for independent iterations
7. **Branching**: Use \`branchone\` for if/else logic, \`branchall\` for parallel processing
8. **Schemas**: Always define input schemas for better UX and validation
## Example Complete Workflow
\`\`\`yaml
summary: "Process user data"
description: "Validates user input, processes data, and sends notifications"
value:
modules:
- id: validate_input
value:
type: rawscript
content: '!inline inline_script_0.inline_script.ts'
# script at path inline_script_0.inline_script.ts will contain
# export async function main(email: string, name: string) {
# if (!email.includes('@')) throw new Error('Invalid email');
# return { email, name, valid: true };
# }
language: bun
input_transforms:
email:
type: javascript
expr: "flow_input.email"
name:
type: javascript
expr: "flow_input.name"
- id: process_data
value:
type: script
path: "f/shared/data_processor"
input_transforms:
user_data:
type: javascript
expr: "results.validate_input"
- id: send_notification
value:
type: rawscript
content: '!inline inline_script_1.inline_script.ts'
# script at path inline_script_1.inline_script.ts will contain
# export async function main(processed_data: any) {
# console.log("Sending notification for:", processed_data.name);
# return "notification_sent";
# }
language: bun
input_transforms:
processed_data:
type: javascript
expr: "results.process_data"
schema:
type: object
properties:
email:
type: string
format: email
description: "User email address"
name:
type: string
description: "User full name"
required: ["email", "name"]
\`\`\`
When generating OpenFlow YAML, ensure proper indentation, valid YAML syntax, and logical step dependencies. Always include meaningful summaries and proper input transforms to connect workflow steps.
${prompts.FLOW_PROMPT}
`;
File diff suppressed because one or more lines are too long
+10 -434
View File
@@ -1,439 +1,15 @@
export const SCRIPT_GUIDANCE = `
---
alwaysApply: true
---
// CLI Script Guidance - Uses centralized prompts from system_prompts/
import * as prompts from "./prompts.ts";
# Windmill Script Writing Guide - Universal System Prompt
You are a coding assistant for the Windmill platform. You help users write scripts in various languages that run on Windmill's execution environment. Each script should be placed in a folder. Ask the user in which folder he wants the script to be located at before starting coding.
// CLI-specific introduction
const CLI_INTRO = `Each script should be placed in a folder. Ask the user in which folder he wants the script to be located at before starting coding.
After writing a script, you do not need to create .lock and .yaml files manually. Instead, you can run \`wmill script generate-metadata\` bash command. This command takes no arguments. After writing the script, you can ask the user if he wants to push the script with \`wmill sync push\`. Both should be run at the root of the repository.
## General Principles
You can use \`wmill resource-type list --schema\` to list all resource types available. You should use that to know the type of the resource you need to use in your script. You can use grep if the output is too long.`;
On Windmill, scripts are executed in isolated environments with specific conventions:
// Assemble complete script guidance
export const SCRIPT_GUIDANCE = `
${CLI_INTRO}
- Scripts must export a main function
- Do not call the main function
- Libraries are installed automatically - do not show installation instructions
- Credentials and configuration are stored in resources and passed as parameters
- The windmill client (wmill) provides APIs for interacting with the platform
- You can use \`wmill resource-type list --schema\` to list all resource types available. You should use that to know the type of the resource you need to use in your script. You can use grep if the output is too long.
## Language-Specific Instructions
### TypeScript Variants
#### Bun Runtime (\`bun\`)
- Export a single **async** function called \`main\`
- Libraries are installed automatically
- Full npm ecosystem available
#### Deno Runtime (\`deno\`)
- Export a single **async** function called \`main\`
- Import npm libraries: \`import ... from "npm:{package}";\`
- Import deno libraries normally
- Libraries are installed automatically
#### TypeScript Resource Types & Windmill Client
**Resource Types:**
On Windmill, credentials and configuration are stored in resources and passed as parameters to main.
If you need credentials, add a parameter to \`main\` with the corresponding resource type inside the \`RT\` namespace: \`RT.Stripe\`.
Only use them if needed to satisfy instructions. Always use the RT namespace.
**Windmill Client (\`import * as wmill from "windmill-client"\`):**
\`\`\`typescript
// Resource operations
wmill.getResource(path?: string, undefinedIfEmpty?: boolean): Promise<any>
wmill.setResource(value: any, path?: string, initializeToTypeIfNotExist?: string): Promise<void>
// State management (persistent across executions)
wmill.getState(): Promise<any>
wmill.setState(state: any): Promise<void>
// Variables
wmill.getVariable(path: string): Promise<string>
wmill.setVariable(path: string, value: string, isSecretIfNotExist?: boolean, descriptionIfNotExist?: string): Promise<void>
// Script execution
wmill.runScript(path?: string | null, hash_?: string | null, args?: Record<string, any> | null, verbose?: boolean): Promise<any>
wmill.runScriptAsync(path: string | null, hash_: string | null, args: Record<string, any> | null, scheduledInSeconds?: number | null): Promise<string>
wmill.waitJob(jobId: string, verbose?: boolean): Promise<any>
wmill.getResult(jobId: string): Promise<any>
wmill.getRootJobId(jobId?: string): Promise<string>
// S3 file operations (if S3 is configured)
wmill.loadS3File(s3object: S3Object, s3ResourcePath?: string | undefined): Promise<Uint8Array | undefined>
wmill.writeS3File(s3object: S3Object | undefined, fileContent: string | Blob, s3ResourcePath?: string | undefined): Promise<S3Object>
// Flow operations
wmill.setFlowUserState(key: string, value: any, errorIfNotPossible?: boolean): Promise<void>
wmill.getFlowUserState(key: string, errorIfNotPossible?: boolean): Promise<any>
wmill.getResumeUrls(approver?: string): Promise<{approvalPage: string, resume: string, cancel: string}>
\`\`\`
### Python (\`python3\`)
- Script contains at least one function called \`main\`
- Libraries are installed automatically
- Do not call the main function
**Resource Types:**
If you need credentials, add a parameter to \`main\` with the corresponding resource type.
**Redefine** the type of needed resources before the main function as TypedDict (only include if actually needed).
Resource type name must be **IN LOWERCASE**.
If an import conflicts with a resource type name, **rename the imported object, not the type name**.
Import TypedDict from typing **if using it**.
**Windmill Client (\`import wmill\`):**
\`\`\`python
# Resource operations
wmill.get_resource(path: str, none_if_undefined: bool = False) -> dict | None
wmill.set_resource(path: str, value: Any, resource_type: str = "any") -> None
# State management
wmill.get_state() -> Any
wmill.set_state(value: Any) -> None
wmill.get_flow_user_state(key: str) -> Any
wmill.set_flow_user_state(key: str, value: Any) -> None
# Variables
wmill.get_variable(path: str) -> str
wmill.set_variable(path: str, value: str, is_secret: bool = False) -> None
# Script execution
wmill.run_script(path: str = None, hash_: str = None, args: dict = None, timeout = None, verbose: bool = False) -> Any
wmill.run_script_async(path: str = None, hash_: str = None, args: dict = None, scheduled_in_secs: int = None) -> str
wmill.wait_job(job_id: str, timeout = None, verbose: bool = False) -> Any
wmill.get_result(job_id: str) -> Any
# S3 operations
wmill.load_s3_file(s3object: S3Object | str, s3_resource_path: str | None = None) -> bytes
wmill.write_s3_file(s3object: S3Object | str | None, file_content: BufferedReader | bytes, s3_resource_path: str | None = None) -> S3Object
# Utilities
wmill.get_workspace() -> str
wmill.whoami() -> dict
wmill.set_progress(value: int, job_id: Optional[str] = None) -> None
\`\`\`
### PHP (\`php\`)
- Script must start with \`<?php\`
- Contains at least one function called \`main\`
- **Redefine** resource types before main function (only if needed)
- Check if class exists using \`class_exists\` before defining types
- Resource type name must be exactly as specified
**Resource Types:**
If you need credentials, add a parameter to \`main\` with the corresponding resource type.
**Redefine** the type of needed resources before the main function.
Before defining each type, check if the class already exists using class_exists.
The resource type name has to be exactly as specified.
**Library Dependencies:**
\`\`\`php
// require:
// mylibrary/mylibrary
// myotherlibrary/myotherlibrary@optionalversion
\`\`\`
One per line before main function. Autoload already included.
### Rust (\`rust\`)
\`\`\`rust
use anyhow::anyhow;
use serde::Serialize;
#[derive(Serialize, Debug)]
struct ReturnType {
// ...
}
fn main(...) -> anyhow::Result<ReturnType>
\`\`\`
**Dependencies:**
\`\`\`\`rust
//! \`\`\`cargo
//! [dependencies]
//! anyhow = "1.0.86"
//! \`\`\`
\`\`\`\`
Serde already included. For async functions, keep main sync and create runtime inside.
### Go (\`go\`)
- File package must be "inner"
- Export single function called \`main\`
- Return type: \`({return_type}, error)\`
### Bash (\`bash\`)
- Do not include "#!/bin/bash"
- Arguments: \`var1="$1"\`, \`var2="$2"\`, etc.
### SQL Variants
#### PostgreSQL (\`postgresql\`)
- Arguments: \`$1::{type}\`, \`$2::{type}\`, etc.
- Name parameters: \`-- $1 name1\` or \`-- $2 name = default\`
#### MySQL (\`mysql\`)
- Arguments: \`?\` placeholders
- Name parameters: \`-- ? name1 ({type})\` or \`-- ? name2 ({type}) = default\`
#### BigQuery (\`bigquery\`)
- Arguments: \`@name1\`, \`@name2\`, etc.
- Name parameters: \`-- @name1 ({type})\` or \`-- @name2 ({type}) = default\`
#### Snowflake (\`snowflake\`)
- Arguments: \`?\` placeholders
- Name parameters: \`-- ? name1 ({type})\` or \`-- ? name2 ({type}) = default\`
#### Microsoft SQL Server (\`mssql\`)
- Arguments: \`@P1\`, \`@P2\`, etc.
- Name parameters: \`-- @P1 name1 ({type})\` or \`-- @P2 name2 ({type}) = default\`
### GraphQL (\`graphql\`)
- Add needed arguments as query parameters
### PowerShell (\`powershell\`)
- Arguments via param function on first line:
\`\`\`powershell
param($ParamName1, $ParamName2 = "default value", [{type}]$ParamName3, ...)
\`\`\`
### C# (\`csharp\`)
- Public static Main method inside a class
- NuGet packages: \`#r "nuget: PackageName, Version"\` at top
- Method signature: \`public static ReturnType Main(parameter types...)\`
### Java (\`java\`)
- Main public class with \`public static main()\` method
- Dependencies: \`//requirements://groupId:artifactId:version\` at top
- Method signature: \`public static Object main(parameter types...)\`
## Supported Languages
\`bunnative\`, \`nativets\`, \`bun\`, \`deno\`, \`python3\`, \`php\`, \`rust\`, \`go\`, \`bash\`, \`postgresql\`, \`mysql\`, \`bigquery\`, \`snowflake\`, \`mssql\`, \`graphql\`, \`powershell\`, \`csharp\`, \`java\`
Always follow the specific conventions for the language being used and include only necessary dependencies and resource types.
# Windmill CLI Commands Summary
## Core Commands
### \`wmill init\`
Bootstrap a new Windmill project with a \`wmill.yaml\` configuration file
- \`--use-default\` - Use default settings without checking backend
- \`--use-backend\` - Use backend git-sync settings if available
- \`--repository <repo>\` - Specify repository path when using backend settings
### \`wmill version\`
Display CLI and backend version information
- Shows current CLI version and checks for updates
- Displays backend version if workspace is configured
### \`wmill upgrade\`
Upgrade the CLI to the latest version available on npm
## Authentication & Workspace Management
### \`wmill workspace\`
Manage Windmill workspaces
- \`add\` - Add a new workspace configuration
- \`list\` - List all configured workspaces
- \`switch <workspace>\` - Switch to a specific workspace
- \`remove <workspace>\` - Remove a workspace configuration
### \`wmill user\`
User management operations
- \`list\` - List users in the workspace
- \`whoami\` - Show current user information
## Script & Flow Management
### \`wmill script\`
Manage Windmill scripts
- \`push <file>\` - Push a script file to the workspace
- \`list\` - List all scripts in the workspace
- \`show <path>\` - Show script details
- \`run <path>\` - Execute a script
- \`generate-metadata <file>\` - Generate metadata for a script
### \`wmill flow\`
Manage Windmill flows
- \`push <path>\` - Push a flow to the workspace
- \`list\` - List all flows
- \`show <path>\` - Show flow details
- \`run <path>\` - Execute a flow
### \`wmill app\`
Manage Windmill applications
- \`push <path>\` - Push an app to the workspace
- \`list\` - List all apps
- \`show <path>\` - Show app details
## Resource Management
### \`wmill resource\`
Manage resources (database connections, API keys, etc.)
- \`list\` - List all resources
- \`push <file>\` - Push a resource definition
- \`show <path>\` - Show resource details
### \`wmill resource-type\`
Manage custom resource types
- Operations for defining and managing custom resource schemas
### \`wmill variable\`
Manage workspace variables and secrets
- \`list\` - List all variables
- \`push <file>\` - Push a variable definition
- \`show <path>\` - Show variable details
## Scheduling & Automation
### \`wmill schedule\`
Manage scheduled jobs
- \`list\` - List all schedules
- \`push <file>\` - Push a schedule definition
- Operations for managing cron-based job scheduling
### \`wmill trigger\`
Manage event triggers
- Operations for managing webhooks and event-based triggers
## Synchronization
### \`wmill sync\`
Synchronize local files with Windmill workspace
- \`pull\` - Download resources from workspace to local files
- \`push\` - Upload local files to workspace
- Supports bidirectional sync with conflict resolution
- Works with \`wmill.yaml\` configuration
### \`wmill gitsync-settings\`
Manage git synchronization settings
- Configure automatic git sync for the workspace
- Pull/push git sync configurations
## Development Tools
### \`wmill dev\`
Start development mode with live reloading
- Watches local files for changes
- Automatically syncs changes to workspace
- Provides real-time feedback during development
### \`wmill hub\`
Interact with Windmill Hub
- \`pull\` - Pull resources from the public Windmill Hub
- Access community-shared scripts, flows, and resource types
## Infrastructure Management
### \`wmill instance\`
Manage Windmill instance settings (Enterprise)
- Configure instance-level settings
- Manage global configurations
### \`wmill worker-groups\`
Manage worker groups for job execution
- Configure and manage worker pool settings
### \`wmill workers\`
Manage individual workers
- Monitor and configure worker instances
### \`wmill queues\`
Manage job queues
- Monitor and configure job execution queues
## Utility Commands
### \`wmill folder\`
Manage workspace folders and organization
- Operations for organizing resources into folders
### \`wmill completions\`
Generate shell completion scripts
- Support for bash, zsh, fish, and PowerShell
## Global Options
All commands support these global options:
- \`--workspace <workspace>\` - Specify target workspace
- \`--token <token>\` - Specify API token
- \`--base-url <url>\` - Specify Windmill instance URL
- \`--config-dir <dir>\` - Custom configuration directory
- \`--debug/--verbose\` - Enable debug logging
- \`--show-diffs\` - Show detailed diff information during sync
The CLI uses a \`wmill.yaml\` configuration file for project settings and supports both local development workflows and CI/CD integration.
`;
${prompts.SCRIPT_PROMPT}
`;
+2 -2
View File
@@ -6,7 +6,7 @@ import {
log,
} from "../deps.ts";
import flow from "./commands/flow/flow.ts";
import app from "./commands/app/apps.ts";
import app from "./commands/app/app.ts";
import script from "./commands/script/script.ts";
import workspace, {
getActiveWorkspace,
@@ -22,7 +22,7 @@ import trigger from "./commands/trigger/trigger.ts";
import sync from "./commands/sync/sync.ts";
import gitsyncSettings from "./commands/gitsync-settings/gitsync-settings.ts";
import instance from "./commands/instance/instance.ts";
import workerGroups from "./commands/worker-groups/worker_groups.ts";
import workerGroups from "./commands/worker-groups/worker-groups.ts";
import dev from "./commands/dev/dev.ts";
import { GlobalOptions } from "./types.ts";
+1 -1
View File
@@ -9,7 +9,7 @@ import {
yamlParseContent,
yamlStringify,
} from "../deps.ts";
import { pushApp } from "./commands/app/apps.ts";
import { pushApp } from "./commands/app/app.ts";
import { pushFolder } from "./commands/folder/folder.ts";
import { pushFlow } from "./commands/flow/flow.ts";
import { pushResource } from "./commands/resource/resource.ts";
@@ -26,7 +26,7 @@ import type {
ChatCompletionUserMessageParam
} from 'openai/resources/chat/completions.mjs'
import {
INLINE_CHAT_SYSTEM_PROMPT,
prepareInlineChatSystemPrompt,
prepareScriptSystemMessage,
prepareScriptTools
} from './script/core'
@@ -67,10 +67,6 @@ export enum AIMode {
}
class AIChatManager {
NAVIGATION_SYSTEM_PROMPT = `
CONSIDERATIONS:
- You are provided with a tool to switch to navigation mode, only use it when you are sure that the user is asking you to navigate the application, help them find something or fetch data from the API. Do not use it otherwise.
`
contextManager = new ContextManager()
historyManager = new HistoryManager()
abortController: AbortController | undefined = undefined
@@ -221,10 +217,10 @@ class AIChatManager {
if (mode === AIMode.SCRIPT) {
const customPrompt = getCombinedCustomPrompt(mode)
const currentModel = getCurrentModel()
this.systemMessage = prepareScriptSystemMessage(currentModel, customPrompt)
this.systemMessage.content = this.NAVIGATION_SYSTEM_PROMPT + this.systemMessage.content
const context = this.contextManager.getSelectedContext()
const lang = this.scriptEditorOptions?.lang ?? 'bun'
const context = this.contextManager.getSelectedContext()
this.systemMessage = prepareScriptSystemMessage(currentModel, lang, {}, customPrompt)
this.systemMessage.content = this.systemMessage.content
this.tools = [...prepareScriptTools(currentModel, lang, context)]
this.helpers = {
getScriptOptions: () => {
@@ -248,7 +244,7 @@ class AIChatManager {
} else if (mode === AIMode.FLOW) {
const customPrompt = getCombinedCustomPrompt(mode)
this.systemMessage = prepareFlowSystemMessage(customPrompt)
this.systemMessage.content = this.NAVIGATION_SYSTEM_PROMPT + this.systemMessage.content
this.systemMessage.content = this.systemMessage.content
this.tools = [...flowTools]
this.helpers = this.flowAiChatHelpers
} else if (mode === AIMode.NAVIGATOR) {
@@ -408,7 +404,6 @@ class AIChatManager {
if (this.mode === AIMode.SCRIPT) {
pendingUserMessage = prepareScriptUserMessage(
pendingPrompt,
this.scriptEditorOptions?.lang as ScriptLang | 'bunnative',
this.contextManager.getSelectedContext()
)
} else if (this.mode === AIMode.FLOW) {
@@ -508,15 +503,13 @@ class AIChatManager {
const systemMessage: ChatCompletionSystemMessageParam = {
role: 'system',
content: INLINE_CHAT_SYSTEM_PROMPT
content: prepareInlineChatSystemPrompt(lang)
}
let reply = ''
try {
const userMessage = prepareScriptUserMessage(instructions, lang, selectedContext, {
isPreprocessor: false
})
const userMessage = prepareScriptUserMessage(instructions, selectedContext)
const messages = [userMessage]
const params = {
@@ -632,10 +625,6 @@ class AIChatManager {
throw new Error('No script options passed')
}
const lang = this.scriptEditorOptions?.lang ?? options.lang ?? 'bun'
const isPreprocessor =
this.scriptEditorOptions?.path === 'preprocessor' || options.isPreprocessor
let userMessage: ChatCompletionMessageParam = {
role: 'user',
content: ''
@@ -655,9 +644,7 @@ class AIChatManager {
userMessage = prepareAskUserMessage(oldInstructions)
break
case AIMode.SCRIPT:
userMessage = prepareScriptUserMessage(oldInstructions, lang, oldSelectedContext, {
isPreprocessor
})
userMessage = prepareScriptUserMessage(oldInstructions, oldSelectedContext)
break
case AIMode.API:
userMessage = prepareApiUserMessage(oldInstructions)
@@ -46,7 +46,7 @@
const flow = $state.snapshot(flowStore).val
return {
flow,
selectedId: selectedId
selectedId: selectedId === 'settings-metadata' ? '' : selectedId
}
},
getModules: (id?: string) => {
@@ -34,10 +34,10 @@ import {
} from '../shared'
import type { ContextElement } from '../context'
import type { ExtendedOpenFlow } from '$lib/components/flows/types'
import openFlowSchema from './openFlow.json'
import { inlineScriptStore, extractAndReplaceInlineScripts } from './inlineScriptsUtils'
import { flowModulesSchema } from './openFlowZod'
import { collectAllModuleIdsFromArray } from './utils'
import { getFlowPrompt } from '$system_prompts'
/**
* Helper interface for AI chat flow operations
@@ -596,14 +596,11 @@ export const flowTools: Tool<FlowAIChatHelpers>[] = [
]
export function prepareFlowSystemMessage(customPrompt?: string): ChatCompletionSystemMessageParam {
let content = `You are a helpful assistant that creates and edits workflows on the Windmill platform.
// Get base flow documentation from centralized prompts (includes FLOW_BASE, OPENFLOW_SCHEMA, RESOURCE_TYPES)
const flowBaseContext = getFlowPrompt()
## IMPORTANT RULES
**Reserved IDs - Do NOT use these module IDs:**
- \`failure\` - Reserved for failure handler module
- \`preprocessor\` - Reserved for preprocessor module
- \`Input\` - Reserved for flow input reference
// Chat-specific tool instructions
const chatToolInstructions = `You are a helpful assistant that creates and edits workflows on the Windmill platform.
## Tool Selection Guide
@@ -625,13 +622,6 @@ export function prepareFlowSystemMessage(customPrompt?: string): ChatCompletionS
- **Search resource types** → \`resource_type\`
- **Get database schema** → \`get_db_schema\`
## Common Mistakes to Avoid
- **Don't forget \`input_transforms\`** - Rawscript parameters won't receive values without them
- **Don't use spaces in module IDs** - Use underscores (e.g., \`fetch_data\` not \`fetch data\`)
- **Don't reference future steps** - \`results.step_id\` only works for steps that execute before the current one
- **Don't create duplicate IDs** - Each module ID must be unique in the flow
## Flow Modification with set_flow_json
Use the \`set_flow_json\` tool to set the entire flow structure at once. Provide the complete modules array and optionally the flow input schema.
@@ -815,43 +805,6 @@ To reduce token usage, rawscript content in the flow you receive is replaced wit
**To inspect existing code:**
- Use \`inspect_inline_script\` tool to view the current code: \`inspect_inline_script({ moduleId: "step_a" })\`
### Input Transforms for Rawscripts
Rawscript modules use \`input_transforms\` to map function parameters to values. Each key in \`input_transforms\` corresponds to a parameter name in your script's \`main\` function.
**Transform Types:**
- \`static\`: Fixed value passed directly
- \`javascript\`: Dynamic expression evaluated at runtime
**Available Variables in JavaScript Expressions:**
- \`flow_input.{property}\` - Access flow input parameters
- \`results.{step_id}\` - Access output from a previous step
- \`flow_input.iter.value\` - Current item when inside a for-loop
- \`flow_input.iter.index\` - Current index when inside a for-loop
**Example - Rawscript using flow input and previous step result:**
\`\`\`json
{
"id": "step_b",
"value": {
"type": "rawscript",
"language": "bun",
"content": "export async function main(userId: string, data: any[]) { return 'Hello, world!'; }",
"input_transforms": {
"userId": { "type": "javascript", "expr": "flow_input.user_id" },
"data": { "type": "javascript", "expr": "results.step_a" }
}
}
}
\`\`\`
**Important:** The parameter names in \`input_transforms\` must match the function parameter names in your script.
### Other Key Concepts
- **Resources**: For flow inputs, use type "object" with format "resource-<type>". For step inputs, use "$res:path/to/resource"
- **Module IDs**: Must be unique and valid identifiers. Used to reference results via \`results.step_id\`
- **Module types**: Use 'bun' as default language for rawscript if unspecified
### Writing Code for Modules
**IMPORTANT: Before writing any code for a rawscript module, you MUST call the \`get_instructions_for_code_generation\` tool with the target language.** This tool provides essential language-specific instructions.
@@ -908,27 +861,6 @@ AI agents can use tools to accomplish tasks. When creating an AI agent module:
- **Tool summaries**: Cannot contain spaces - use underscores
- **Tool types**: \`flowmodule\` for scripts/flows, \`mcp\` for MCP server tools
## Resource Types
On Windmill, credentials and configuration are stored in resources. Resource types define the format of the resource.
- Use the \`resource_type\` tool to search for available resource types (e.g. stripe, google, postgresql, etc.)
- If the user needs a resource as flow input, set the property type in the schema to "object" and add a key called "format" set to "resource-nameofresourcetype" (e.g. "resource-stripe")
- If the user wants a specific resource as step input, set the step value to a static string in the format: "$res:path/to/resource"
### OpenFlow Schema Reference
Below is the complete OpenAPI schema for OpenFlow. All field descriptions and behaviors are defined here. Refer to this as the authoritative reference when generating flow JSON:
\`\`\`json
${JSON.stringify(openFlowSchema, null, 2)}
\`\`\`
The schema includes detailed descriptions for:
- **FlowModuleValue types**: rawscript, script, flow, forloopflow, whileloopflow, branchone, branchall, identity, aiagent
- **Module configuration**: stop_after_if, skip_if, suspend, sleep, cache_ttl, retry, mock, timeout
- **InputTransform**: static vs javascript, available variables (results, flow_input, flow_input.iter)
- **Special modules**: preprocessor_module, failure_module
- **Loop options**: iterator, parallel, parallelism, skip_failures
- **Branch types**: BranchOne (first match), BranchAll (all execute)
### Contexts
You have access to the following contexts:
@@ -937,6 +869,8 @@ You have access to the following contexts:
- Focused flow modules: IDs of modules the user is focused on. Your response should focus on these modules
`
let content = chatToolInstructions + '\n\n' + flowBaseContext
// If there's a custom prompt, append it to the system prompt
if (customPrompt?.trim()) {
content = `${content}\n\nUSER GIVEN INSTRUCTIONS:\n${customPrompt.trim()}`
@@ -10,7 +10,6 @@ import type {
} from 'openai/resources/index.mjs'
import { type DBSchema, dbSchemas } from '$lib/stores'
import type { ContextElement } from '../context'
import { PYTHON_PREPROCESSOR_MODULE_CODE, TS_PREPROCESSOR_MODULE_CODE } from '$lib/script_helpers'
import {
createSearchHubScriptsTool,
type Tool,
@@ -23,6 +22,7 @@ import { getModelContextWindow } from '../../lib'
import type { ReviewChangesOpts } from '../monaco-adapter'
import { getCurrentModel } from '$lib/aiStore'
import { getDbSchemas } from '$lib/components/apps/components/display/dbtable/metadata'
import { getScriptPrompt } from '$system_prompts'
// Score threshold for npm packages search filtering
const SCORE_THRESHOLD = 1000
@@ -73,119 +73,6 @@ async function getResourceTypes(prompt: string, workspace: string) {
return resourceTypes
}
const TS_RESOURCE_TYPE_SYSTEM = `On Windmill, credentials and configuration are stored in resources and passed as parameters to main.
If you need credentials, you should add a parameter to \`main\` with the corresponding resource type inside the \`RT\` namespace: for instance \`RT.Stripe\`.
You should only use them if you need them to satisfy the user's instructions. Always use the RT namespace.\n`
const TS_WINDMILL_CLIENT_CONTEXT = `
The windmill client (wmill) can be used to interact with Windmill from the script. Import it with \`import * as wmill from "windmill-client"\`. Key functions include:
// Resource operations
wmill.getResource(path?: string, undefinedIfEmpty?: boolean): Promise<any> // Get resource value by path
wmill.setResource(value: any, path?: string, initializeToTypeIfNotExist?: string): Promise<void> // Set resource value
// State management (persistent across executions)
wmill.getState(): Promise<any> // Get shared state
wmill.setState(state: any): Promise<void> // Set shared state
// Variables
wmill.getVariable(path: string): Promise<string> // Get variable value
wmill.setVariable(path: string, value: string, isSecretIfNotExist?: boolean, descriptionIfNotExist?: string): Promise<void> // Set variable value
// Script execution
wmill.runScript(path?: string | null, hash_?: string | null, args?: Record<string, any> | null, verbose?: boolean): Promise<any> // Run script synchronously
wmill.runScriptAsync(path: string | null, hash_: string | null, args: Record<string, any> | null, scheduledInSeconds?: number | null): Promise<string> // Run script async, returns job ID
wmill.waitJob(jobId: string, verbose?: boolean): Promise<any> // Wait for job completion and get result
wmill.getResult(jobId: string): Promise<any> // Get job result by ID
wmill.getResultMaybe(jobId: string): Promise<any> // Get job result by ID, returns undefined if not found
wmill.getRootJobId(jobId?: string): Promise<string> // Get root job ID from job ID
// S3 file operations (if S3 is configured)
wmill.loadS3File(s3object: S3Object, s3ResourcePath?: string | undefined): Promise<Uint8Array | undefined> // Load file content from S3
wmill.loadS3FileStream(s3object: S3Object, s3ResourcePath?: string | undefined): Promise<Blob | undefined> // Load file content from S3 as stream
wmill.writeS3File(s3object: S3Object | undefined, fileContent: string | Blob, s3ResourcePath?: string | undefined): Promise<S3Object> // Write file to S3
// Flow operations
wmill.setFlowUserState(key: string, value: any, errorIfNotPossible?: boolean): Promise<void> // Set flow user state
wmill.getFlowUserState(key: string, errorIfNotPossible?: boolean): Promise<any> // Get flow user state
wmill.getResumeUrls(approver?: string): Promise<{approvalPage: string, resume: string, cancel: string}> // Get approval URLs
`
const PYTHON_WINDMILL_CLIENT_CONTEXT = `
The windmill client (wmill) can be used to interact with Windmill from the script. Import it with \`import wmill\`. Key functions include:
// Resource operations
wmill.get_resource(path: str, none_if_undefined: bool = False) -> dict | None // Get resource value by path
wmill.set_resource(path: str, value: Any, resource_type: str = "any") -> None // Set resource value
// State management (persistent across executions)
wmill.get_state() -> Any // Get shared state (deprecated, use flow user state)
wmill.set_state(value: Any) -> None // Set shared state
wmill.get_state_path() -> str // Get state path
wmill.get_flow_user_state(key: str) -> Any // Get flow user state
wmill.set_flow_user_state(key: str, value: Any) -> None // Set flow user state
// Variables
wmill.get_variable(path: str) -> str // Get variable value
wmill.set_variable(path: str, value: str, is_secret: bool = False) -> None // Set variable value
// Script execution
wmill.run_script(path: str = None, hash_: str = None, args: dict = None, timeout = None, verbose: bool = False, cleanup: bool = True, assert_result_is_not_none: bool = True) -> Any // Run script synchronously
wmill.run_script_async(path: str = None, hash_: str = None, args: dict = None, scheduled_in_secs: int = None) -> str // Run script async, returns job ID
wmill.wait_job(job_id: str, timeout = None, verbose: bool = False, cleanup: bool = True, assert_result_is_not_none: bool = False) -> Any // Wait for job completion and get result
wmill.get_result(job_id: str, assert_result_is_not_none: bool = True) -> Any // Get job result by ID
wmill.get_root_job_id(job_id: str | None = None) -> str // Get root job ID from job ID
// S3 file operations (if S3 is configured)
wmill.load_s3_file(s3object: S3Object | str, s3_resource_path: str | None = None) -> bytes // Load file content from S3
wmill.load_s3_file_reader(s3object: S3Object | str, s3_resource_path: str | None = None) -> BufferedReader // Load S3 file as stream reader
wmill.write_s3_file(s3object: S3Object | str | None, file_content: BufferedReader | bytes, s3_resource_path: str | None = None, content_type: str | None = None, content_disposition: str | None = None) -> S3Object // Write file to S3
// Flow operations
wmill.run_flow_async(path: str, args: dict = None, scheduled_in_secs: int = None, do_not_track_in_parent: bool = True) -> str // Run flow asynchronously
wmill.get_resume_urls(approver: str = None) -> dict // Get approval URLs for flow steps
// Utilities
wmill.get_workspace() -> str // Get current workspace
wmill.whoami() -> dict // Get current user information
wmill.get_job_status(job_id: str) -> str // Get job status ("RUNNING" | "WAITING" | "COMPLETED")
wmill.set_progress(value: int, job_id: Optional[str] = None) -> None // Set job progress (0-100)
wmill.get_progress(job_id: Optional[str] = None) -> Any // Get job progress`
const PYTHON_RESOURCE_TYPE_SYSTEM = `On Windmill, credentials and configuration are stored in resources and passed as parameters to main.
If you need credentials, you should add a parameter to \`main\` with the corresponding resource type.
You need to **redefine** the type of the resources that are needed before the main function as TypedDict, but only include them if they are actually needed to achieve the function purpose.
The resource type name has to be exactly as specified (has to be IN LOWERCASE).
If an import conflicts with a resource type name, **you have to rename the imported object, not the type name**.
Make sure to import TypedDict from typing **if you're using it**`
const PHP_RESOURCE_TYPE_SYSTEM = `On Windmill, credentials and configuration are stored in resources and passed as parameters to main.
If you need credentials, you should add a parameter to \`main\` with the corresponding resource type
You need to **redefine** the type of the resources that are needed before the main function, but only include them if they are actually needed to achieve the function purpose.
Before defining each type, check if the class already exists using class_exists.
The resource type name has to be exactly as specified.`
const PREPROCESSOR_INSTRUCTION_BASE = `The current script is a preprocessor. It processes raw trigger data from various sources (webhook, custom HTTP route, SQS, WebSocket, Kafka, NATS, MQTT, Postgres, or email) before passing it to the flow. This separates the trigger logic from the flow logic and keeps the auto-generated UI clean.
The returned object determines the parameter values passed to the flow.
e.g., \`{ b: 1, a: 2 }\` → Calls the flow with \`a = 2\` and \`b = 1\`, assuming the flow has two inputs called \`a\` and \`b\`.
The preprocessor receives a single parameter called event.
Here's a sample script which includes the event object definition:\n`
const TS_PREPROCESSOR_INSTRUCTION =
PREPROCESSOR_INSTRUCTION_BASE +
`\`\`\`typescript
${TS_PREPROCESSOR_MODULE_CODE}
\`\`\`\n`
const PYTHON_PREPROCESSOR_INSTRUCTION =
PREPROCESSOR_INSTRUCTION_BASE +
`\`\`\`python
${PYTHON_PREPROCESSOR_MODULE_CODE}
\`\`\``
export const SUPPORTED_CHAT_SCRIPT_LANGUAGES = [
'bunnative',
'nativets',
@@ -214,108 +101,24 @@ export function getLangContext(
allowResourcesFetch = false,
isPreprocessor = false
}: { allowResourcesFetch?: boolean; isPreprocessor?: boolean; isFailure?: boolean } = {}
) {
const tsContext =
(isPreprocessor
? TS_PREPROCESSOR_INSTRUCTION
: TS_RESOURCE_TYPE_SYSTEM +
(allowResourcesFetch
? `To query the RT namespace, you can use the \`search_resource_types\` tool.\n`
: '')) + TS_WINDMILL_CLIENT_CONTEXT
): string {
// Get base language context from centralized prompts
let context = getScriptPrompt(lang)
const mainFunctionName = isPreprocessor ? 'preprocessor' : 'main'
switch (lang) {
case 'bunnative':
case 'nativets':
return (
`The user is coding in TypeScript. On Windmill, it is expected that the script exports a single **async** function called \`${mainFunctionName}\`. You should use fetch (available globally, no need to import) and are not allowed to import any libraries.\n` +
tsContext
)
case 'bun':
return (
`The user is coding in TypeScript (bun runtime). On Windmill, it is expected that the script exports a single **async** function called \`${mainFunctionName}\`. Do not call the ${mainFunctionName} function. Libraries are installed automatically, do not show how to install them.\n` +
tsContext
)
case 'deno':
return (
`The user is coding in TypeScript (deno runtime). On Windmill, it is expected that the script exports a single **async** function called \`${mainFunctionName}\`. Do not call the ${mainFunctionName} function. Libraries are installed automatically, do not show how to install them.\n` +
tsContext +
'\nYou can import deno libraries or you can import npm libraries like that: `import ... from "npm:{package}";`.'
)
case 'python3':
return (
`The user is coding in Python. On Windmill, it is expected the script contains at least one function called \`${mainFunctionName}\`. Do not call the ${mainFunctionName} function. Libraries are installed automatically, do not show how to install them.` +
(isPreprocessor
? PYTHON_PREPROCESSOR_INSTRUCTION
: PYTHON_RESOURCE_TYPE_SYSTEM +
`${allowResourcesFetch ? `\nTo query the available resource types, you can use the \`search_resource_types\` tool.` : ''}`) +
PYTHON_WINDMILL_CLIENT_CONTEXT
)
case 'php':
return (
'The user is coding in PHP. On Windmill, it is expected the script contains at least one function called `main`. The script must start with <?php.' +
PHP_RESOURCE_TYPE_SYSTEM +
`${allowResourcesFetch ? `\nTo query the available resource types, you can use the \`search_resource_types\` tool.` : ''}` +
`\nIf you need to import libraries, you need to specify them as comments in the following manner before the main function:
\`\`\`
// require:
// mylibrary/mylibrary
// myotherlibrary/myotherlibrary@optionalversion
\`\`\`
Make sure to have one per line.
No need to require autoload, it is already done.`
)
case 'rust':
return `The user is coding in Rust. On Windmill, it is expected the script contains at least one function called \`main\` (without calling it) defined like this:
\`\`\`rust
use anyhow::anyhow;
use serde::Serialize;
#[derive(Serialize, Debug)]
struct ReturnType {
// ...
}
fn main(...) -> anyhow::Result<ReturnType>
\`\`\`
Arguments should be owned. Make sure the return type is serializable.
Packages must be made available with a partial cargo.toml by adding the following comment at the beginning of the script:
//! \`\`\`cargo
//! [dependencies]
//! anyhow = "1.0.86"
//! \`\`\'
Serde is already included, no need to add it again.
If you want to handle async functions (e.g., using tokio), you need to keep the main function sync and create the runtime inside.`
case 'go':
return `The user is coding in Go. On Windmill, it is expected the script exports a single function called \`main\`. Its return type has to be (\`{return_type}\`, error). The file package has to be "inner".`
case 'bash':
return `The user is coding in Bash. Do not include "#!/bin/bash". On Windmill, arguments are always string and can only be obtained with "var1="$1"", "var2="$2"", etc..`
case 'postgresql':
return `The user is coding in PostgreSQL. On Windmill, arguments can be obtained directly in the statement with \`$1::{type}\`, \`$2::{type}\`, etc... Name the parameters (without specifying the type) by adding comments at the beginning of the script before the statement like that: \`-- $1 name1\` or \`-- $2 name = default\` (one per row)`
case 'mysql':
return 'The user is coding in MySQL. On Windmill, arguments can be obtained directly in the statement with ?. Name the parameters by adding comments before the statement like that: `-- ? name1 ({type})` or `-- ? name2 ({type}) = default` (one per row)'
case 'bigquery':
return 'The user is coding in BigQuery. On Windmill, arguments can be obtained by adding comments before the statement like that: `-- @name1 ({type})` or `-- @name2 ({type}) = default` (one per row). They can then be obtained directly in the statement with `@name1`, `@name2`, etc....'
case 'snowflake':
return 'The user is coding in Snowflake. On Windmill, arguments can be obtained directly in the statement with ?. Name the parameters by adding comments before the statement like that: `-- ? name1 ({type})` or `-- ? name2 ({type}) = default` (one per row)'
case 'mssql':
return 'The user is coding in Microsoft SQL Server. On Windmill, arguments can be obtained directly in the statement with @P1, @P2, etc.. Name the parameters by adding comments before the statement like that: `-- @P1 name1 ({type})` or `-- @P2 name2 ({type}) = default` (one per row)'
case 'graphql':
return 'The user is coding in GraphQL. If needed, add the needed arguments as query parameters.'
case 'powershell':
return 'The user is coding in PowerShell. On Windmill, arguments can be obtained by calling the param function on the first line of the script like that: `param($ParamName1, $ParamName2 = "default value", [{type}]$ParamName3, ...)`'
case 'csharp':
return 'The user is coding in C#. On Windmill, it is expected the script contains a public static Main method inside a class. The class name is irrelevant. NuGet packages can be added using the format: #r "nuget: PackageName, Version" at the top of the script. The Main method signature should be: public static ReturnType Main(parameter types...)'
case 'java':
return 'The user is coding in Java. On Windmill, it is expected the script contains a Main public class and a public static main() method. The return type can be Object or void. Dependencies can be added using the format: //requirements://groupId:artifactId:version at the top of the script. The method signature should be: public static Object main(parameter types...)'
case 'duckdb':
return "The user is coding in DuckDB. On Windmill, arguments are defined with comments like `-- $name (text) = default` or `-- $name (text)` (one per line) and used in the statement with $age, $name, etc. To use Ducklake, attach it with `ATTACH 'ducklake' AS dl;` (for main ducklake) or `ATTACH 'ducklake://name' AS dl;` for named ducklakes, then perform CRUD operations. To connect to external databases, use `ATTACH '$res:path/to/resource' AS db (TYPE postgres);` and query with `SELECT * FROM db.schema.table;`. To read S3 files, use `SELECT * FROM read_csv('s3:///path/to/file.csv');` for default storage or `SELECT * FROM read_csv('s3://secondary_storage_name/path/to/file.csv');` for named storage"
default:
return ''
// Add tool usage instructions for applicable languages
if (['python3', 'php', 'bun', 'deno', 'nativets', 'bunnative'].includes(lang)) {
if (allowResourcesFetch) {
context += '\n\nTo query available resource types, use the `search_resource_types` tool.'
}
}
// Note preprocessor function naming if applicable
if (isPreprocessor) {
context +=
'\n\nThe main function for this script should be named `preprocessor` instead of `main`.'
}
return context
}
export async function getFormattedResourceTypes(
@@ -467,22 +270,29 @@ export async function main() {
\`\`\`
`
export function prepareInlineChatSystemPrompt(lang: ScriptLang | 'bunnative') {
return INLINE_CHAT_SYSTEM_PROMPT + getLangContext(lang, { allowResourcesFetch: true })
}
export const CHAT_USER_PROMPT = `
INSTRUCTIONS:
{instructions}
WINDMILL LANGUAGE CONTEXT:
{lang_context}
`
export function prepareScriptSystemMessage(
currentModel: AIProviderModel,
language: ScriptLang | 'bunnative',
options: { isPreprocessor?: boolean; allowResourcesFetch?: boolean } = {},
customPrompt?: string
): ChatCompletionSystemMessageParam {
let content = buildChatSystemPrompt(currentModel)
// If there's a custom prompt, prepend it to the system prompt
// Add language context to the system prompt
const langContext = getLangContext(language, { allowResourcesFetch: true, ...options })
content += `\n\nWINDMILL LANGUAGE CONTEXT:\n${langContext}`
// If there's a custom prompt, append it to the system prompt
if (customPrompt?.trim()) {
content = `${content}\n\nUSER GIVEN INSTRUCTIONS:\n${customPrompt.trim()}`
}
@@ -521,18 +331,12 @@ export function prepareScriptTools(
export function prepareScriptUserMessage(
instructions: string,
language: ScriptLang | 'bunnative',
selectedContext: ContextElement[],
options: {
isPreprocessor?: boolean
} = {}
selectedContext: ContextElement[]
): ChatCompletionUserMessageParam {
let userMessage = CHAT_USER_PROMPT.replace('{instructions}', instructions).replace(
'{lang_context}',
getLangContext(language, { allowResourcesFetch: true, ...options })
)
let userMessage = CHAT_USER_PROMPT.replace('{instructions}', instructions)
const contextInstructions = buildContextString(selectedContext)
userMessage += contextInstructions
return {
role: 'user',
content: userMessage
+3
View File
@@ -26,6 +26,9 @@ const config = {
prerender: { entries: [] },
paths: {
base: process.env.VITE_BASE_URL ?? ''
},
alias: {
'$system_prompts': '../system_prompts/auto-generated'
}
}
}
+426 -8
View File
@@ -32,7 +32,17 @@ JobStatus = Literal["RUNNING", "WAITING", "COMPLETED"]
class Windmill:
"""Windmill client for interacting with the Windmill API."""
def __init__(self, base_url=None, token=None, workspace=None, verify=True):
"""Initialize the Windmill client.
Args:
base_url: API base URL (defaults to BASE_INTERNAL_URL or WM_BASE_URL env)
token: Authentication token (defaults to WM_TOKEN env)
workspace: Workspace ID (defaults to WM_WORKSPACE env)
verify: Whether to verify SSL certificates
"""
base = (
base_url
or os.environ.get("BASE_INTERNAL_URL")
@@ -75,6 +85,11 @@ class Windmill:
return mocked_api
def get_client(self) -> httpx.Client:
"""Get the HTTP client instance.
Returns:
Configured httpx.Client for API requests
"""
return httpx.Client(
base_url=self.base_url,
headers=self.headers,
@@ -82,6 +97,16 @@ class Windmill:
)
def get(self, endpoint, raise_for_status=True, **kwargs) -> httpx.Response:
"""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
"""
endpoint = endpoint.lstrip("/")
resp = self.client.get(f"/{endpoint}", **kwargs)
if raise_for_status:
@@ -94,6 +119,16 @@ class Windmill:
return resp
def post(self, 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
"""
endpoint = endpoint.lstrip("/")
resp = self.client.post(f"/{endpoint}", **kwargs)
if raise_for_status:
@@ -106,6 +141,14 @@ class Windmill:
return resp
def create_token(self, duration=dt.timedelta(days=1)) -> str:
"""Create a new authentication token.
Args:
duration: Token validity duration (default: 1 day)
Returns:
New authentication token string
"""
endpoint = "/users/tokens/create"
payload = {
"label": f"refresh {time.time()}",
@@ -299,6 +342,22 @@ class Windmill:
cleanup: bool = True,
assert_result_is_not_none: bool = False,
):
"""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 cancel_job():
logger.warning(f"cancelling job: {job_id}")
self.post(
@@ -407,19 +466,52 @@ class Windmill:
return result
def get_job(self, job_id: str) -> dict:
"""Get job details by ID.
Args:
job_id: UUID of the job
Returns:
Job details dictionary
"""
return self.get(f"/w/{self.workspace}/jobs_u/get/{job_id}").json()
def get_root_job_id(self, job_id: str | None = None) -> 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
"""
job_id = job_id or os.environ.get("WM_JOB_ID")
return self.get(f"/w/{self.workspace}/jobs_u/get_root_job_id/{job_id}").json()
def get_id_token(self, audience: str, expires_in: int | None = None) -> str:
"""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
"""
params = {}
if expires_in is not None:
params["expires_in"] = expires_in
return self.post(f"/w/{self.workspace}/oidc/token/{audience}", params=params).text
def get_job_status(self, job_id: str) -> JobStatus:
"""Get the status of a job.
Args:
job_id: UUID of the job
Returns:
Job status: "RUNNING", "WAITING", or "COMPLETED"
"""
job = self.get_job(job_id)
job_type = job.get("type", "")
assert job_type, f"{job} is not a valid job"
@@ -434,6 +526,15 @@ class Windmill:
job_id: str,
assert_result_is_not_none: bool = True,
) -> Any:
"""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
"""
result = self.get(f"/w/{self.workspace}/jobs_u/completed/get_result/{job_id}")
result_text = result.text
if assert_result_is_not_none and result_text is None:
@@ -444,6 +545,14 @@ class Windmill:
return result_text
def get_variable(self, path: str) -> str:
"""Get a variable value by path.
Args:
path: Variable path in Windmill
Returns:
Variable value as string
"""
path = parse_variable_syntax(path) or path
if self.mocked_api is not None:
variables = self.mocked_api["variables"]
@@ -454,17 +563,20 @@ class Windmill:
logger.info(
f"MockedAPI present, but variable not found at {path}, falling back to real API"
)
"""Get variable from Windmill"""
return self.get(f"/w/{self.workspace}/variables/get_value/{path}").json()
def set_variable(self, path: str, value: str, is_secret: bool = False) -> None:
"""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)
"""
path = parse_variable_syntax(path) or path
if self.mocked_api is not None:
self.mocked_api["variables"][path] = value
return
"""Set variable from Windmill"""
# check if variable exists
r = self.get(
f"/w/{self.workspace}/variables/get/{path}", raise_for_status=False
@@ -492,6 +604,15 @@ class Windmill:
path: str,
none_if_undefined: bool = False,
) -> dict | None:
"""Get a resource value by path.
Args:
path: Resource path in Windmill
none_if_undefined: Return None instead of raising if not found
Returns:
Resource value dictionary or None
"""
path = parse_resource_syntax(path) or path
if self.mocked_api is not None:
resources = self.mocked_api["resources"]
@@ -508,8 +629,6 @@ class Windmill:
logger.info(
f"MockedAPI present, but resource not found at ${path}, falling back to real API"
)
"""Get resource from Windmill"""
try:
return self.get(
f"/w/{self.workspace}/resources/get_value_interpolated/{path}"
@@ -526,6 +645,13 @@ class Windmill:
path: str,
resource_type: str,
):
"""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
"""
path = parse_resource_syntax(path) or path
if self.mocked_api is not None:
self.mocked_api["resources"][path] = value
@@ -582,9 +708,20 @@ class Windmill:
).json()
def set_state(self, value: Any):
"""Set the workflow state.
Args:
value: State value to set
"""
self.set_resource(value, path=self.state_path, resource_type="state")
def set_progress(self, value: int, job_id: Optional[str] = None):
"""Set job progress percentage (0-99).
Args:
value: Progress percentage
job_id: Job ID (defaults to current WM_JOB_ID)
"""
workspace = get_workspace()
flow_id = os.environ.get("WM_FLOW_JOB_ID")
job_id = job_id or os.environ.get("WM_JOB_ID")
@@ -602,6 +739,14 @@ class Windmill:
)
def get_progress(self, job_id: Optional[str] = None) -> Any:
"""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
"""
workspace = get_workspace()
job_id = job_id or os.environ.get("WM_JOB_ID")
@@ -640,6 +785,11 @@ class Windmill:
@property
def version(self):
"""Get the Windmill server version.
Returns:
Version string
"""
return self.get("version").text
def get_duckdb_connection_settings(
@@ -816,11 +966,27 @@ class Windmill:
return S3Object(s3=response["file_key"])
def sign_s3_objects(self, s3_objects: list[S3Object | str]) -> list[S3Object]:
"""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
"""
return self.post(
f"/w/{self.workspace}/apps/sign_s3_objects", json={"s3_objects": list(map(parse_s3_object, s3_objects))}
).json()
def sign_s3_object(self, s3_object: S3Object | str) -> 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
"""
return self.post(
f"/w/{self.workspace}/apps/sign_s3_objects",
json={"s3_objects": [s3_object]},
@@ -917,14 +1083,29 @@ class Windmill:
)
def whoami(self) -> dict:
"""Get the current user information.
Returns:
User details dictionary
"""
return self.get("/users/whoami").json()
@property
def user(self) -> dict:
"""Get the current user information (alias for whoami).
Returns:
User details dictionary
"""
return self.whoami()
@property
def state_path(self) -> str:
"""Get the state resource path from environment.
Returns:
State path string
"""
state_path = os.environ.get(
"WM_STATE_PATH_NEW", os.environ.get("WM_STATE_PATH")
)
@@ -934,10 +1115,16 @@ class Windmill:
@property
def state(self) -> Any:
"""Get the workflow state.
Returns:
State value or None if not set
"""
return self.get_resource(path=self.state_path, none_if_undefined=True)
@state.setter
def state(self, value: Any) -> None:
"""Set the workflow state."""
self.set_state(value)
@staticmethod
@@ -981,6 +1168,14 @@ class Windmill:
return json.load(f)
def get_resume_urls(self, approver: str = None) -> dict:
"""Get URLs needed for resuming a flow after suspension.
Args:
approver: Optional approver name
Returns:
Dictionary with approvalPage, resume, and cancel URLs
"""
nonce = random.randint(0, 1000000000)
job_id = os.environ.get("WM_JOB_ID") or "NO_ID"
return self.get(
@@ -1094,9 +1289,25 @@ class Windmill:
)
def datatable(self, name: str = "main"):
"""Get a DataTable client for SQL queries.
Args:
name: Database name (default: "main")
Returns:
DataTableClient instance
"""
return DataTableClient(self, name)
def ducklake(self, name: str = "main"):
"""Get a DuckLake client for DuckDB queries.
Args:
name: Database name (default: "main")
Returns:
DucklakeClient instance
"""
return DucklakeClient(self, name)
@@ -1132,11 +1343,24 @@ def deprecate(in_favor_of: str):
@init_global_client
def get_workspace() -> str:
"""Get the current workspace ID.
Returns:
Workspace ID string
"""
return _client.workspace
@init_global_client
def get_root_job_id(job_id: str | None = None) -> str:
"""Get the root job ID for a flow hierarchy.
Args:
job_id: Job ID (defaults to current WM_JOB_ID)
Returns:
Root job ID
"""
return _client.get_root_job_id(job_id)
@@ -1152,6 +1376,16 @@ def run_script_async(
args: Dict[str, Any] = None,
scheduled_in_secs: int = None,
) -> str:
"""Create a script job and return its job ID.
Args:
hash_or_path: Script hash or path (determined by presence of '/')
args: Script arguments
scheduled_in_secs: Delay before execution in seconds
Returns:
Job ID string
"""
is_path = "/" in hash_or_path
hash_ = None if is_path else hash_or_path
path = hash_or_path if is_path else None
@@ -1173,6 +1407,17 @@ def run_flow_async(
# lead to incorrectness and failures
do_not_track_in_parent: bool = True,
) -> str:
"""Create a flow job and return its job ID.
Args:
path: Flow path
args: Flow arguments
scheduled_in_secs: Delay before execution in seconds
do_not_track_in_parent: Whether to track in parent job (default: True)
Returns:
Job ID string
"""
return _client.run_flow_async(
path=path,
args=args,
@@ -1190,6 +1435,19 @@ def run_script_sync(
cleanup: bool = True,
timeout: dt.timedelta = None,
) -> Any:
"""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
Returns:
Script result
"""
return _client.run_script(
hash_=hash,
args=args,
@@ -1206,6 +1464,16 @@ def run_script_by_path_async(
args: Dict[str, Any] = None,
scheduled_in_secs: Union[None, int] = None,
) -> str:
"""Create a script job by path and return its job ID.
Args:
path: Script path
args: Script arguments
scheduled_in_secs: Delay before execution in seconds
Returns:
Job ID string
"""
return _client.run_script_by_path_async(
path=path,
args=args,
@@ -1219,6 +1487,16 @@ def run_script_by_hash_async(
args: Dict[str, Any] = None,
scheduled_in_secs: Union[None, int] = None,
) -> str:
"""Create a script job by hash and return its job ID.
Args:
hash_: Script hash
args: Script arguments
scheduled_in_secs: Delay before execution in seconds
Returns:
Job ID string
"""
return _client.run_script_by_hash_async(
hash_=hash_,
args=args,
@@ -1235,6 +1513,19 @@ def run_script_by_path_sync(
cleanup: bool = True,
timeout: dt.timedelta = 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
Returns:
Script result
"""
return _client.run_script(
path=path,
args=args,
@@ -1255,11 +1546,28 @@ def get_id_token(audience: str) -> str:
@init_global_client
def get_job_status(job_id: str) -> JobStatus:
"""Get the status of a job.
Args:
job_id: UUID of the job
Returns:
Job status: "RUNNING", "WAITING", or "COMPLETED"
"""
return _client.get_job_status(job_id)
@init_global_client
def get_result(job_id: str, assert_result_is_not_none=True) -> Dict[str, Any]:
"""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
"""
return _client.get_result(
job_id=job_id, assert_result_is_not_none=assert_result_is_not_none
)
@@ -1559,11 +1867,24 @@ def set_flow_user_state(key: str, value: Any) -> None:
@init_global_client
def get_state_path() -> str:
"""Get the state resource path from environment.
Returns:
State path string
"""
return _client.state_path
@init_global_client
def get_resume_urls(approver: str = None) -> dict:
"""Get URLs needed for resuming a flow after suspension.
Args:
approver: Optional approver name
Returns:
Dictionary with approvalPage, resume, and cancel URLs
"""
return _client.get_resume_urls(approver)
@@ -1590,6 +1911,17 @@ def request_interactive_slack_approval(
def send_teams_message(
conversation_id: str, text: str, success: bool, card_block: dict = None
):
"""Send a message to a Microsoft Teams conversation.
Args:
conversation_id: Teams conversation ID
text: Message text
success: Whether to style as success message
card_block: Optional adaptive card block
Returns:
HTTP response from Teams
"""
return _client.send_teams_message(conversation_id, text, success, card_block)
@@ -1702,13 +2034,40 @@ def username_to_email(username: str) -> str:
@init_global_client
def datatable(name: str = "main") -> DataTableClient:
"""Get a DataTable client for SQL queries.
Args:
name: Database name (default: "main")
Returns:
DataTableClient instance
"""
return _client.datatable(name)
@init_global_client
def ducklake(name: str = "main") -> DucklakeClient:
"""Get a DuckLake client for DuckDB queries.
Args:
name: Database name (default: "main")
Returns:
DucklakeClient instance
"""
return _client.ducklake(name)
def task(*args, **kwargs):
"""Decorator to mark a function as a workflow task.
When executed inside a Windmill job, the decorated function runs as a
separate workflow step. Outside Windmill, it executes normally.
Args:
tag: Optional worker tag for execution
Returns:
Decorated function
"""
from inspect import signature
def f(func, tag: str | None = None):
@@ -1807,10 +2166,28 @@ def stream_result(stream) -> None:
append_to_result_stream(text)
class DataTableClient:
"""Client for executing SQL queries against Windmill DataTables."""
def __init__(self, client: Windmill, name: str):
"""Initialize DataTableClient.
Args:
client: Windmill client instance
name: DataTable name
"""
self.client = client
self.name = name
def query(self, sql: str, *args):
"""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
"""
args_dict = {}
args_def = ""
for i, arg in enumerate(args):
@@ -1818,7 +2195,7 @@ class DataTableClient:
args_def += f"-- ${i+1} arg{i+1}\n"
sql = args_def + sql
return SqlQuery(
sql,
sql,
lambda sql: self.client.run_inline_script_preview(
content=sql,
language="postgresql",
@@ -1827,10 +2204,28 @@ class DataTableClient:
)
class DucklakeClient:
"""Client for executing DuckDB queries against Windmill DuckLake."""
def __init__(self, client: Windmill, name: str):
"""Initialize DucklakeClient.
Args:
client: Windmill client instance
name: DuckLake database name
"""
self.client = client
self.name = name
def query(self, sql: str, **kwargs):
"""Execute a DuckDB query against the DuckLake database.
Args:
sql: SQL query string with $name placeholders
**kwargs: Named arguments to bind to query placeholders
Returns:
SqlQuery instance for fetching results
"""
args_dict = {}
args_def = ""
for key, value in kwargs.items():
@@ -1839,7 +2234,7 @@ class DucklakeClient:
attach = f"ATTACH 'ducklake://{self.name}' AS dl;USE dl;\n"
sql = args_def + attach + sql
return SqlQuery(
sql,
sql,
lambda sql: self.client.run_inline_script_preview(
content=sql,
language="duckdb",
@@ -1848,15 +2243,38 @@ class DucklakeClient:
)
class SqlQuery:
"""Query result handler for DataTable and DuckLake queries."""
def __init__(self, sql: str, fetch_fn):
"""Initialize SqlQuery.
Args:
sql: SQL query string
fetch_fn: Function to execute the query
"""
self.sql = sql
self.fetch_fn = fetch_fn
def fetch(self, result_collection: str | None = None):
"""Execute query and fetch results.
Args:
result_collection: Optional result collection mode
Returns:
Query results
"""
sql = self.sql
if result_collection is not None:
sql = f'-- result_collection={result_collection}\n{sql}'
return self.fetch_fn(sql)
def fetch_one(self):
"""Execute query and fetch first row of results.
Returns:
First row of query results
"""
return self.fetch(result_collection="last_statement_first_row")
def infer_sql_type(value) -> str:
+17
View File
@@ -5,6 +5,15 @@ import httpx
class S3BufferedReader(BufferedReader):
"""Streaming buffered reader for S3 files via Windmill's S3 proxy.
Args:
workspace: Windmill workspace ID
windmill_client: HTTP client for Windmill API
file_key: S3 file key/path
s3_resource_path: Optional path to S3 resource configuration
storage: Optional storage backend identifier
"""
def __init__(self, workspace: str, windmill_client: httpx.Client, file_key: str, s3_resource_path: Optional[str], storage: Optional[str]):
params = {
"file_key": file_key,
@@ -62,6 +71,14 @@ class S3BufferedReader(BufferedReader):
def bytes_generator(buffered_reader: Union[BufferedReader, BytesIO]):
"""Yield 50KB chunks from a buffered reader.
Args:
buffered_reader: File-like object to read from
Yields:
Bytes chunks of up to 50KB
"""
while True:
byte = buffered_reader.read(50 * 1024)
if not byte:
+7
View File
@@ -2,6 +2,7 @@ from typing import Optional
class S3Object(dict):
"""S3 file reference with file key, optional storage identifier, and presigned token."""
s3: str
storage: Optional[str]
presigned: Optional[str]
@@ -11,6 +12,7 @@ class S3Object(dict):
class S3FsClientKwargs(dict):
"""S3FS client keyword arguments for region configuration."""
region_name: str
def __getattr__(self, attr):
@@ -18,6 +20,7 @@ class S3FsClientKwargs(dict):
class S3FsArgs(dict):
"""S3FS connection arguments including endpoint, credentials, and client settings."""
endpoint_url: str
key: str
secret: str
@@ -30,6 +33,7 @@ class S3FsArgs(dict):
class StorageOptions(dict):
"""Storage options for Polars S3 connectivity with AWS credentials and endpoint."""
aws_endpoint_url: str
aws_access_key_id: str
aws_secret_access_key: str
@@ -41,6 +45,7 @@ class StorageOptions(dict):
class PolarsConnectionSettings(dict):
"""Polars S3 connection settings containing S3FS args and storage options."""
s3fs_args: S3FsArgs
storage_options: StorageOptions
@@ -49,6 +54,7 @@ class PolarsConnectionSettings(dict):
class Boto3ConnectionSettings(dict):
"""Boto3 S3 connection settings with endpoint, region, and AWS credentials."""
endpoint_url: str
region_name: str
use_ssl: bool
@@ -60,6 +66,7 @@ class Boto3ConnectionSettings(dict):
class DuckDbConnectionSettings(dict):
"""DuckDB S3 connection settings as a configuration string."""
connection_settings_str: str
def __getattr__(self, attr):
+73
View File
@@ -0,0 +1,73 @@
# System Prompts
This directory contains the single source of truth for AI system prompts used by both the frontend copilot and CLI guidance.
## Structure
```
system_prompts/
├── base/ # Core instruction templates (manually written)
├── languages/ # Language-specific instructions (manually written)
└── auto-generated/ # Auto-generated files (DO NOT EDIT)
├── sdks/ # SDK documentation
├── cli/ # CLI command documentation
├── prompts.ts # TypeScript exports
└── index.ts # Helper functions
```
## Usage
### Regenerating Prompts
When SDK methods or the OpenFlow schema change, run:
```bash
python system_prompts/generate.py
```
This will:
1. Parse TypeScript and Python SDK files to extract function signatures
2. Parse the OpenFlow YAML schema
3. Parse the CLI commands
4. Assemble complete prompts from markdown files
5. Generate TypeScript exports in `auto-generated/`
### Scope
These system prompts contain ONLY:
- How to write Windmill scripts (language syntax, conventions, SDK usage)
- How to structure Windmill flows (OpenFlow schema, module types, data flow)
- Resource type handling, S3 operations
They DO NOT contain:
- Tool usage instructions (edit_code, set_flow_json, etc.)
- IDE/editor specific commands
- Testing tool invocations
Tool instructions are added separately by the frontend and CLI.
## Integration
### Frontend
Uses Vite path alias `$system_prompts` pointing to `auto-generated/`:
```typescript
import { FLOW_GUIDANCE } from "$system_prompts/flow";
import { getLangContext } from "$system_prompts/languages";
```
### CLI
Copies the relevant prompts in /cli/src/guidance/prompts.ts
```
## Editing Guidelines
- Edit markdown files in `base/`, `languages/`
- Never edit files in `auto-generated/` directly
- After editing, run `generate.py` to update exports
@@ -0,0 +1,363 @@
# Windmill CLI Commands
The Windmill CLI (`wmill`) provides commands for managing scripts, flows, apps, and other resources.
Current version: 1.591.2
## 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
**Subcommands:**
- `app push <file_path:string> <remote_path:string>` - push a local app
- `app generate-locks [app_folder:string]` - re-generate the lockfiles for app runnables inline scripts that have changed
- `--yes` - Skip confirmation prompt
- `--dry-run` - Perform a dry run without making changes
- `--default-ts <runtime:string>` - Default TypeScript runtime (bun or deno)
### dependencies
workspace dependencies related commands
**Alias:** `deps`
**Subcommands:**
- `dependencies push <file_path:string>` - Push workspace dependencies from a local file
- `--language <language:string>` - Programming language (python3, typescript, go, php). If not specified, will be inferred from file extension.
- `--name <name:string>` - Name for the dependencies. If not specified, creates workspace default dependencies.
### dev
Launch a dev server that will spawn a webserver with HMR
**Options:**
- `--includes <pattern...:string>` - Filter paths givena glob pattern or path
### flow
flow related commands
**Options:**
- `--show-archived` - Enable archived scripts in output
**Subcommands:**
- `flow push <file_path:string> <remote_path:string>` - push a local flow spec. This overrides any remote versions.
- `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.
- `flow generate-locks [flow:file]` - re-generate the lock files of all inline scripts of all updated flows
- `--yes` - Skip confirmation prompt
- `-e --excludes <patterns:file[]>` - Comma separated patterns to specify which file to NOT take into account.
- `flow bootstrap <flow_path:string>` - create a new empty flow
- `--summary <summary:string>` - script summary
- `--description <description:string>` - script description
### folder
folder related commands
**Subcommands:**
- `folder push <file_path:string> <remote_path:string>` - push a local folder spec. This overrides any remote versions.
### 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
- `--replace` - Replace existing settings (non-interactive mode)
- `--diff` - Show differences without applying changes
- `--json-output` - Output in JSON format
- `--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
- `--diff` - Show what would be pushed without applying changes
- `--json-output` - Output in JSON format
- `--yes` - Skip interactive prompts and use default behavior
- `--promotion <branch:string>` - Use promotionOverrides from the specified branch instead of regular overrides
### 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 and SMTP)
- `--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 and SMTP)
- `--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
### jobs
Pull completed and queued jobs from workspace
**Arguments:** `[workspace:string]`
**Options:**
- `-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
### 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
### resource
resource related commands
**Subcommands:**
- `resource push <file_path:string> <remote_path:string>` - push a local resource spec. This overrides any remote versions.
### resource-type
resource type related commands
**Subcommands:**
- `resource-type list` - list all resource types
- `--schema` - Show schema in the output
- `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
**Subcommands:**
- `schedule push <file_path:string> <remote_path:string>` - push a local schedule spec. This overrides any remote versions.
### script
script related commands
**Options:**
- `--show-archived` - Enable archived scripts in output
**Subcommands:**
- `script push <path:file>` - push a local script spec. This overrides any remote versions. Use the script file (.ts, .js, .py, .sh)
- `script show <path:file>` - show a scripts content
- `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.
- `script bootstrap <path:file> <language:string>` - create a new script
- `--summary <summary:string>` - script summary
- `--description <description:string>` - script description
- `script generate-metadata [script:file]` - re-generate the metadata file updating the lock and the script schema (for flows, use `wmill flow generate-locks`)
- `--yes` - Skip confirmation prompt
- `--dry-run` - Perform a dry run without making changes
- `--lock-only` - re-generate only the lock
- `--schema-only` - re-generate only script schema
- `-e --excludes <patterns:file[]>` - Comma separated patterns to specify which file to NOT take into account.
### 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
- `--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
- `-e --excludes <patterns:file[]>` - Comma separated patterns to specify which file to NOT take into account. Overrides wmill.yaml excludes
- `--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
- `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
- `--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
- `-e --excludes <patterns:file[]>` - Comma separated patterns to specify which file to NOT take into account.
- `--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
### trigger
trigger related commands
**Subcommands:**
- `trigger push <file_path:string> <remote_path:string>` - push a local trigger spec. This overrides any remote versions.
### 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`
### variable
variable related commands
**Subcommands:**
- `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.
- `--plain-secrets` - Push secrets as plain text
- `--public` - Legacy option, use --plain-secrets 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 instance settings, users, configs, group and overwrite remote
- `--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.
- `workspace remove <workspace_name:string>` - Remove a workspace
- `workspace whoami` - Show the currently active user
- `workspace bind` - Bind the current Git branch to the active workspace
- `--branch <branch:string>` - Specify branch (defaults to current)
- `workspace unbind` - Remove workspace binding from the current Git branch
- `--branch <branch:string>` - Specify branch (defaults to current)
- `workspace fork [workspace_name:string] [workspace_id:string]` - Create a forked workspace
- `--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.
- `workspace delete-fork <fork_name:string>` - Delete a forked workspace and git branch
- `-y --yes` - Skip confirmation prompt
File diff suppressed because one or more lines are too long
+39
View File
@@ -0,0 +1,39 @@
// Auto-generated by generate.py - DO NOT EDIT
// Re-export all prompts
export * from './prompts';
import * as prompts from './prompts';
// Languages that use the TypeScript SDK
const TS_SDK_LANGUAGES = ['bun', 'deno', 'nativets', 'bunnative'];
// Languages that use the Python SDK
const PY_SDK_LANGUAGES = ['python3'];
// Helper to combine prompts for scripts
export function getScriptPrompt(language: string): string {
const langKey = `LANG_${language.toUpperCase()}` as keyof typeof prompts;
const langPrompt = (prompts as Record<string, string>)[langKey] || '';
// Determine which SDK to include based on language
let sdkPrompt = '';
if (TS_SDK_LANGUAGES.includes(language)) {
sdkPrompt = prompts.SDK_TYPESCRIPT;
} else if (PY_SDK_LANGUAGES.includes(language)) {
sdkPrompt = prompts.SDK_PYTHON;
}
return [
prompts.SCRIPT_BASE,
langPrompt,
sdkPrompt
].filter(Boolean).join('\n\n');
}
// Helper to combine prompts for flows
export function getFlowPrompt(): string {
return [
prompts.FLOW_BASE,
prompts.OPENFLOW_SCHEMA
].filter(Boolean).join('\n\n');
}
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,560 @@
# Python SDK (wmill)
Import: import wmill
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) -> 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) -> 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) -> 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) -> 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) -> 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) -> 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) -> Any
# Run a script on the current worker without creating a job
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
#
# Returns:
# Resource value dictionary or None
def get_resource(path: str, none_if_undefined: bool = False) -> 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
def set_state(value: 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
# 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
#
# Returns:
# Dictionary with approvalPage, resume, and cancel URLs
def get_resume_urls(approver: str = 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
#
# 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) -> 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
#
# 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) -> 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
def get_state() -> Any
# Get the state resource path from environment.
#
# Returns:
# State path string
def get_state_path() -> str
# Decorator to mark a function as a workflow task.
#
# When executed inside a Windmill job, the decorated function runs as a
# separate workflow step. Outside Windmill, it executes normally.
#
# Args:
# tag: Optional worker tag for execution
#
# Returns:
# Decorated function
def task(*args, **kwargs)
# Parse resource syntax from string.
def parse_resource_syntax(s: str) -> Optional[str]
# Parse S3 object from string or S3Object format.
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)
# 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()
# 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
@@ -0,0 +1,465 @@
# TypeScript SDK (windmill-client)
Import: import * as wmill from 'windmill-client'
/**
* 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): 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
* @returns Script execution result
*/
async runScriptByPath(path: string, args: Record<string, any> | null = null, verbose: boolean = false): 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
* @returns Script execution result
*/
async runScriptByHash(hash_: string, args: Record<string, any> | null = null, verbose: boolean = false): 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
* @returns Flow execution result
*/
async runFlow(path: string | null = null, args: Record<string, any> | null = null, verbose: boolean = false): 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>
/**
* Wrap a function to execute as a Windmill task within a flow context
* @param f - Function to wrap as a task
* @returns Async wrapper function that executes as a Windmill job
*/
task<P, T>(f: (_: P) => T): (_: P) => Promise<T>
/**
* @deprecated Use runScriptByPathAsync or runScriptByHashAsync instead
*/
async runScriptAsync(path: string | null, hash_: string | null, args: Record<string, any> | null, scheduledInSeconds: number | 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)
* @returns Job ID of the created job
*/
async runScriptByPathAsync(path: string, args: Record<string, any> | null = null, scheduledInSeconds: number | 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)
* @returns Job ID of the created job
*/
async runScriptByHashAsync(hash_: string, args: Record<string, any> | null = null, scheduledInSeconds: number | 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)
* @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): 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
*/
async setState(state: any): 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
*/
async getState(): 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>
/**
* Get S3 client settings from a resource or workspace default
* @param s3_resource_path - Path to S3 resource (uses workspace default if undefined)
* @returns S3 client configuration settings
*/
async denoS3LightClientSettings(s3_resource_path: string | 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);
* ```
*/
async loadS3File(s3object: S3Object, s3ResourcePath: 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());
* ```
*/
async loadS3FileStream(s3object: S3Object, s3ResourcePath: 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)
* ```
*/
async writeS3File(s3object: S3Object | undefined, fileContent: string | Blob, s3ResourcePath: string | undefined = undefined, contentType: string | undefined = undefined, contentDisposition: string | undefined = undefined): Promise<S3Object>
/**
* 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
* @returns approval page UI URL, resume and cancel API URLs for resuming the flow
*/
async getResumeUrls(approver?: string): 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.
*
* @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"] },
* });
* ```
*
* **Note:** This function requires execution within a Windmill flow or flow preview.
*/
async requestInteractiveSlackApproval({ slackResourcePath, channelId, message, approver, defaultArgsJson, dynamicEnumsJson, }: 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>
/**
* Parse an S3 object from URI string or record format
* @param s3Object - S3 object as URI string (s3://storage/key) or record
* @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"): SqlTemplateFunction
/**
* Create a SQL template function for DuckDB/ducklake queries
* @param name - DuckDB database name (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()
*/
ducklake(name: string = "main"): SqlTemplateFunction
async polarsConnectionSettings(s3_resource_path: string | undefined): Promise<any>
async duckdbConnectionSettings(s3_resource_path: string | undefined): Promise<any>
+106
View File
@@ -0,0 +1,106 @@
# Windmill Flow Building Guide
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
## 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
- `results.step_id.property` - Access specific property from previous step output
- `flow_input.iter.value` - Current item when inside a for-loop
- `flow_input.iter.index` - Current index when inside a for-loop
## 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"`
## Failure Handler
Executes when any step fails. Has access to error details:
- `error.message` - Error message
- `error.step_id` - ID of failed step
- `error.name` - Error name
- `error.stack` - Stack trace
## 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"
}
}
```
+27
View File
@@ -0,0 +1,27 @@
# Windmill Script Writing Guide
## General Principles
- Scripts must export a main function (do not call it)
- Libraries are installed automatically - do not show installation instructions
- Credentials and configuration are stored in resources and passed as parameters
- The windmill client (`wmill`) provides APIs for interacting with the platform
## Function Naming
- Main function: `main` (or `preprocessor` for preprocessor scripts)
- Must be async for TypeScript variants
## Return Values
- Scripts can return any JSON-serializable value
- Return values become available to subsequent flow steps via `results.step_id`
## Preprocessor Scripts
Preprocessor scripts process raw trigger data from various sources (webhook, custom HTTP route, SQS, WebSocket, Kafka, NATS, MQTT, Postgres, or email) before passing it to the flow. This separates the trigger logic from the flow logic and keeps the auto-generated UI clean.
The returned object determines the parameter values passed to the flow.
e.g., `{ b: 1, a: 2 }` calls the flow with `a = 2` and `b = 1`, assuming the flow has two inputs called `a` and `b`.
The preprocessor receives a single parameter called `event`.
+873
View File
@@ -0,0 +1,873 @@
#!/usr/bin/env python3
"""
Generate system prompts documentation from SDKs and OpenFlow schema.
This script:
1. Parses TypeScript SDK to extract function signatures
2. Parses Python SDK using Python's ast module
3. Parses OpenFlow YAML schema
4. Generates markdown files in sdks/ and schemas/
5. Assembles complete prompts and generates TypeScript exports in generated/
Usage:
python generate.py
"""
import ast
import json
import re
from pathlib import Path
import yaml
# Paths relative to this script
SCRIPT_DIR = Path(__file__).parent
ROOT_DIR = SCRIPT_DIR.parent
TS_SDK_DIR = ROOT_DIR / "typescript-client"
PY_SDK_PATH = ROOT_DIR / "python-client" / "wmill" / "wmill" / "client.py"
OPENFLOW_SCHEMA_PATH = ROOT_DIR / "openflow.openapi.yaml"
OUTPUT_SDKS_DIR = SCRIPT_DIR / "auto-generated" / "sdks"
OUTPUT_GENERATED_DIR = SCRIPT_DIR / "auto-generated"
OUTPUT_CLI_DIR = SCRIPT_DIR / "auto-generated" / "cli"
# CLI guidance directory (DNT can't import from outside cli/, so we copy files there)
CLI_GUIDANCE_DIR = ROOT_DIR / "cli" / "src" / "guidance"
# CLI source paths for extracting command documentation
CLI_DIR = ROOT_DIR / "cli"
CLI_MAIN = CLI_DIR / "src" / "main.ts"
CLI_COMMANDS_DIR = CLI_DIR / "src" / "commands"
def clean_jsdoc(jsdoc: str) -> str:
"""Clean up JSDoc comment, removing delimiters and leading asterisks."""
# Remove /** and */
jsdoc = re.sub(r'^/\*\*\s*', '', jsdoc)
jsdoc = re.sub(r'\s*\*/$', '', jsdoc)
# Remove leading * from each line
lines = jsdoc.split('\n')
cleaned = []
for line in lines:
line = re.sub(r'^\s*\*\s?', '', line)
cleaned.append(line)
return '\n'.join(cleaned).strip()
def extract_balanced(content: str, start_pos: int, open_char: str, close_char: str) -> tuple[str, int]:
"""
Extract content between balanced brackets starting at start_pos.
Returns (extracted_content, end_position) or ('', -1) if not found.
"""
if start_pos >= len(content) or content[start_pos] != open_char:
return '', -1
depth = 0
i = start_pos
while i < len(content):
if content[i] == open_char:
depth += 1
elif content[i] == close_char:
depth -= 1
if depth == 0:
return content[start_pos + 1:i], i
i += 1
return '', -1
def extract_return_type(content: str, start_pos: int) -> tuple[str, int]:
"""
Extract return type from position after ')', handling nested braces.
Returns (return_type, end_position of function body open brace).
"""
i = start_pos
# Skip whitespace
while i < len(content) and content[i] in ' \t\n':
i += 1
if i >= len(content) or content[i] != ':':
# No return type, find opening brace
while i < len(content) and content[i] != '{':
i += 1
return '', i
i += 1 # Skip ':'
# Now extract the return type, handling nested braces and angle brackets
return_type_start = i
brace_depth = 0
angle_depth = 0
while i < len(content):
char = content[i]
if char == '<':
angle_depth += 1
elif char == '>':
angle_depth -= 1
elif char == '{':
if angle_depth > 0:
# Inside a type like Promise<{...}>
brace_depth += 1
else:
# This is the function body opening brace
return content[return_type_start:i].strip(), i
elif char == '}':
brace_depth -= 1
i += 1
return '', -1
def extract_ts_functions(content: str) -> list[dict]:
"""Extract exported function signatures from TypeScript SDK."""
functions = []
seen_names = set()
# Pattern to find JSDoc followed by export function declaration
# Only captures up to the function name and optional generic
jsdoc_func_pattern = re.compile(
r'(/\*\*(?:[^*]|\*(?!/))*\*/)\s*' # JSDoc comment
r'export\s+(async\s+)?function\s+(\w+)\s*' # export [async] function name
r'(<[^>]+>)?\s*', # optional generic
re.MULTILINE
)
# Pattern to find export function without JSDoc
func_pattern = re.compile(
r'export\s+(async\s+)?function\s+(\w+)\s*' # export [async] function name
r'(<[^>]+>)?\s*', # optional generic
re.MULTILINE
)
# First, find all functions with JSDoc
for match in jsdoc_func_pattern.finditer(content):
jsdoc_raw, is_async, name, generic = match.groups()
if name in seen_names:
continue
# Find the opening parenthesis for parameters
pos = match.end()
while pos < len(content) and content[pos] in ' \t\n':
pos += 1
if pos >= len(content) or content[pos] != '(':
continue
# Extract balanced parameters
params, paren_end = extract_balanced(content, pos, '(', ')')
if paren_end == -1:
continue
# Extract return type (handles multi-line types like Promise<{...}>)
return_type, _ = extract_return_type(content, paren_end + 1)
if not return_type:
return_type = 'Promise<void>' if is_async else 'void'
docstring = clean_jsdoc(jsdoc_raw)
seen_names.add(name)
functions.append({
'name': name,
'generic': generic or '',
'params': clean_params(params),
'return_type': return_type,
'async': bool(is_async),
'docstring': docstring
})
# Then find functions without JSDoc (that weren't already captured)
for match in func_pattern.finditer(content):
is_async, name, generic = match.groups()
if name in seen_names:
continue
# Find the opening parenthesis for parameters
pos = match.end()
while pos < len(content) and content[pos] in ' \t\n':
pos += 1
if pos >= len(content) or content[pos] != '(':
continue
# Extract balanced parameters
params, paren_end = extract_balanced(content, pos, '(', ')')
if paren_end == -1:
continue
# Extract return type (handles multi-line types like Promise<{...}>)
return_type, _ = extract_return_type(content, paren_end + 1)
if not return_type:
return_type = 'Promise<void>' if is_async else 'void'
seen_names.add(name)
functions.append({
'name': name,
'generic': generic or '',
'params': clean_params(params),
'return_type': return_type,
'async': bool(is_async),
'docstring': ''
})
return functions
def clean_params(params: str) -> str:
"""Clean up parameter string."""
if not params:
return ''
# Remove excessive whitespace and newlines
params = re.sub(r'\s+', ' ', params).strip()
return params
def extract_ts_types(content: str) -> list[dict]:
"""Extract exported type definitions from TypeScript SDK."""
types = []
# Pattern for exported type aliases
type_pattern = re.compile(
r'export\s+type\s+(\w+)\s*=\s*([^;]+);',
re.MULTILINE
)
# Pattern for exported interfaces
interface_pattern = re.compile(
r'export\s+interface\s+(\w+)\s*\{([^}]+)\}',
re.MULTILINE | re.DOTALL
)
for match in type_pattern.finditer(content):
name, definition = match.groups()
types.append({
'name': name,
'kind': 'type',
'definition': definition.strip()
})
for match in interface_pattern.finditer(content):
name, body = match.groups()
types.append({
'name': name,
'kind': 'interface',
'definition': body.strip()
})
return types
def extract_py_functions(content: str) -> list[dict]:
"""Extract function signatures from Python SDK using AST."""
functions = []
seen_names = set()
try:
tree = ast.parse(content)
except SyntaxError as e:
print(f"Warning: Could not parse Python SDK: {e}")
return functions
def process_function(node):
"""Process a function node and add to functions list if not duplicate."""
# Skip private functions
if node.name.startswith('_') and not node.name.startswith('__'):
return
# Skip duplicates
if node.name in seen_names:
return
# Get docstring
docstring = ast.get_docstring(node) or ''
# Build parameter list
params = []
args = node.args
# Handle regular args
num_defaults = len(args.defaults)
num_args = len(args.args)
for i, arg in enumerate(args.args):
if arg.arg == 'self':
continue
param_str = arg.arg
if arg.annotation:
param_str += f": {ast.unparse(arg.annotation)}"
# Check if has default
default_idx = i - (num_args - num_defaults)
if default_idx >= 0:
default = args.defaults[default_idx]
param_str += f" = {ast.unparse(default)}"
params.append(param_str)
# Handle *args
if args.vararg:
params.append(f"*{args.vararg.arg}")
# Handle keyword-only args
for i, arg in enumerate(args.kwonlyargs):
param_str = arg.arg
if arg.annotation:
param_str += f": {ast.unparse(arg.annotation)}"
if args.kw_defaults[i]:
param_str += f" = {ast.unparse(args.kw_defaults[i])}"
params.append(param_str)
# Handle **kwargs
if args.kwarg:
params.append(f"**{args.kwarg.arg}")
# Get return type
return_type = ''
if node.returns:
return_type = ast.unparse(node.returns)
seen_names.add(node.name)
functions.append({
'name': node.name,
'params': ', '.join(params),
'return_type': return_type,
'docstring': docstring,
'async': isinstance(node, ast.AsyncFunctionDef)
})
# Process top-level functions and class methods (but not nested functions)
for node in tree.body:
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
process_function(node)
elif isinstance(node, ast.ClassDef):
for item in node.body:
if isinstance(item, (ast.FunctionDef, ast.AsyncFunctionDef)):
process_function(item)
return functions
def extract_py_classes(content: str) -> list[dict]:
"""Extract class definitions from Python SDK."""
classes = []
try:
tree = ast.parse(content)
except SyntaxError:
return classes
for node in ast.walk(tree):
if isinstance(node, ast.ClassDef):
methods = []
for item in node.body:
if isinstance(item, (ast.FunctionDef, ast.AsyncFunctionDef)):
if not item.name.startswith('_') or item.name == '__init__':
docstring = ast.get_docstring(item) or ''
methods.append({
'name': item.name,
'docstring': docstring
})
classes.append({
'name': node.name,
'docstring': ast.get_docstring(node) or '',
'methods': methods
})
return classes
def parse_command_block(content: str) -> dict:
"""
Parse a Cliffy Command() definition block and extract metadata.
Returns a dict with: description, options, subcommands, arguments, alias
"""
result = {
'description': '',
'options': [],
'subcommands': [],
'arguments': '',
'alias': ''
}
# Find the command block - starts with "new Command()" or "const command = new Command()"
# and ends with "export default"
command_match = re.search(
r'(?:const\s+command\s*=\s*)?new\s+Command\(\)([\s\S]*?)(?=export\s+default)',
content
)
if not command_match:
return result
block = command_match.group(1)
# Extract main description (first one in the block, before any subcommand)
first_subcommand_pos = block.find('.command(')
if first_subcommand_pos == -1:
first_subcommand_pos = len(block)
top_section = block[:first_subcommand_pos]
# Handle multi-line descriptions with template literals or string concatenation
# Pattern: .description("text") or .description(\n "text",\n)
desc_match = re.search(r'\.description\(\s*["\']([^"\']+)["\']\s*,?\s*\)', top_section, re.DOTALL)
if not desc_match:
# Try matching descriptions that use template literals
desc_match = re.search(r'\.description\(\s*`([^`]+)`\s*,?\s*\)', top_section, re.DOTALL)
if desc_match:
result['description'] = desc_match.group(1).strip()
# Extract alias
alias_match = re.search(r'\.alias\(\s*["\']([^"\']+)["\']\s*\)', top_section)
if alias_match:
result['alias'] = alias_match.group(1)
# Extract options pattern
option_pattern = re.compile(
r'\.option\(\s*["\']([^"\']+)["\']\s*,\s*["\']([^"\']+)["\']\s*\)',
re.MULTILINE
)
# Extract top-level options (before any .command() or .action())
top_section_until_action = re.split(r'\.action\(', top_section)[0]
for match in option_pattern.finditer(top_section_until_action):
flag, desc = match.groups()
result['options'].append({'flag': flag, 'description': desc})
# Extract top-level arguments
args_match = re.search(r'\.arguments\(\s*["\']([^"\']+)["\']\s*\)', top_section)
if args_match:
result['arguments'] = args_match.group(1)
# Extract subcommands with their arguments, options, and descriptions
# Split by .command( to find subcommand boundaries
subcommand_sections = re.split(r'(?=\.command\()', block)
for section in subcommand_sections:
# Check if this starts a new subcommand
cmd_match = re.match(r'\.command\(\s*["\']([^"\']+)["\']\s*(?:,\s*["\']([^"\']+)["\'])?\s*\)', section)
if cmd_match:
cmd_name = cmd_match.group(1)
cmd_desc = cmd_match.group(2) or ''
# Check for description in chained .description() call
# Handle multi-line descriptions
desc_match = re.search(r'\.description\(\s*["\']([^"\']+)["\']\s*,?\s*\)', section, re.DOTALL)
if desc_match:
cmd_desc = desc_match.group(1).strip()
# Check for arguments
args_match = re.search(r'\.arguments\(\s*["\']([^"\']+)["\']\s*\)', section)
cmd_args = args_match.group(1) if args_match else ''
# Check for options specific to this subcommand
# Only get options that appear before .action()
cmd_options = []
section_until_action = re.split(r'\.action\(', section)[0]
for opt_match in option_pattern.finditer(section_until_action):
flag, desc = opt_match.groups()
cmd_options.append({'flag': flag, 'description': desc})
result['subcommands'].append({
'name': cmd_name,
'description': cmd_desc,
'arguments': cmd_args,
'options': cmd_options
})
return result
def find_command_file(cmd_name: str) -> Path | None:
"""Find the command file for a given command name.
Convention: directory name should match main command file name.
E.g., flow/flow.ts, app/app.ts, worker-groups/worker-groups.ts
"""
# Standard pattern: command-name/command-name.ts
standard_path = CLI_COMMANDS_DIR / cmd_name / f"{cmd_name}.ts"
if standard_path.exists():
return standard_path
return None
def extract_cli_commands() -> dict:
"""
Extract CLI command metadata from the CLI source files.
Returns a dict with global_options and commands.
"""
result = {
'version': '',
'global_options': [],
'commands': []
}
if not CLI_MAIN.exists():
print(f"Warning: CLI main file not found at {CLI_MAIN}")
return result
main_content = CLI_MAIN.read_text()
# Extract version
version_match = re.search(r'export\s+const\s+VERSION\s*=\s*["\']([^"\']+)["\']', main_content)
if version_match:
result['version'] = version_match.group(1)
# Extract global options from main.ts
global_opt_pattern = re.compile(
r'\.globalOption\(\s*["\']([^"\']+)["\']\s*,\s*["\']([^"\']+)["\']\s*\)',
re.MULTILINE
)
for match in global_opt_pattern.finditer(main_content):
flag, desc = match.groups()
result['global_options'].append({'flag': flag, 'description': desc})
# Extract command registrations from main.ts
# Pattern: .command("name", importedCommand) or .command("name with desc", ...)
cmd_reg_pattern = re.compile(
r'\.command\(\s*["\']([^"\']+)["\']\s*,\s*(\w+)\s*\)',
re.MULTILINE
)
# Also handle inline commands like .command("version --version", "description")
inline_cmd_pattern = re.compile(
r'\.command\(\s*["\']([^"\']+)["\']\s*,\s*["\']([^"\']+)["\']\s*\)',
re.MULTILINE
)
registered_commands = []
for match in cmd_reg_pattern.finditer(main_content):
cmd_name = match.group(1).split()[0] # Get just the name, not flags
registered_commands.append(cmd_name)
# Process each registered command
for cmd_name in registered_commands:
cmd_file = find_command_file(cmd_name)
if cmd_file:
try:
cmd_content = cmd_file.read_text()
cmd_data = parse_command_block(cmd_content)
cmd_data['name'] = cmd_name
result['commands'].append(cmd_data)
except Exception as e:
print(f"Warning: Could not parse command file for {cmd_name}: {e}")
else:
# Some commands might be inline (like 'version', 'upgrade', 'completions')
pass
# Handle special inline commands from main.ts
for match in inline_cmd_pattern.finditer(main_content):
cmd_name = match.group(1).split()[0]
cmd_desc = match.group(2)
if cmd_name not in [c['name'] for c in result['commands']]:
result['commands'].append({
'name': cmd_name,
'description': cmd_desc,
'options': [],
'subcommands': [],
'arguments': '',
'alias': ''
})
return result
def generate_cli_commands_markdown(cli_data: dict) -> str:
"""Generate markdown documentation from extracted CLI command data."""
md = "# Windmill CLI Commands\n\n"
md += "The Windmill CLI (`wmill`) provides commands for managing scripts, flows, apps, and other resources.\n\n"
if cli_data.get('version'):
md += f"Current version: {cli_data['version']}\n\n"
# Global options
if cli_data.get('global_options'):
md += "## Global Options\n\n"
for opt in cli_data['global_options']:
flag = opt['flag']
desc = opt['description']
md += f"- `{flag}` - {desc}\n"
md += "\n"
# Commands
if cli_data.get('commands'):
md += "## Commands\n\n"
for cmd in sorted(cli_data['commands'], key=lambda x: x['name']):
md += f"### {cmd['name']}\n\n"
if cmd.get('description'):
md += f"{cmd['description']}\n\n"
if cmd.get('alias'):
md += f"**Alias:** `{cmd['alias']}`\n\n"
if cmd.get('arguments'):
md += f"**Arguments:** `{cmd['arguments']}`\n\n"
# Top-level options for this command
if cmd.get('options'):
md += "**Options:**\n"
for opt in cmd['options']:
md += f"- `{opt['flag']}` - {opt['description']}\n"
md += "\n"
# Subcommands
if cmd.get('subcommands'):
md += "**Subcommands:**\n\n"
for sub in cmd['subcommands']:
sub_name = sub['name']
sub_args = f" {sub['arguments']}" if sub.get('arguments') else ""
sub_desc = sub.get('description', '')
md += f"- `{cmd['name']} {sub_name}{sub_args}`"
if sub_desc:
md += f" - {sub_desc}"
md += "\n"
# Subcommand options
if sub.get('options'):
for opt in sub['options']:
md += f" - `{opt['flag']}` - {opt['description']}\n"
md += "\n"
return md
def generate_ts_sdk_markdown(functions: list[dict], types: list[dict]) -> str:
"""Generate compact documentation for TypeScript SDK."""
md = "# TypeScript SDK (windmill-client)\n\n"
md += "Import: import * as wmill from 'windmill-client'\n\n"
for i, func in enumerate(functions):
if func.get('docstring'):
# Format docstrings with JSDoc /** */ syntax
md += "/**\n"
docstring_lines = func['docstring'].split('\n')
for line in docstring_lines:
md += f" * {line}\n"
md += " */\n"
async_prefix = 'async ' if func['async'] else ''
md += f"{async_prefix}{func['name']}{func['generic']}({func['params']}): {func['return_type']}"
md += "\n"
if i < len(functions) - 1:
md += "\n"
return md
def generate_py_sdk_markdown(functions: list[dict], classes: list[dict]) -> str:
"""Generate compact documentation for Python SDK."""
md = "# Python SDK (wmill)\n\n"
md += "Import: import wmill\n\n"
for func in functions:
# Skip private functions
if func['name'].startswith('_'):
continue
docstring = func.get('docstring')
if docstring:
# Format multi-line docstrings with # prefix on each line
docstring_lines = docstring.split('\n')
for line in docstring_lines:
md += f"# {line}\n"
async_prefix = 'async ' if func['async'] else ''
return_annotation = f" -> {func['return_type']}" if func['return_type'] else ''
md += f"{async_prefix}def {func['name']}({func['params']}){return_annotation}\n"
md += "\n"
return md
def escape_for_ts(content: str) -> str:
"""Escape content for TypeScript template literal."""
return content.replace('\\', '\\\\').replace('`', '\\`').replace('${', '\\${')
def generate_ts_exports(prompts: dict[str, str]) -> str:
"""Generate TypeScript file that exports all prompts."""
ts = "// Auto-generated by generate.py - DO NOT EDIT\n\n"
for name, content in prompts.items():
escaped = escape_for_ts(content)
ts += f"export const {name} = `{escaped}`;\n\n"
return ts
def read_markdown_file(path: Path) -> str:
"""Read a markdown file and return its content."""
if path.exists():
return path.read_text()
return ''
def main():
"""Main generation function."""
print("Generating system prompts documentation...")
# Ensure output directories exist
OUTPUT_SDKS_DIR.mkdir(parents=True, exist_ok=True)
OUTPUT_GENERATED_DIR.mkdir(parents=True, exist_ok=True)
# Read SDK files
ts_content = ''
if TS_SDK_DIR.exists():
for ts_file in TS_SDK_DIR.glob('*.ts'):
if not ts_file.name.endswith('.d.ts'):
ts_content += ts_file.read_text() + '\n'
py_content = PY_SDK_PATH.read_text() if PY_SDK_PATH.exists() else ''
openflow_raw = OPENFLOW_SCHEMA_PATH.read_text() if OPENFLOW_SCHEMA_PATH.exists() else ''
# Extract only components.schemas from OpenFlow and convert to minified JSON
openflow_yaml = yaml.safe_load(openflow_raw) if openflow_raw else {}
openflow_schemas = openflow_yaml.get('components', {}).get('schemas', {})
openflow_schemas_json = json.dumps(openflow_schemas, separators=(',', ':'))
openflow_content = f"## OpenFlow Schema\n\n{openflow_schemas_json}"
# Extract TypeScript SDK info
print("Parsing TypeScript SDK...")
ts_functions = extract_ts_functions(ts_content)
ts_types = extract_ts_types(ts_content)
ts_sdk_md = generate_ts_sdk_markdown(ts_functions, ts_types)
(OUTPUT_SDKS_DIR / "typescript.md").write_text(ts_sdk_md)
print(f" Found {len(ts_functions)} functions, {len(ts_types)} types")
# Extract Python SDK info
print("Parsing Python SDK...")
py_functions = extract_py_functions(py_content)
py_classes = extract_py_classes(py_content)
py_sdk_md = generate_py_sdk_markdown(py_functions, py_classes)
(OUTPUT_SDKS_DIR / "python.md").write_text(py_sdk_md)
print(f" Found {len(py_functions)} functions, {len(py_classes)} classes")
# Read base prompts
print("Assembling complete prompts...")
base_dir = SCRIPT_DIR / "base"
languages_dir = SCRIPT_DIR / "languages"
script_base = read_markdown_file(base_dir / "script-base.md")
flow_base = read_markdown_file(base_dir / "flow-base.md")
# Read language files
languages = {}
for lang_file in languages_dir.glob("*.md"):
languages[lang_file.stem] = lang_file.read_text()
# Extract and generate CLI commands documentation
print("Extracting CLI commands...")
cli_data = extract_cli_commands()
cli_commands = generate_cli_commands_markdown(cli_data)
OUTPUT_CLI_DIR.mkdir(parents=True, exist_ok=True)
(OUTPUT_CLI_DIR / "cli-commands.md").write_text(cli_commands)
print(f" Found {len(cli_data['commands'])} commands, {len(cli_data['global_options'])} global options")
# Assemble prompts for export
prompts = {
# Base prompts
'SCRIPT_BASE': script_base,
'FLOW_BASE': flow_base,
# SDKs
'SDK_TYPESCRIPT': ts_sdk_md,
'SDK_PYTHON': py_sdk_md,
# Schema (raw YAML content)
'OPENFLOW_SCHEMA': openflow_content,
# CLI
'CLI_COMMANDS': cli_commands,
}
# Add language prompts
for lang_name, lang_content in languages.items():
prompts[f'LANG_{lang_name.upper()}'] = lang_content
# Generate TypeScript exports
ts_exports = generate_ts_exports(prompts)
(OUTPUT_GENERATED_DIR / "prompts.ts").write_text(ts_exports)
# Generate complete script.md (all languages combined)
script_md_parts = [script_base]
for lang_name in sorted(languages.keys()):
script_md_parts.append(languages[lang_name])
script_md_parts.extend([ts_sdk_md, py_sdk_md])
script_md = "\n\n".join(filter(None, script_md_parts))
(OUTPUT_GENERATED_DIR / "script.md").write_text(script_md)
# Generate complete flow.md
flow_md_parts = [flow_base, openflow_content]
flow_md = "\n\n".join(filter(None, flow_md_parts))
(OUTPUT_GENERATED_DIR / "flow.md").write_text(flow_md)
# Generate an index file
index_content = """// Auto-generated by generate.py - DO NOT EDIT
// Re-export all prompts
export * from './prompts';
import * as prompts from './prompts';
// Languages that use the TypeScript SDK
const TS_SDK_LANGUAGES = ['bun', 'deno', 'nativets', 'bunnative'];
// Languages that use the Python SDK
const PY_SDK_LANGUAGES = ['python3'];
// Helper to combine prompts for scripts
export function getScriptPrompt(language: string): string {
const langKey = `LANG_${language.toUpperCase()}` as keyof typeof prompts;
const langPrompt = (prompts as Record<string, string>)[langKey] || '';
// Determine which SDK to include based on language
let sdkPrompt = '';
if (TS_SDK_LANGUAGES.includes(language)) {
sdkPrompt = prompts.SDK_TYPESCRIPT;
} else if (PY_SDK_LANGUAGES.includes(language)) {
sdkPrompt = prompts.SDK_PYTHON;
}
return [
prompts.SCRIPT_BASE,
langPrompt,
sdkPrompt
].filter(Boolean).join('\\n\\n');
}
// Helper to combine prompts for flows
export function getFlowPrompt(): string {
return [
prompts.FLOW_BASE,
prompts.OPENFLOW_SCHEMA
].filter(Boolean).join('\\n\\n');
}
"""
(OUTPUT_GENERATED_DIR / "index.ts").write_text(index_content)
# Generate CLI-specific prompts.ts with only SCRIPT_PROMPT and FLOW_PROMPT
print("Generating CLI prompts...")
CLI_GUIDANCE_DIR.mkdir(parents=True, exist_ok=True)
cli_prompts = {
'SCRIPT_PROMPT': script_md,
'FLOW_PROMPT': flow_md,
'CLI_COMMANDS': cli_commands,
}
cli_prompts_ts = generate_ts_exports(cli_prompts)
(CLI_GUIDANCE_DIR / "prompts.ts").write_text(cli_prompts_ts)
print(f"\nGenerated files:")
print(f" - auto-generated/sdks/typescript.md")
print(f" - auto-generated/sdks/python.md")
print(f" - auto-generated/cli/cli-commands.md (auto-generated from CLI source)")
print(f" - auto-generated/prompts.ts")
print(f" - auto-generated/index.ts")
print(f" - auto-generated/script.md")
print(f" - auto-generated/flow.md")
print(f"\nGenerated for CLI:")
print(f" - cli/src/guidance/prompts.ts")
print("\nDone!")
if __name__ == '__main__':
main()
+49
View File
@@ -0,0 +1,49 @@
# 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"
```
+11
View File
@@ -0,0 +1,11 @@
# 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;
```
+110
View File
@@ -0,0 +1,110 @@
# TypeScript (Bun)
Bun runtime with full npm ecosystem and fastest execution.
## 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.
## Imports
```typescript
import Stripe from "stripe";
import { someFunction } from "some-package";
```
## Windmill Client
Import the windmill client for platform interactions:
```typescript
import * as wmill from "windmill-client";
```
See the SDK documentation for available methods.
## 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.
### S3Object Type
The S3Object type represents a file in S3 storage:
```typescript
type S3Object = {
s3: string; // Path within the bucket
};
```
## TypeScript 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: S3Object = await wmill.writeS3File(
s3object, // Target path (or undefined to auto-generate)
fileContent, // string or Blob
s3ResourcePath // Optional: specific S3 resource to use
);
```
+108
View File
@@ -0,0 +1,108 @@
# TypeScript (Bun Native)
Native TypeScript execution with fetch only - no external imports allowed.
## 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.
## 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.
## Imports
**No imports allowed.** Use the globally available `fetch` function:
```typescript
export async function main(url: string) {
const response = await fetch(url);
return await response.json();
}
```
## Windmill Client
The windmill client is not available in native TypeScript mode. Use fetch to call APIs directly.
## 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.
### S3Object Type
The S3Object type represents a file in S3 storage:
```typescript
type S3Object = {
s3: string; // Path within the bucket
};
```
## TypeScript 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: S3Object = await wmill.writeS3File(
s3object, // Target path (or undefined to auto-generate)
fileContent, // string or Blob
s3ResourcePath // Optional: specific S3 resource to use
);
```
+41
View File
@@ -0,0 +1,41 @@
# 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);
}
}
```
+114
View File
@@ -0,0 +1,114 @@
# TypeScript (Deno)
Deno runtime with npm support via `npm:` prefix and native Deno libraries.
## 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.
## 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";
```
See the SDK documentation for available methods.
## 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.
### S3Object Type
The S3Object type represents a file in S3 storage:
```typescript
type S3Object = {
s3: string; // Path within the bucket
};
```
## TypeScript 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: S3Object = await wmill.writeS3File(
s3object, // Target path (or undefined to auto-generate)
fileContent, // string or Blob
s3ResourcePath // Optional: specific S3 resource to use
);
```
+51
View File
@@ -0,0 +1,51 @@
# 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');
```
+58
View File
@@ -0,0 +1,58 @@
# 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
}
```
+45
View File
@@ -0,0 +1,45 @@
# 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
}
}
```
+38
View File
@@ -0,0 +1,38 @@
# 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);
}
}
```
+11
View File
@@ -0,0 +1,11 @@
# 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;
```
+11
View File
@@ -0,0 +1,11 @@
# 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 > ?;
```
+75
View File
@@ -0,0 +1,75 @@
# TypeScript (Native)
Native TypeScript execution with fetch only - no external imports allowed.
## 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.
## 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.
## Imports
**No imports allowed.** Use the globally available `fetch` function:
```typescript
export async function main(url: string) {
const response = await fetch(url);
return await response.json();
}
```
## Windmill Client
The windmill client is not available in native TypeScript mode. Use fetch to call APIs directly.
## 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
};
}
```
+57
View File
@@ -0,0 +1,57 @@
# 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.
+11
View File
@@ -0,0 +1,11 @@
# 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;
```
+55
View File
@@ -0,0 +1,55 @@
# 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
```
+118
View File
@@ -0,0 +1,118 @@
# 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.
```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
)
```
+75
View File
@@ -0,0 +1,75 @@
# 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 })
})
}
```
+11
View File
@@ -0,0 +1,11 @@
# 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 > ?;
```
+98 -19
View File
@@ -50,6 +50,11 @@ export const SHARED_FOLDER = "/shared";
let mockedApi: MockedApi | undefined = undefined;
/**
* 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)
*/
export function setClient(token?: string, baseUrl?: string) {
if (baseUrl === undefined) {
baseUrl =
@@ -181,6 +186,13 @@ async function _runScriptInternal(
return await waitJob(jobId, verbose);
}
/**
* 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
* @returns Script execution result
*/
export async function runScriptByPath(
path: string,
args: Record<string, any> | null = null,
@@ -189,6 +201,13 @@ export async function runScriptByPath(
return _runScriptInternal(path, null, args, verbose);
}
/**
* 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
* @returns Script execution result
*/
export async function runScriptByHash(
hash_: string,
args: Record<string, any> | null = null,
@@ -215,6 +234,13 @@ export async function streamResult(stream: AsyncIterable<string>) {
}
}
/**
* 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
* @returns Flow execution result
*/
export async function runFlow(
path: string | null = null,
args: Record<string, any> | null = null,
@@ -230,6 +256,12 @@ export async function runFlow(
return await waitJob(jobId, verbose);
}
/**
* 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
*/
export async function waitJob(
jobId: string,
verbose: boolean = false
@@ -266,11 +298,21 @@ export async function waitJob(
}
}
/**
* Get the result of a completed job
* @param jobId - ID of the completed job
* @returns Job result
*/
export async function getResult(jobId: string): Promise<any> {
const workspace = getWorkspace();
return await JobService.getCompletedJobResult({ workspace, id: jobId });
}
/**
* 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
*/
export async function getResultMaybe(jobId: string): Promise<any> {
const workspace = getWorkspace();
return await JobService.getCompletedJobResultMaybe({ workspace, id: jobId });
@@ -287,6 +329,11 @@ function getParamNames(func: Function): string[] {
return result;
}
/**
* Wrap a function to execute as a Windmill task within a flow context
* @param f - Function to wrap as a task
* @returns Async wrapper function that executes as a Windmill job
*/
export function task<P, T>(f: (_: P) => T): (_: P) => Promise<T> {
return async (...y) => {
const args: Record<string, any> = {};
@@ -378,6 +425,13 @@ async function _runScriptAsyncInternal(
}).then((res) => res.text());
}
/**
* 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)
* @returns Job ID of the created job
*/
export async function runScriptByPathAsync(
path: string,
args: Record<string, any> | null = null,
@@ -386,6 +440,13 @@ export async function runScriptByPathAsync(
return _runScriptAsyncInternal(path, null, args, scheduledInSeconds);
}
/**
* 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)
* @returns Job ID of the created job
*/
export async function runScriptByHashAsync(
hash_: string,
args: Record<string, any> | null = null,
@@ -394,6 +455,14 @@ export async function runScriptByHashAsync(
return _runScriptAsyncInternal(null, hash_, args, scheduledInSeconds);
}
/**
* 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)
* @returns Job ID of the created job
*/
export async function runFlowAsync(
path: string | null,
args: Record<string, any> | null,
@@ -455,6 +524,10 @@ export async function resolveDefaultResource(obj: any): Promise<any> {
}
}
/**
* Get the state file path from environment variables
* @returns State path string
*/
export function getStatePath(): string {
const state_path = getEnv("WM_STATE_PATH_NEW") ?? getEnv("WM_STATE_PATH");
if (state_path === undefined) {
@@ -618,25 +691,6 @@ export async function getFlowUserState(
}
}
// /**
// * Set the shared state
// * @param state state to set
// */
// export async function setSharedState(
// state: any,
// path = "state.json"
// ): Promise<void> {
// await Deno.writeTextFile(SHARED_FOLDER + "/" + path, JSON.stringify(state));
// }
// /**
// * Get the shared state
// * @param state state to set
// */
// export async function getSharedState(path = "state.json"): Promise<any> {
// return JSON.parse(await Deno.readTextFile(SHARED_FOLDER + "/" + path));
// }
/**
* Get the internal state
* @deprecated use getState instead
@@ -718,6 +772,11 @@ export async function setVariable(
}
}
/**
* Build a PostgreSQL connection URL from a database resource
* @param path - Path to the database resource
* @returns PostgreSQL connection URL string
*/
export async function databaseUrlFromResource(path: string): Promise<string> {
const resource = await getResource(path);
return `postgresql://${resource.user}:${resource.password}@${resource.host}:${resource.port}/${resource.dbname}?sslmode=${resource.sslmode}`;
@@ -744,6 +803,11 @@ export async function databaseUrlFromResource(path: string): Promise<string> {
// });
// }
/**
* Get S3 client settings from a resource or workspace default
* @param s3_resource_path - Path to S3 resource (uses workspace default if undefined)
* @returns S3 client configuration settings
*/
export async function denoS3LightClientSettings(
s3_resource_path: string | undefined
): Promise<DenoS3LightClientSettings> {
@@ -1018,10 +1082,20 @@ export async function getIdToken(
});
}
/**
* Convert a base64-encoded string to Uint8Array
* @param data - Base64-encoded string
* @returns Decoded Uint8Array
*/
export function base64ToUint8Array(data: string): Uint8Array {
return Uint8Array.from(atob(data), (c) => c.charCodeAt(0));
}
/**
* Convert a Uint8Array to base64-encoded string
* @param arrayBuffer - Uint8Array to encode
* @returns Base64-encoded string
*/
export function uint8ArrayToBase64(arrayBuffer: Uint8Array): string {
let base64 = "";
const encodings =
@@ -1342,6 +1416,11 @@ function parseResourceSyntax(s: string | undefined) {
if (s?.startsWith("res://")) return s.substring(6);
}
/**
* Parse an S3 object from URI string or record format
* @param s3Object - S3 object as URI string (s3://storage/key) or record
* @returns S3 object record with storage and s3 key
*/
export function parseS3Object(s3Object: S3Object): S3ObjectRecord {
if (typeof s3Object === "object") return s3Object;
const match = s3Object.match(/^s3:\/\/([^/]*)\/(.*)$/);
+23
View File
@@ -1,18 +1,41 @@
/**
* S3 object representation, either as a URI string or a record object
*/
export type S3Object = S3ObjectURI | S3ObjectRecord;
/**
* S3 object URI in the format `s3://storage/key`
*/
export type S3ObjectURI = `s3://${string}/${string}`;
/**
* S3 object record with file key, optional storage identifier, and optional presigned token
*/
export type S3ObjectRecord = {
/** File key/path in S3 bucket */
s3: string;
/** Storage backend identifier */
storage?: string;
/** Presigned URL query string for public access */
presigned?: string;
};
/**
* S3 client configuration settings for Deno S3 light client
*/
export type DenoS3LightClientSettings = {
/** S3 endpoint URL */
endPoint: string;
/** AWS region */
region: string;
/** Bucket name */
bucket?: string;
/** Use HTTPS connection */
useSSL?: boolean;
/** AWS access key */
accessKey?: string;
/** AWS secret key */
secretKey?: string;
/** Use path-style URLs instead of virtual-hosted style */
pathStyle?: boolean;
};
+24
View File
@@ -33,25 +33,46 @@ type SqlResult<ResultCollectionT extends ResultCollection> =
: ResultCollectionT extends "all_statements_first_row_scalar"
? any[]
: unknown;
/**
* SQL statement object with query content, arguments, and execution methods
*/
export type SqlStatement = {
/** Raw SQL content with formatted arguments */
content: string;
/** Argument values keyed by parameter name */
args: Record<string, any>;
/**
* Execute the SQL query and return results
* @param params - Optional parameters including result collection mode
* @returns Query results based on the result collection mode
*/
fetch<ResultCollectionT extends ResultCollection = "last_statement_all_rows">(
params?: FetchParams<ResultCollectionT | ResultCollection> // The union is for auto-completion
): Promise<SqlResult<ResultCollectionT>>;
/**
* Execute the SQL query and return only the first row
* @param params - Optional parameters
* @returns First row of the query result
*/
fetchOne(
params?: Omit<FetchParams<"last_statement_first_row">, "resultCollection">
): Promise<SqlResult<"last_statement_first_row">>;
};
/**
* Template tag function for creating SQL statements with parameterized values
*/
export interface SqlTemplateFunction {
(strings: TemplateStringsArray, ...values: any[]): SqlStatement;
}
/**
* 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'
@@ -66,6 +87,9 @@ export function datatable(name: string = "main"): SqlTemplateFunction {
}
/**
* Create a SQL template function for DuckDB/ducklake queries
* @param name - DuckDB database name (default: "main")
* @returns SQL template function for building parameterized queries
* @example
* let sql = wmill.ducklake()
* let name = 'Robin'