diff --git a/cli/README.md b/cli/README.md index c7e3fda45b..ee3b68c35f 100644 --- a/cli/README.md +++ b/cli/README.md @@ -112,39 +112,15 @@ source <(wmill completions zsh) ### Testing with a local `windmill-yaml-validator` -The CLI imports `windmill-yaml-validator` from npm (`npm:windmill-yaml-validator@1.1.0`). -To test local changes to the validator before publishing, use the Deno compatibility -script and import map override: - -1. Make the validator sources Deno-compatible: +To test local changes to the validator before publishing, use `npm link`: ```bash -cd ../windmill-yaml-validator -./deno-compat.sh -``` +# In windmill-yaml-validator/ +npm run build +npm link -2. Add the following entries to `cli/deno.json` imports: - -```json -"npm:windmill-yaml-validator@1.1.0": "../windmill-yaml-validator/src/index.ts", -"ajv": "npm:ajv@^8.17.1", -"@stoplight/yaml": "npm:@stoplight/yaml@^4.3.0" -``` - -3. Run the CLI directly with Deno: - -```bash -deno run -A src/main.ts lint -``` - -4. When done, restore everything: - -```bash -# Restore validator sources -cd ../windmill-yaml-validator -./deno-compat.sh -r - -# Remove the 3 import map lines from cli/deno.json +# In cli/ +npm link windmill-yaml-validator ``` ### Running Tests @@ -156,13 +132,13 @@ cd ../windmill-yaml-validator **Run tests locally (full features):** ```bash -deno test --allow-all --no-check +bun test test/ ``` **Run tests in CI mode (minimal features, skips EE tests):** ```bash -CI_MINIMAL_FEATURES=true deno test --allow-all --no-check +CI_MINIMAL_FEATURES=true bun test test/ ``` | Variable | Description | diff --git a/cli/bun.lock b/cli/bun.lock index 7fea1c929b..e400420a63 100644 --- a/cli/bun.lock +++ b/cli/bun.lock @@ -34,6 +34,7 @@ "yaml": "^2.7.0", }, "devDependencies": { + "@types/bun": "^1.3.9", "@types/diff": "^5.2.3", "@types/node": "^22.0.0", "@types/tar-stream": "^3.1.4", @@ -151,6 +152,8 @@ "@sveltejs/acorn-typescript": ["@sveltejs/acorn-typescript@1.0.9", "", { "peerDependencies": { "acorn": "^8.9.0" } }, "sha512-lVJX6qEgs/4DOcRTpo56tmKzVPtoWAaVbL4hfO7t7NVwl9AAXzQR6cihesW1BmNMPl+bK6dreu2sOKBP2Q9CIA=="], + "@types/bun": ["@types/bun@1.3.9", "", { "dependencies": { "bun-types": "1.3.9" } }, "sha512-KQ571yULOdWJiMH+RIWIOZ7B2RXQGpL1YQrBtLIV3FqDcCu6FsbFUBwhdKUlCKUpS3PJDsHlJ1QKlpxoVR+xtw=="], + "@types/diff": ["@types/diff@5.2.3", "", {}, "sha512-K0Oqlrq3kQMaO2RhfrNQX5trmt+XLyom88zS0u84nnIcLvFnRUMRRHmrGny5GSM+kNO9IZLARsdQHDzkhAgmrQ=="], "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], @@ -183,6 +186,8 @@ "brace-expansion": ["brace-expansion@5.0.2", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw=="], + "bun-types": ["bun-types@1.3.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-+UBWWOakIP4Tswh0Bt0QD0alpTY8cb5hvgiYeWCMet9YukHbzuruIEeXC2D7nMJPB12kbh8C7XJykSexEqGKJg=="], + "bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="], "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], diff --git a/cli/package.json b/cli/package.json index a07d43a1a4..6102f815f2 100644 --- a/cli/package.json +++ b/cli/package.json @@ -42,6 +42,7 @@ "yaml": "^2.7.0" }, "devDependencies": { + "@types/bun": "^1.3.9", "@types/diff": "^5.2.3", "@types/node": "^22.0.0", "@types/tar-stream": "^3.1.4", diff --git a/cli/src/commands/folder/folder.ts b/cli/src/commands/folder/folder.ts index e3967d8ce3..eb5a51c4f8 100644 --- a/cli/src/commands/folder/folder.ts +++ b/cli/src/commands/folder/folder.ts @@ -1,4 +1,4 @@ -import { stat, writeFile, mkdir } from "node:fs/promises"; +import { stat, readdir, writeFile, mkdir } from "node:fs/promises"; import { stringify as yamlStringify } from "yaml"; import { colors } from "@cliffy/ansi/colors"; @@ -6,17 +6,19 @@ import { Command } from "@cliffy/command"; import { Table } from "@cliffy/table"; import * as log from "../../core/log.ts"; import { sep as SEP } from "node:path"; +import { Confirm } from "@cliffy/prompt/confirm"; import * as wmill from "../../../gen/services.gen.ts"; import { requireLogin } from "../../core/auth.ts"; -import { resolveWorkspace, validatePath } from "../../core/context.ts"; +import { resolveWorkspace } from "../../core/context.ts"; import { GlobalOptions, isSuperset, parseFromFile } from "../../types.ts"; import { Folder } from "../../../gen/types.gen.ts"; export interface FolderFile { + summary: string | undefined; + display_name: string | undefined; owners: Array | undefined; extra_perms: { [record: string]: boolean } | undefined; - display_name: string | undefined; } async function list(opts: GlobalOptions & { json?: boolean }) { @@ -45,7 +47,7 @@ async function list(opts: GlobalOptions & { json?: boolean }) { } } -async function newFolder(opts: GlobalOptions, name: string) { +async function newFolder(opts: GlobalOptions & { summary?: string }, name: string) { const dirPath = `f${SEP}${name}`; const filePath = `${dirPath}${SEP}folder.meta.yaml`; try { @@ -54,7 +56,9 @@ async function newFolder(opts: GlobalOptions, name: string) { } catch (e: any) { if (e.message?.startsWith("File already exists")) throw e; } - const template: Omit = { + const template: FolderFile = { + summary: opts.summary ?? "", + display_name: name, owners: [], extra_perms: {}, }; @@ -143,30 +147,72 @@ export async function pushFolder( } } -async function push(opts: GlobalOptions, filePath: string, remotePath: string) { +async function push(opts: GlobalOptions, name: string) { const workspace = await resolveWorkspace(opts); await requireLogin(opts); - if (!validatePath(remotePath)) { - return; - } - - const fstat = await stat(filePath); - if (!fstat.isFile()) { - throw new Error("file path must refer to a file."); + const metaPath = `f${SEP}${name}${SEP}folder.meta.yaml`; + try { + await stat(metaPath); + } catch { + throw new Error(`Could not find ${metaPath}. Does the folder exist locally?`); } console.log(colors.bold.yellow("Pushing folder...")); await pushFolder( workspace.workspaceId, - remotePath, + name, undefined, - parseFromFile(filePath) + parseFromFile(metaPath) ); console.log(colors.bold.underline.green("Folder pushed")); } +async function addMissing(opts: GlobalOptions & { yes?: boolean }) { + const fDir = `f`; + try { + await stat(fDir); + } catch { + log.info("No 'f/' directory found. Nothing to do."); + return; + } + const entries = await readdir(fDir, { withFileTypes: true }); + const missing: string[] = []; + for (const entry of entries) { + if (!entry.isDirectory()) continue; + const metaPath = `${fDir}${SEP}${entry.name}${SEP}folder.meta.yaml`; + try { + await stat(metaPath); + } catch { + missing.push(entry.name); + } + } + if (missing.length === 0) { + log.info("All folders already have a folder.meta.yaml. Nothing to do."); + return; + } + log.info(`Missing folder.meta.yaml for:`); + for (const name of missing) { + log.info(` - ${name}`); + } + if ( + !opts.yes && + !(await Confirm.prompt({ + message: `Create ${missing.length} folder.meta.yaml file(s)?`, + default: true, + })) + ) { + return; + } + for (const name of missing) { + await newFolder(opts, name); + } + log.info( + `\nCreated ${missing.length} folder.meta.yaml file(s). You can now run 'wmill sync push' to push them.`, + ); +} + const command = new Command() .description("folder related commands") .option("--json", "Output as JSON (for piping to jq)") @@ -180,12 +226,19 @@ const command = new Command() .action(get as any) .command("new", "create a new folder locally") .arguments("") + .option("--summary ", "folder summary") .action(newFolder as any) .command( "push", - "push a local folder spec. This overrides any remote versions." + "push a local folder to the remote by name. This overrides any remote versions." ) - .arguments(" ") - .action(push as any); + .arguments("") + .action(push as any) + .command( + "add-missing", + "create default folder.meta.yaml for all subdirectories of f/ that are missing one" + ) + .option("-y, --yes", "skip confirmation prompt") + .action(addMissing as any); export default command; diff --git a/cli/src/commands/sync/sync.ts b/cli/src/commands/sync/sync.ts index 0f6d90c182..99a07d1f5d 100644 --- a/cli/src/commands/sync/sync.ts +++ b/cli/src/commands/sync/sync.ts @@ -2480,6 +2480,45 @@ export async function push( log.info( `remote (${workspace.name}) <- local: ${changes.length} changes to apply`, ); + // Check that every folder referenced in the changeset has a local folder.meta.yaml + const missingFolders: string[] = []; + if (changes.length > 0) { + const folderNames = new Set(); + for (const change of changes) { + const parts = change.path.split(SEP); + if (parts.length >= 3 && parts[0] === "f" && change.name !== "deleted") { + folderNames.add(parts[1]); + } + } + for (const folderName of folderNames) { + try { + await stat(path.join("f", folderName, "folder.meta.yaml")); + } catch { + missingFolders.push(folderName); + } + } + } + + if (missingFolders.length > 0) { + const folderList = missingFolders.map((f) => ` - ${f}`).join("\n"); + const user = await wmill.whoami({ workspace: workspace.workspaceId }); + const userIsAdmin = user.is_admin; + const msg = + `${userIsAdmin ? "Warning: " : ""}Missing folder.meta.yaml for:\n${folderList}\n` + + `Run 'wmill folder add-missing' to create them locally, then push again.`; + if (!userIsAdmin) { + if (opts.jsonOutput) { + console.log(JSON.stringify({ success: false, error: "missing_folders", missing_folders: missingFolders, message: msg }, null, 2)); + } else { + log.error(msg); + } + process.exit(1); + } + if (!opts.jsonOutput) { + log.warn(msg); + } + } + // Handle JSON output for dry-run if (opts.dryRun && opts.jsonOutput) { const result = { @@ -2511,6 +2550,7 @@ export async function push( if (!opts.jsonOutput) { prettyChanges(changes, specificItems, opts.branch); } + if (opts.dryRun) { log.info(colors.gray(`Dry run complete.`)); return; diff --git a/cli/src/guidance/skills.ts b/cli/src/guidance/skills.ts index 88f8cbb842..4a16eb2f32 100644 --- a/cli/src/guidance/skills.ts +++ b/cli/src/guidance/skills.ts @@ -3945,7 +3945,7 @@ Reference a specific resource using \`$res:\` prefix: ## OpenFlow Schema -{"OpenFlow":{"type":"object","description":"Top-level flow definition containing metadata, configuration, and the flow structure","properties":{"summary":{"type":"string","description":"Short description of what this flow does"},"description":{"type":"string","description":"Detailed documentation for this flow"},"value":{"$ref":"#/components/schemas/FlowValue"},"schema":{"type":"object","description":"JSON Schema for flow inputs. Use this to define input parameters, their types, defaults, and validation. For resource inputs, set type to 'object' and format to 'resource-' (e.g., 'resource-stripe')"}},"required":["summary","value"]},"FlowValue":{"type":"object","description":"The flow structure containing modules and optional preprocessor/failure handlers","properties":{"modules":{"type":"array","description":"Array of steps that execute in sequence. Each step can be a script, subflow, loop, or branch","items":{"$ref":"#/components/schemas/FlowModule"}},"failure_module":{"description":"Special module that executes when the flow fails. Receives error object with message, name, stack, and step_id. Must have id 'failure'. Only supports script/rawscript types","$ref":"#/components/schemas/FlowModule"},"preprocessor_module":{"description":"Special module that runs before the first step on external triggers. Must have id 'preprocessor'. Only supports script/rawscript types. Cannot reference other step results","$ref":"#/components/schemas/FlowModule"},"same_worker":{"type":"boolean","description":"If true, all steps run on the same worker for better performance"},"concurrent_limit":{"type":"number","description":"Maximum number of concurrent executions of this flow"},"concurrency_key":{"type":"string","description":"Expression to group concurrent executions (e.g., by user ID)"},"concurrency_time_window_s":{"type":"number","description":"Time window in seconds for concurrent_limit"},"debounce_delay_s":{"type":"number","description":"Delay in seconds to debounce flow executions"},"debounce_key":{"type":"string","description":"Expression to group debounced executions"},"debounce_args_to_accumulate":{"type":"array","description":"Arguments to accumulate across debounced executions","items":{"type":"string"}},"max_total_debouncing_time":{"type":"number","description":"Maximum total time in seconds that a job can be debounced"},"max_total_debounces_amount":{"type":"number","description":"Maximum number of times a job can be debounced"},"skip_expr":{"type":"string","description":"JavaScript expression to conditionally skip the entire flow"},"cache_ttl":{"type":"number","description":"Cache duration in seconds for flow results"},"cache_ignore_s3_path":{"type":"boolean"},"flow_env":{"type":"object","description":"Environment variables available to all steps","additionalProperties":{"type":"string"}},"priority":{"type":"number","description":"Execution priority (higher numbers run first)"},"early_return":{"type":"string","description":"JavaScript expression to return early from the flow"},"chat_input_enabled":{"type":"boolean","description":"Whether this flow accepts chat-style input"},"notes":{"type":"array","description":"Sticky notes attached to the flow","items":{"$ref":"#/components/schemas/FlowNote"}}},"required":["modules"]},"Retry":{"type":"object","description":"Retry configuration for failed module executions","properties":{"constant":{"type":"object","description":"Retry with constant delay between attempts","properties":{"attempts":{"type":"integer","description":"Number of retry attempts"},"seconds":{"type":"integer","description":"Seconds to wait between retries"}}},"exponential":{"type":"object","description":"Retry with exponential backoff (delay doubles each time)","properties":{"attempts":{"type":"integer","description":"Number of retry attempts"},"multiplier":{"type":"integer","description":"Multiplier for exponential backoff"},"seconds":{"type":"integer","minimum":1,"description":"Initial delay in seconds"},"random_factor":{"type":"integer","minimum":0,"maximum":100,"description":"Random jitter percentage (0-100) to avoid thundering herd"}}},"retry_if":{"$ref":"#/components/schemas/RetryIf"}}},"FlowNote":{"type":"object","description":"A sticky note attached to a flow for documentation and annotation","properties":{"id":{"type":"string","description":"Unique identifier for the note"},"text":{"type":"string","description":"Content of the note"},"position":{"type":"object","description":"Position of the note in the flow editor","properties":{"x":{"type":"number","description":"X coordinate"},"y":{"type":"number","description":"Y coordinate"}},"required":["x","y"]},"size":{"type":"object","description":"Size of the note in the flow editor","properties":{"width":{"type":"number","description":"Width in pixels"},"height":{"type":"number","description":"Height in pixels"}},"required":["width","height"]},"color":{"type":"string","description":"Color of the note (e.g., \\"yellow\\", \\"#ffff00\\")"},"type":{"type":"string","enum":["free","group"],"description":"Type of note - 'free' for standalone notes, 'group' for notes that group other nodes"},"locked":{"type":"boolean","default":false,"description":"Whether the note is locked and cannot be edited or moved"},"contained_node_ids":{"type":"array","items":{"type":"string"},"description":"For group notes, the IDs of nodes contained within this group"}},"required":["id","text","color","type"]},"RetryIf":{"type":"object","description":"Conditional retry based on error or result","properties":{"expr":{"type":"string","description":"JavaScript expression that returns true to retry. Has access to 'result' and 'error' variables"}},"required":["expr"]},"StopAfterIf":{"type":"object","description":"Early termination condition for a module","properties":{"skip_if_stopped":{"type":"boolean","description":"If true, following steps are skipped when this condition triggers"},"expr":{"type":"string","description":"JavaScript expression evaluated after the module runs. Can use 'result' (step's result) or 'flow_input'. Return true to stop"},"error_message":{"type":"string","description":"Custom error message shown when stopping"}},"required":["expr"]},"FlowModule":{"type":"object","description":"A single step in a flow. Can be a script, subflow, loop, or branch","properties":{"id":{"type":"string","description":"Unique identifier for this step. Used to reference results via 'results.step_id'. Must be a valid identifier (alphanumeric, underscore, hyphen)"},"value":{"$ref":"#/components/schemas/FlowModuleValue"},"stop_after_if":{"description":"Early termination condition evaluated after this step completes","$ref":"#/components/schemas/StopAfterIf"},"stop_after_all_iters_if":{"description":"For loops only - early termination condition evaluated after all iterations complete","$ref":"#/components/schemas/StopAfterIf"},"skip_if":{"type":"object","description":"Conditionally skip this step based on previous results or flow inputs","properties":{"expr":{"type":"string","description":"JavaScript expression that returns true to skip. Can use 'flow_input' or 'results.'"}},"required":["expr"]},"sleep":{"description":"Delay before executing this step (in seconds or as expression)","$ref":"#/components/schemas/InputTransform"},"cache_ttl":{"type":"number","description":"Cache duration in seconds for this step's results"},"cache_ignore_s3_path":{"type":"boolean"},"timeout":{"description":"Maximum execution time in seconds (static value or expression)","$ref":"#/components/schemas/InputTransform"},"delete_after_use":{"type":"boolean","description":"If true, this step's result is deleted after use to save memory"},"summary":{"type":"string","description":"Short description of what this step does"},"mock":{"type":"object","description":"Mock configuration for testing without executing the actual step","properties":{"enabled":{"type":"boolean","description":"If true, return mock value instead of executing"},"return_value":{"description":"Value to return when mocked"}}},"suspend":{"type":"object","description":"Configuration for approval/resume steps that wait for user input","properties":{"required_events":{"type":"integer","description":"Number of approvals required before continuing"},"timeout":{"type":"integer","description":"Timeout in seconds before auto-continuing or canceling"},"resume_form":{"type":"object","description":"Form schema for collecting input when resuming","properties":{"schema":{"type":"object","description":"JSON Schema for the resume form"}}},"user_auth_required":{"type":"boolean","description":"If true, only authenticated users can approve"},"user_groups_required":{"description":"Expression or list of groups that can approve","$ref":"#/components/schemas/InputTransform"},"self_approval_disabled":{"type":"boolean","description":"If true, the user who started the flow cannot approve"},"hide_cancel":{"type":"boolean","description":"If true, hide the cancel button on the approval form"},"continue_on_disapprove_timeout":{"type":"boolean","description":"If true, continue flow on timeout instead of canceling"}}},"priority":{"type":"number","description":"Execution priority for this step (higher numbers run first)"},"continue_on_error":{"type":"boolean","description":"If true, flow continues even if this step fails"},"retry":{"description":"Retry configuration if this step fails","$ref":"#/components/schemas/Retry"}},"required":["value","id"]},"InputTransform":{"description":"Maps input parameters for a step. Can be a static value or a JavaScript expression that references previous results or flow inputs","oneOf":[{"$ref":"#/components/schemas/StaticTransform"},{"$ref":"#/components/schemas/JavascriptTransform"},{"$ref":"#/components/schemas/AiTransform"}],"discriminator":{"propertyName":"type","mapping":{"static":"#/components/schemas/StaticTransform","javascript":"#/components/schemas/JavascriptTransform","ai":"#/components/schemas/AiTransform"}}},"StaticTransform":{"type":"object","description":"Static value passed directly to the step. Use for hardcoded values or resource references like '$res:path/to/resource'","properties":{"value":{"description":"The static value. For resources, use format '$res:path/to/resource'"},"type":{"type":"string","enum":["static"]}},"required":["type"]},"JavascriptTransform":{"type":"object","description":"JavaScript expression evaluated at runtime. Can reference previous step results via 'results.step_id' or flow inputs via 'flow_input.property'. Inside loops, use 'flow_input.iter.value' for the current iteration value","properties":{"expr":{"type":"string","description":"JavaScript expression returning the value. Available variables - results (object with all previous step results), flow_input (flow inputs), flow_input.iter (in loops)"},"type":{"type":"string","enum":["javascript"]}},"required":["expr","type"]},"AiTransform":{"type":"object","description":"Value resolved by the AI runtime for this input. The AI engine decides how to satisfy the parameter.","properties":{"type":{"type":"string","enum":["ai"]}},"required":["type"]},"AIProviderKind":{"type":"string","description":"Supported AI provider types","enum":["openai","azure_openai","anthropic","mistral","deepseek","googleai","groq","openrouter","togetherai","customai","aws_bedrock"]},"ProviderConfig":{"type":"object","description":"Complete AI provider configuration with resource reference and model selection","properties":{"kind":{"$ref":"#/components/schemas/AIProviderKind"},"resource":{"type":"string","description":"Resource reference in format '$res:{resource_path}' pointing to provider credentials"},"model":{"type":"string","description":"Model identifier (e.g., 'gpt-4', 'claude-3-opus-20240229', 'gemini-pro')"}},"required":["kind","resource","model"]},"StaticProviderTransform":{"type":"object","description":"Static provider configuration passed directly to the AI agent","properties":{"value":{"$ref":"#/components/schemas/ProviderConfig"},"type":{"type":"string","enum":["static"]}},"required":["type","value"]},"ProviderTransform":{"description":"Provider configuration - can be static (ProviderConfig), JavaScript expression, or AI-determined","oneOf":[{"$ref":"#/components/schemas/StaticProviderTransform"},{"$ref":"#/components/schemas/JavascriptTransform"},{"$ref":"#/components/schemas/AiTransform"}],"discriminator":{"propertyName":"type","mapping":{"static":"#/components/schemas/StaticProviderTransform","javascript":"#/components/schemas/JavascriptTransform","ai":"#/components/schemas/AiTransform"}}},"MemoryOff":{"type":"object","description":"No conversation memory/context","properties":{"kind":{"type":"string","enum":["off"]}},"required":["kind"]},"MemoryAuto":{"type":"object","description":"Automatic context management","properties":{"kind":{"type":"string","enum":["auto"]},"context_length":{"type":"integer","description":"Maximum number of messages to retain in context"},"memory_id":{"type":"string","description":"Identifier for persistent memory across agent invocations"}},"required":["kind"]},"MemoryMessage":{"type":"object","description":"A single message in conversation history","properties":{"role":{"type":"string","enum":["user","assistant","system"]},"content":{"type":"string"}},"required":["role","content"]},"MemoryManual":{"type":"object","description":"Explicit message history","properties":{"kind":{"type":"string","enum":["manual"]},"messages":{"type":"array","items":{"$ref":"#/components/schemas/MemoryMessage"}}},"required":["kind","messages"]},"MemoryConfig":{"description":"Conversation memory configuration","oneOf":[{"$ref":"#/components/schemas/MemoryOff"},{"$ref":"#/components/schemas/MemoryAuto"},{"$ref":"#/components/schemas/MemoryManual"}],"discriminator":{"propertyName":"kind","mapping":{"off":"#/components/schemas/MemoryOff","auto":"#/components/schemas/MemoryAuto","manual":"#/components/schemas/MemoryManual"}}},"StaticMemoryTransform":{"type":"object","description":"Static memory configuration passed directly to the AI agent","properties":{"value":{"$ref":"#/components/schemas/MemoryConfig"},"type":{"type":"string","enum":["static"]}},"required":["type","value"]},"MemoryTransform":{"description":"Memory configuration - can be static (MemoryConfig), JavaScript expression, or AI-determined","oneOf":[{"$ref":"#/components/schemas/StaticMemoryTransform"},{"$ref":"#/components/schemas/JavascriptTransform"},{"$ref":"#/components/schemas/AiTransform"}],"discriminator":{"propertyName":"type","mapping":{"static":"#/components/schemas/StaticMemoryTransform","javascript":"#/components/schemas/JavascriptTransform","ai":"#/components/schemas/AiTransform"}}},"FlowModuleValue":{"description":"The actual implementation of a flow step. Can be a script (inline or referenced), subflow, loop, branch, or special module type","oneOf":[{"$ref":"#/components/schemas/RawScript"},{"$ref":"#/components/schemas/PathScript"},{"$ref":"#/components/schemas/PathFlow"},{"$ref":"#/components/schemas/ForloopFlow"},{"$ref":"#/components/schemas/WhileloopFlow"},{"$ref":"#/components/schemas/BranchOne"},{"$ref":"#/components/schemas/BranchAll"},{"$ref":"#/components/schemas/Identity"},{"$ref":"#/components/schemas/AiAgent"}],"discriminator":{"propertyName":"type","mapping":{"rawscript":"#/components/schemas/RawScript","script":"#/components/schemas/PathScript","flow":"#/components/schemas/PathFlow","forloopflow":"#/components/schemas/ForloopFlow","whileloopflow":"#/components/schemas/WhileloopFlow","branchone":"#/components/schemas/BranchOne","branchall":"#/components/schemas/BranchAll","identity":"#/components/schemas/Identity","aiagent":"#/components/schemas/AiAgent"}}},"RawScript":{"type":"object","description":"Inline script with code defined directly in the flow. Use 'bun' as default language if unspecified. The script receives arguments from input_transforms","properties":{"input_transforms":{"type":"object","description":"Map of parameter names to their values (static or JavaScript expressions). These become the script's input arguments","additionalProperties":{"$ref":"#/components/schemas/InputTransform"}},"content":{"type":"string","description":"The script source code. Should export a 'main' function"},"language":{"type":"string","description":"Programming language for this script","enum":["deno","bun","python3","go","bash","powershell","postgresql","mysql","bigquery","snowflake","mssql","oracledb","graphql","nativets","php"]},"path":{"type":"string","description":"Optional path for saving this script"},"lock":{"type":"string","description":"Lock file content for dependencies"},"type":{"type":"string","enum":["rawscript"]},"tag":{"type":"string","description":"Worker group tag for execution routing"},"concurrent_limit":{"type":"number","description":"Maximum concurrent executions of this script"},"concurrency_time_window_s":{"type":"number","description":"Time window for concurrent_limit"},"custom_concurrency_key":{"type":"string","description":"Custom key for grouping concurrent executions"},"is_trigger":{"type":"boolean","description":"If true, this script is a trigger that can start the flow"},"assets":{"type":"array","description":"External resources this script accesses (S3 objects, resources, etc.)","items":{"type":"object","required":["path","kind"],"properties":{"path":{"type":"string","description":"Path to the asset"},"kind":{"type":"string","description":"Type of asset","enum":["s3object","resource","ducklake","datatable"]},"access_type":{"type":"string","description":"Access level for this asset","enum":["r","w","rw"]},"alt_access_type":{"type":"string","description":"Alternative access level","enum":["r","w","rw"]}}}}},"required":["type","content","language","input_transforms"]},"PathScript":{"type":"object","description":"Reference to an existing script by path. Use this when calling a previously saved script instead of writing inline code","properties":{"input_transforms":{"type":"object","description":"Map of parameter names to their values (static or JavaScript expressions). These become the script's input arguments","additionalProperties":{"$ref":"#/components/schemas/InputTransform"}},"path":{"type":"string","description":"Path to the script in the workspace (e.g., 'f/scripts/send_email')"},"hash":{"type":"string","description":"Optional specific version hash of the script to use"},"type":{"type":"string","enum":["script"]},"tag_override":{"type":"string","description":"Override the script's default worker group tag"},"is_trigger":{"type":"boolean","description":"If true, this script is a trigger that can start the flow"}},"required":["type","path","input_transforms"]},"PathFlow":{"type":"object","description":"Reference to an existing flow by path. Use this to call another flow as a subflow","properties":{"input_transforms":{"type":"object","description":"Map of parameter names to their values (static or JavaScript expressions). These become the subflow's input arguments","additionalProperties":{"$ref":"#/components/schemas/InputTransform"}},"path":{"type":"string","description":"Path to the flow in the workspace (e.g., 'f/flows/process_user')"},"type":{"type":"string","enum":["flow"]}},"required":["type","path","input_transforms"]},"ForloopFlow":{"type":"object","description":"Executes nested modules in a loop over an iterator. Inside the loop, use 'flow_input.iter.value' to access the current iteration value, and 'flow_input.iter.index' for the index. Supports parallel execution for better performance on I/O-bound operations","properties":{"modules":{"type":"array","description":"Steps to execute for each iteration. These can reference the iteration value via 'flow_input.iter.value'","items":{"$ref":"#/components/schemas/FlowModule"}},"iterator":{"description":"JavaScript expression that returns an array to iterate over. Can reference 'results.step_id' or 'flow_input'","$ref":"#/components/schemas/InputTransform"},"skip_failures":{"type":"boolean","description":"If true, iteration failures don't stop the loop. Failed iterations return null"},"type":{"type":"string","enum":["forloopflow"]},"parallel":{"type":"boolean","description":"If true, iterations run concurrently (faster for I/O-bound operations). Use with parallelism to control concurrency"},"parallelism":{"description":"Maximum number of concurrent iterations when parallel=true. Limits resource usage. Can be static number or expression","$ref":"#/components/schemas/InputTransform"},"squash":{"type":"boolean"}},"required":["modules","iterator","skip_failures","type"]},"WhileloopFlow":{"type":"object","description":"Executes nested modules repeatedly while a condition is true. The loop checks the condition after each iteration. Use stop_after_if on modules to control loop termination","properties":{"modules":{"type":"array","description":"Steps to execute in each iteration. Use stop_after_if to control when the loop ends","items":{"$ref":"#/components/schemas/FlowModule"}},"skip_failures":{"type":"boolean","description":"If true, iteration failures don't stop the loop. Failed iterations return null"},"type":{"type":"string","enum":["whileloopflow"]},"parallel":{"type":"boolean","description":"If true, iterations run concurrently (use with caution in while loops)"},"parallelism":{"description":"Maximum number of concurrent iterations when parallel=true","$ref":"#/components/schemas/InputTransform"},"squash":{"type":"boolean"}},"required":["modules","skip_failures","type"]},"BranchOne":{"type":"object","description":"Conditional branching where only the first matching branch executes. Branches are evaluated in order, and the first one with a true expression runs. If no branches match, the default branch executes","properties":{"branches":{"type":"array","description":"Array of branches to evaluate in order. The first branch with expr evaluating to true executes","items":{"type":"object","properties":{"summary":{"type":"string","description":"Short description of this branch condition"},"expr":{"type":"string","description":"JavaScript expression that returns boolean. Can use 'results.step_id' or 'flow_input'. First true expr wins"},"modules":{"type":"array","description":"Steps to execute if this branch's expr is true","items":{"$ref":"#/components/schemas/FlowModule"}}},"required":["modules","expr"]}},"default":{"type":"array","description":"Steps to execute if no branch expressions match","items":{"$ref":"#/components/schemas/FlowModule"}},"type":{"type":"string","enum":["branchone"]}},"required":["branches","default","type"]},"BranchAll":{"type":"object","description":"Parallel branching where all branches execute simultaneously. Unlike BranchOne, all branches run regardless of conditions. Useful for executing independent tasks concurrently","properties":{"branches":{"type":"array","description":"Array of branches that all execute (either in parallel or sequentially)","items":{"type":"object","properties":{"summary":{"type":"string","description":"Short description of this branch's purpose"},"skip_failure":{"type":"boolean","description":"If true, failure in this branch doesn't fail the entire flow"},"modules":{"type":"array","description":"Steps to execute in this branch","items":{"$ref":"#/components/schemas/FlowModule"}}},"required":["modules"]}},"type":{"type":"string","enum":["branchall"]},"parallel":{"type":"boolean","description":"If true, all branches execute concurrently. If false, they execute sequentially"}},"required":["branches","type"]},"AgentTool":{"type":"object","description":"A tool available to an AI agent. Can be a flow module or an external MCP (Model Context Protocol) tool","properties":{"id":{"type":"string","description":"Unique identifier for this tool. Cannot contain spaces - use underscores instead (e.g., 'get_user_data' not 'get user data')"},"summary":{"type":"string","description":"Short description of what this tool does (shown to the AI)"},"value":{"$ref":"#/components/schemas/ToolValue"}},"required":["id","value"]},"ToolValue":{"description":"The implementation of a tool. Can be a flow module (script/flow) or an MCP tool reference","oneOf":[{"$ref":"#/components/schemas/FlowModuleTool"},{"$ref":"#/components/schemas/McpToolValue"},{"$ref":"#/components/schemas/WebsearchToolValue"}],"discriminator":{"propertyName":"tool_type","mapping":{"flowmodule":"#/components/schemas/FlowModuleTool","mcp":"#/components/schemas/McpToolValue","websearch":"#/components/schemas/WebsearchToolValue"}}},"FlowModuleTool":{"description":"A tool implemented as a flow module (script, flow, etc.). The AI can call this like any other flow module","allOf":[{"type":"object","properties":{"tool_type":{"type":"string","enum":["flowmodule"]}},"required":["tool_type"]},{"$ref":"#/components/schemas/FlowModuleValue"}]},"WebsearchToolValue":{"type":"object","description":"A tool implemented as a websearch tool. The AI can call this like any other websearch tool","properties":{"tool_type":{"type":"string","enum":["websearch"]}},"required":["tool_type"]},"McpToolValue":{"type":"object","description":"Reference to an external MCP (Model Context Protocol) tool. The AI can call tools from MCP servers","properties":{"tool_type":{"type":"string","enum":["mcp"]},"resource_path":{"type":"string","description":"Path to the MCP resource/server configuration"},"include_tools":{"type":"array","description":"Whitelist of specific tools to include from this MCP server","items":{"type":"string"}},"exclude_tools":{"type":"array","description":"Blacklist of tools to exclude from this MCP server","items":{"type":"string"}}},"required":["tool_type","resource_path"]},"AiAgent":{"type":"object","description":"AI agent step that can use tools to accomplish tasks. The agent receives inputs and can call any of its configured tools to complete the task","properties":{"input_transforms":{"type":"object","description":"Input parameters for the AI agent mapped to their values","properties":{"provider":{"$ref":"#/components/schemas/ProviderTransform"},"output_type":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"Output format type.\\nValid values: 'text' (default) - plain text response, 'image' - image generation\\n"},"user_message":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"The user's prompt/message to the AI agent. Supports variable interpolation with flow.input syntax."},"system_prompt":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"System instructions that guide the AI's behavior, persona, and response style. Optional."},"streaming":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"Boolean. If true, stream the AI response incrementally.\\nStreaming events include: token_delta, tool_call, tool_call_arguments, tool_execution, tool_result\\n"},"memory":{"$ref":"#/components/schemas/MemoryTransform"},"output_schema":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"JSON Schema object defining structured output format. Used when you need the AI to return data in a specific shape.\\nSupports standard JSON Schema properties: type, properties, required, items, enum, pattern, minLength, maxLength, minimum, maximum, etc.\\nExample: { type: 'object', properties: { name: { type: 'string' }, age: { type: 'integer' } }, required: ['name'] }\\n"},"user_images":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"Array of image references for vision-capable models.\\nFormat: Array<{ bucket: string, key: string }> - S3 object references\\nExample: [{ bucket: 'my-bucket', key: 'images/photo.jpg' }]\\n"},"max_completion_tokens":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"Integer. Maximum number of tokens the AI will generate in its response.\\nRange: 1 to 4,294,967,295. Typical values: 256-4096 for most use cases.\\n"},"temperature":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"Float. Controls randomness/creativity of responses.\\nRange: 0.0 to 2.0 (provider-dependent)\\n- 0.0 = deterministic, focused responses\\n- 0.7 = balanced (common default)\\n- 1.0+ = more creative/random\\n"}},"required":["provider","user_message","output_type"]},"tools":{"type":"array","description":"Array of tools the agent can use. The agent decides which tools to call based on the task","items":{"$ref":"#/components/schemas/AgentTool"}},"type":{"type":"string","enum":["aiagent"]},"parallel":{"type":"boolean","description":"If true, the agent can execute multiple tool calls in parallel"}},"required":["tools","type","input_transforms"]},"Identity":{"type":"object","description":"Pass-through module that returns its input unchanged. Useful for flow structure or as a placeholder","properties":{"type":{"type":"string","enum":["identity"]},"flow":{"type":"boolean","description":"If true, marks this as a flow identity (special handling)"}},"required":["type"]},"FlowStatus":{"type":"object","properties":{"step":{"type":"integer"},"modules":{"type":"array","items":{"$ref":"#/components/schemas/FlowStatusModule"}},"user_states":{"additionalProperties":true},"preprocessor_module":{"allOf":[{"$ref":"#/components/schemas/FlowStatusModule"}]},"failure_module":{"allOf":[{"$ref":"#/components/schemas/FlowStatusModule"},{"type":"object","properties":{"parent_module":{"type":"string"}}}]},"retry":{"type":"object","properties":{"fail_count":{"type":"integer"},"failed_jobs":{"type":"array","items":{"type":"string","format":"uuid"}}}}},"required":["step","modules","failure_module"]},"FlowStatusModule":{"type":"object","properties":{"type":{"type":"string","enum":["WaitingForPriorSteps","WaitingForEvents","WaitingForExecutor","InProgress","Success","Failure"]},"id":{"type":"string"},"job":{"type":"string","format":"uuid"},"count":{"type":"integer"},"progress":{"type":"integer"},"iterator":{"type":"object","properties":{"index":{"type":"integer"},"itered":{"type":"array","items":{}},"itered_len":{"type":"integer"},"args":{}}},"flow_jobs":{"type":"array","items":{"type":"string"}},"flow_jobs_success":{"type":"array","items":{"type":"boolean"}},"flow_jobs_duration":{"type":"object","properties":{"started_at":{"type":"array","items":{"type":"string"}},"duration_ms":{"type":"array","items":{"type":"integer"}}}},"branch_chosen":{"type":"object","properties":{"type":{"type":"string","enum":["branch","default"]},"branch":{"type":"integer"}},"required":["type"]},"branchall":{"type":"object","properties":{"branch":{"type":"integer"},"len":{"type":"integer"}},"required":["branch","len"]},"approvers":{"type":"array","items":{"type":"object","properties":{"resume_id":{"type":"integer"},"approver":{"type":"string"}},"required":["resume_id","approver"]}},"failed_retries":{"type":"array","items":{"type":"string","format":"uuid"}},"skipped":{"type":"boolean"},"agent_actions":{"type":"array","items":{"type":"object","oneOf":[{"type":"object","properties":{"job_id":{"type":"string","format":"uuid"},"function_name":{"type":"string"},"type":{"type":"string","enum":["tool_call"]},"module_id":{"type":"string"}},"required":["job_id","function_name","type","module_id"]},{"type":"object","properties":{"call_id":{"type":"string","format":"uuid"},"function_name":{"type":"string"},"resource_path":{"type":"string"},"type":{"type":"string","enum":["mcp_tool_call"]},"arguments":{"type":"object"}},"required":["call_id","function_name","resource_path","type"]},{"type":"object","properties":{"type":{"type":"string","enum":["web_search"]}},"required":["type"]},{"type":"object","properties":{"type":{"type":"string","enum":["message"]}},"required":["content","type"]}]}},"agent_actions_success":{"type":"array","items":{"type":"boolean"}}},"required":["type"]}}`, +{"OpenFlow":{"type":"object","description":"Top-level flow definition containing metadata, configuration, and the flow structure","properties":{"summary":{"type":"string","description":"Short description of what this flow does"},"description":{"type":"string","description":"Detailed documentation for this flow"},"value":{"$ref":"#/components/schemas/FlowValue"},"schema":{"type":"object","description":"JSON Schema for flow inputs. Use this to define input parameters, their types, defaults, and validation. For resource inputs, set type to 'object' and format to 'resource-' (e.g., 'resource-stripe')"},"on_behalf_of_email":{"type":"string","description":"The flow will be run with the permissions of the user with this email."}},"required":["summary","value"]},"FlowValue":{"type":"object","description":"The flow structure containing modules and optional preprocessor/failure handlers","properties":{"modules":{"type":"array","description":"Array of steps that execute in sequence. Each step can be a script, subflow, loop, or branch","items":{"$ref":"#/components/schemas/FlowModule"}},"failure_module":{"description":"Special module that executes when the flow fails. Receives error object with message, name, stack, and step_id. Must have id 'failure'. Only supports script/rawscript types","$ref":"#/components/schemas/FlowModule"},"preprocessor_module":{"description":"Special module that runs before the first step on external triggers. Must have id 'preprocessor'. Only supports script/rawscript types. Cannot reference other step results","$ref":"#/components/schemas/FlowModule"},"same_worker":{"type":"boolean","description":"If true, all steps run on the same worker for better performance"},"concurrent_limit":{"type":"number","description":"Maximum number of concurrent executions of this flow"},"concurrency_key":{"type":"string","description":"Expression to group concurrent executions (e.g., by user ID)"},"concurrency_time_window_s":{"type":"number","description":"Time window in seconds for concurrent_limit"},"debounce_delay_s":{"type":"number","description":"Delay in seconds to debounce flow executions"},"debounce_key":{"type":"string","description":"Expression to group debounced executions"},"debounce_args_to_accumulate":{"type":"array","description":"Arguments to accumulate across debounced executions","items":{"type":"string"}},"max_total_debouncing_time":{"type":"number","description":"Maximum total time in seconds that a job can be debounced"},"max_total_debounces_amount":{"type":"number","description":"Maximum number of times a job can be debounced"},"skip_expr":{"type":"string","description":"JavaScript expression to conditionally skip the entire flow"},"cache_ttl":{"type":"number","description":"Cache duration in seconds for flow results"},"cache_ignore_s3_path":{"type":"boolean"},"flow_env":{"type":"object","description":"Environment variables available to all steps","additionalProperties":{"type":"string"}},"priority":{"type":"number","description":"Execution priority (higher numbers run first)"},"early_return":{"type":"string","description":"JavaScript expression to return early from the flow"},"chat_input_enabled":{"type":"boolean","description":"Whether this flow accepts chat-style input"},"notes":{"type":"array","description":"Sticky notes attached to the flow","items":{"$ref":"#/components/schemas/FlowNote"}}},"required":["modules"]},"Retry":{"type":"object","description":"Retry configuration for failed module executions","properties":{"constant":{"type":"object","description":"Retry with constant delay between attempts","properties":{"attempts":{"type":"integer","description":"Number of retry attempts"},"seconds":{"type":"integer","description":"Seconds to wait between retries"}}},"exponential":{"type":"object","description":"Retry with exponential backoff (delay doubles each time)","properties":{"attempts":{"type":"integer","description":"Number of retry attempts"},"multiplier":{"type":"integer","description":"Multiplier for exponential backoff"},"seconds":{"type":"integer","minimum":1,"description":"Initial delay in seconds"},"random_factor":{"type":"integer","minimum":0,"maximum":100,"description":"Random jitter percentage (0-100) to avoid thundering herd"}}},"retry_if":{"$ref":"#/components/schemas/RetryIf"}}},"FlowNote":{"type":"object","description":"A sticky note attached to a flow for documentation and annotation","properties":{"id":{"type":"string","description":"Unique identifier for the note"},"text":{"type":"string","description":"Content of the note"},"position":{"type":"object","description":"Position of the note in the flow editor","properties":{"x":{"type":"number","description":"X coordinate"},"y":{"type":"number","description":"Y coordinate"}},"required":["x","y"]},"size":{"type":"object","description":"Size of the note in the flow editor","properties":{"width":{"type":"number","description":"Width in pixels"},"height":{"type":"number","description":"Height in pixels"}},"required":["width","height"]},"color":{"type":"string","description":"Color of the note (e.g., \\"yellow\\", \\"#ffff00\\")"},"type":{"type":"string","enum":["free","group"],"description":"Type of note - 'free' for standalone notes, 'group' for notes that group other nodes"},"locked":{"type":"boolean","default":false,"description":"Whether the note is locked and cannot be edited or moved"},"contained_node_ids":{"type":"array","items":{"type":"string"},"description":"For group notes, the IDs of nodes contained within this group"}},"required":["id","text","color","type"]},"RetryIf":{"type":"object","description":"Conditional retry based on error or result","properties":{"expr":{"type":"string","description":"JavaScript expression that returns true to retry. Has access to 'result' and 'error' variables"}},"required":["expr"]},"StopAfterIf":{"type":"object","description":"Early termination condition for a module","properties":{"skip_if_stopped":{"type":"boolean","description":"If true, following steps are skipped when this condition triggers"},"expr":{"type":"string","description":"JavaScript expression evaluated after the module runs. Can use 'result' (step's result) or 'flow_input'. Return true to stop"},"error_message":{"type":"string","description":"Custom error message shown when stopping"}},"required":["expr"]},"FlowModule":{"type":"object","description":"A single step in a flow. Can be a script, subflow, loop, or branch","properties":{"id":{"type":"string","description":"Unique identifier for this step. Used to reference results via 'results.step_id'. Must be a valid identifier (alphanumeric, underscore, hyphen)"},"value":{"$ref":"#/components/schemas/FlowModuleValue"},"stop_after_if":{"description":"Early termination condition evaluated after this step completes","$ref":"#/components/schemas/StopAfterIf"},"stop_after_all_iters_if":{"description":"For loops only - early termination condition evaluated after all iterations complete","$ref":"#/components/schemas/StopAfterIf"},"skip_if":{"type":"object","description":"Conditionally skip this step based on previous results or flow inputs","properties":{"expr":{"type":"string","description":"JavaScript expression that returns true to skip. Can use 'flow_input' or 'results.'"}},"required":["expr"]},"sleep":{"description":"Delay before executing this step (in seconds or as expression)","$ref":"#/components/schemas/InputTransform"},"cache_ttl":{"type":"number","description":"Cache duration in seconds for this step's results"},"cache_ignore_s3_path":{"type":"boolean"},"timeout":{"description":"Maximum execution time in seconds (static value or expression)","$ref":"#/components/schemas/InputTransform"},"delete_after_use":{"type":"boolean","description":"If true, this step's result is deleted after use to save memory"},"summary":{"type":"string","description":"Short description of what this step does"},"mock":{"type":"object","description":"Mock configuration for testing without executing the actual step","properties":{"enabled":{"type":"boolean","description":"If true, return mock value instead of executing"},"return_value":{"description":"Value to return when mocked"}}},"suspend":{"type":"object","description":"Configuration for approval/resume steps that wait for user input","properties":{"required_events":{"type":"integer","description":"Number of approvals required before continuing"},"timeout":{"type":"integer","description":"Timeout in seconds before auto-continuing or canceling"},"resume_form":{"type":"object","description":"Form schema for collecting input when resuming","properties":{"schema":{"type":"object","description":"JSON Schema for the resume form"}}},"user_auth_required":{"type":"boolean","description":"If true, only authenticated users can approve"},"user_groups_required":{"description":"Expression or list of groups that can approve","$ref":"#/components/schemas/InputTransform"},"self_approval_disabled":{"type":"boolean","description":"If true, the user who started the flow cannot approve"},"hide_cancel":{"type":"boolean","description":"If true, hide the cancel button on the approval form"},"continue_on_disapprove_timeout":{"type":"boolean","description":"If true, continue flow on timeout instead of canceling"}}},"priority":{"type":"number","description":"Execution priority for this step (higher numbers run first)"},"continue_on_error":{"type":"boolean","description":"If true, flow continues even if this step fails"},"retry":{"description":"Retry configuration if this step fails","$ref":"#/components/schemas/Retry"}},"required":["value","id"]},"InputTransform":{"description":"Maps input parameters for a step. Can be a static value or a JavaScript expression that references previous results or flow inputs","oneOf":[{"$ref":"#/components/schemas/StaticTransform"},{"$ref":"#/components/schemas/JavascriptTransform"},{"$ref":"#/components/schemas/AiTransform"}],"discriminator":{"propertyName":"type","mapping":{"static":"#/components/schemas/StaticTransform","javascript":"#/components/schemas/JavascriptTransform","ai":"#/components/schemas/AiTransform"}}},"StaticTransform":{"type":"object","description":"Static value passed directly to the step. Use for hardcoded values or resource references like '$res:path/to/resource'","properties":{"value":{"description":"The static value. For resources, use format '$res:path/to/resource'"},"type":{"type":"string","enum":["static"]}},"required":["type"]},"JavascriptTransform":{"type":"object","description":"JavaScript expression evaluated at runtime. Can reference previous step results via 'results.step_id' or flow inputs via 'flow_input.property'. Inside loops, use 'flow_input.iter.value' for the current iteration value","properties":{"expr":{"type":"string","description":"JavaScript expression returning the value. Available variables - results (object with all previous step results), flow_input (flow inputs), flow_input.iter (in loops)"},"type":{"type":"string","enum":["javascript"]}},"required":["expr","type"]},"AiTransform":{"type":"object","description":"Value resolved by the AI runtime for this input. The AI engine decides how to satisfy the parameter.","properties":{"type":{"type":"string","enum":["ai"]}},"required":["type"]},"AIProviderKind":{"type":"string","description":"Supported AI provider types","enum":["openai","azure_openai","anthropic","mistral","deepseek","googleai","groq","openrouter","togetherai","customai","aws_bedrock"]},"ProviderConfig":{"type":"object","description":"Complete AI provider configuration with resource reference and model selection","properties":{"kind":{"$ref":"#/components/schemas/AIProviderKind"},"resource":{"type":"string","description":"Resource reference in format '$res:{resource_path}' pointing to provider credentials"},"model":{"type":"string","description":"Model identifier (e.g., 'gpt-4', 'claude-3-opus-20240229', 'gemini-pro')"}},"required":["kind","resource","model"]},"StaticProviderTransform":{"type":"object","description":"Static provider configuration passed directly to the AI agent","properties":{"value":{"$ref":"#/components/schemas/ProviderConfig"},"type":{"type":"string","enum":["static"]}},"required":["type","value"]},"ProviderTransform":{"description":"Provider configuration - can be static (ProviderConfig), JavaScript expression, or AI-determined","oneOf":[{"$ref":"#/components/schemas/StaticProviderTransform"},{"$ref":"#/components/schemas/JavascriptTransform"},{"$ref":"#/components/schemas/AiTransform"}],"discriminator":{"propertyName":"type","mapping":{"static":"#/components/schemas/StaticProviderTransform","javascript":"#/components/schemas/JavascriptTransform","ai":"#/components/schemas/AiTransform"}}},"MemoryOff":{"type":"object","description":"No conversation memory/context","properties":{"kind":{"type":"string","enum":["off"]}},"required":["kind"]},"MemoryAuto":{"type":"object","description":"Automatic context management","properties":{"kind":{"type":"string","enum":["auto"]},"context_length":{"type":"integer","description":"Maximum number of messages to retain in context"},"memory_id":{"type":"string","description":"Identifier for persistent memory across agent invocations"}},"required":["kind"]},"MemoryMessage":{"type":"object","description":"A single message in conversation history","properties":{"role":{"type":"string","enum":["user","assistant","system"]},"content":{"type":"string"}},"required":["role","content"]},"MemoryManual":{"type":"object","description":"Explicit message history","properties":{"kind":{"type":"string","enum":["manual"]},"messages":{"type":"array","items":{"$ref":"#/components/schemas/MemoryMessage"}}},"required":["kind","messages"]},"MemoryConfig":{"description":"Conversation memory configuration","oneOf":[{"$ref":"#/components/schemas/MemoryOff"},{"$ref":"#/components/schemas/MemoryAuto"},{"$ref":"#/components/schemas/MemoryManual"}],"discriminator":{"propertyName":"kind","mapping":{"off":"#/components/schemas/MemoryOff","auto":"#/components/schemas/MemoryAuto","manual":"#/components/schemas/MemoryManual"}}},"StaticMemoryTransform":{"type":"object","description":"Static memory configuration passed directly to the AI agent","properties":{"value":{"$ref":"#/components/schemas/MemoryConfig"},"type":{"type":"string","enum":["static"]}},"required":["type","value"]},"MemoryTransform":{"description":"Memory configuration - can be static (MemoryConfig), JavaScript expression, or AI-determined","oneOf":[{"$ref":"#/components/schemas/StaticMemoryTransform"},{"$ref":"#/components/schemas/JavascriptTransform"},{"$ref":"#/components/schemas/AiTransform"}],"discriminator":{"propertyName":"type","mapping":{"static":"#/components/schemas/StaticMemoryTransform","javascript":"#/components/schemas/JavascriptTransform","ai":"#/components/schemas/AiTransform"}}},"FlowModuleValue":{"description":"The actual implementation of a flow step. Can be a script (inline or referenced), subflow, loop, branch, or special module type","oneOf":[{"$ref":"#/components/schemas/RawScript"},{"$ref":"#/components/schemas/PathScript"},{"$ref":"#/components/schemas/PathFlow"},{"$ref":"#/components/schemas/ForloopFlow"},{"$ref":"#/components/schemas/WhileloopFlow"},{"$ref":"#/components/schemas/BranchOne"},{"$ref":"#/components/schemas/BranchAll"},{"$ref":"#/components/schemas/Identity"},{"$ref":"#/components/schemas/AiAgent"}],"discriminator":{"propertyName":"type","mapping":{"rawscript":"#/components/schemas/RawScript","script":"#/components/schemas/PathScript","flow":"#/components/schemas/PathFlow","forloopflow":"#/components/schemas/ForloopFlow","whileloopflow":"#/components/schemas/WhileloopFlow","branchone":"#/components/schemas/BranchOne","branchall":"#/components/schemas/BranchAll","identity":"#/components/schemas/Identity","aiagent":"#/components/schemas/AiAgent"}}},"RawScript":{"type":"object","description":"Inline script with code defined directly in the flow. Use 'bun' as default language if unspecified. The script receives arguments from input_transforms","properties":{"input_transforms":{"type":"object","description":"Map of parameter names to their values (static or JavaScript expressions). These become the script's input arguments","additionalProperties":{"$ref":"#/components/schemas/InputTransform"}},"content":{"type":"string","description":"The script source code. Should export a 'main' function"},"language":{"type":"string","description":"Programming language for this script","enum":["deno","bun","python3","go","bash","powershell","postgresql","mysql","bigquery","snowflake","mssql","oracledb","graphql","nativets","php","rust","ansible","csharp","nu","java","ruby","duckdb"]},"path":{"type":"string","description":"Optional path for saving this script"},"lock":{"type":"string","description":"Lock file content for dependencies"},"type":{"type":"string","enum":["rawscript"]},"tag":{"type":"string","description":"Worker group tag for execution routing"},"concurrent_limit":{"type":"number","description":"Maximum concurrent executions of this script"},"concurrency_time_window_s":{"type":"number","description":"Time window for concurrent_limit"},"custom_concurrency_key":{"type":"string","description":"Custom key for grouping concurrent executions"},"is_trigger":{"type":"boolean","description":"If true, this script is a trigger that can start the flow"},"assets":{"type":"array","description":"External resources this script accesses (S3 objects, resources, etc.)","items":{"type":"object","required":["path","kind"],"properties":{"path":{"type":"string","description":"Path to the asset"},"kind":{"type":"string","description":"Type of asset","enum":["s3object","resource","ducklake","datatable"]},"access_type":{"type":"string","nullable":true,"description":"Access level for this asset","enum":["r","w","rw"]},"alt_access_type":{"type":"string","nullable":true,"description":"Alternative access level","enum":["r","w","rw"]}}}}},"required":["type","content","language","input_transforms"]},"PathScript":{"type":"object","description":"Reference to an existing script by path. Use this when calling a previously saved script instead of writing inline code","properties":{"input_transforms":{"type":"object","description":"Map of parameter names to their values (static or JavaScript expressions). These become the script's input arguments","additionalProperties":{"$ref":"#/components/schemas/InputTransform"}},"path":{"type":"string","description":"Path to the script in the workspace (e.g., 'f/scripts/send_email')"},"hash":{"type":"string","description":"Optional specific version hash of the script to use"},"type":{"type":"string","enum":["script"]},"tag_override":{"type":"string","description":"Override the script's default worker group tag"},"is_trigger":{"type":"boolean","description":"If true, this script is a trigger that can start the flow"}},"required":["type","path","input_transforms"]},"PathFlow":{"type":"object","description":"Reference to an existing flow by path. Use this to call another flow as a subflow","properties":{"input_transforms":{"type":"object","description":"Map of parameter names to their values (static or JavaScript expressions). These become the subflow's input arguments","additionalProperties":{"$ref":"#/components/schemas/InputTransform"}},"path":{"type":"string","description":"Path to the flow in the workspace (e.g., 'f/flows/process_user')"},"type":{"type":"string","enum":["flow"]}},"required":["type","path","input_transforms"]},"ForloopFlow":{"type":"object","description":"Executes nested modules in a loop over an iterator. Inside the loop, use 'flow_input.iter.value' to access the current iteration value, and 'flow_input.iter.index' for the index. Supports parallel execution for better performance on I/O-bound operations","properties":{"modules":{"type":"array","description":"Steps to execute for each iteration. These can reference the iteration value via 'flow_input.iter.value'","items":{"$ref":"#/components/schemas/FlowModule"}},"iterator":{"description":"JavaScript expression that returns an array to iterate over. Can reference 'results.step_id' or 'flow_input'","$ref":"#/components/schemas/InputTransform"},"skip_failures":{"type":"boolean","description":"If true, iteration failures don't stop the loop. Failed iterations return null"},"type":{"type":"string","enum":["forloopflow"]},"parallel":{"type":"boolean","description":"If true, iterations run concurrently (faster for I/O-bound operations). Use with parallelism to control concurrency"},"parallelism":{"description":"Maximum number of concurrent iterations when parallel=true. Limits resource usage. Can be static number or expression","$ref":"#/components/schemas/InputTransform"},"squash":{"type":"boolean"}},"required":["modules","iterator","skip_failures","type"]},"WhileloopFlow":{"type":"object","description":"Executes nested modules repeatedly while a condition is true. The loop checks the condition after each iteration. Use stop_after_if on modules to control loop termination","properties":{"modules":{"type":"array","description":"Steps to execute in each iteration. Use stop_after_if to control when the loop ends","items":{"$ref":"#/components/schemas/FlowModule"}},"skip_failures":{"type":"boolean","description":"If true, iteration failures don't stop the loop. Failed iterations return null"},"type":{"type":"string","enum":["whileloopflow"]},"parallel":{"type":"boolean","description":"If true, iterations run concurrently (use with caution in while loops)"},"parallelism":{"description":"Maximum number of concurrent iterations when parallel=true","$ref":"#/components/schemas/InputTransform"},"squash":{"type":"boolean"}},"required":["modules","skip_failures","type"]},"BranchOne":{"type":"object","description":"Conditional branching where only the first matching branch executes. Branches are evaluated in order, and the first one with a true expression runs. If no branches match, the default branch executes","properties":{"branches":{"type":"array","description":"Array of branches to evaluate in order. The first branch with expr evaluating to true executes","items":{"type":"object","properties":{"summary":{"type":"string","description":"Short description of this branch condition"},"expr":{"type":"string","description":"JavaScript expression that returns boolean. Can use 'results.step_id' or 'flow_input'. First true expr wins"},"modules":{"type":"array","description":"Steps to execute if this branch's expr is true","items":{"$ref":"#/components/schemas/FlowModule"}}},"required":["modules","expr"]}},"default":{"type":"array","description":"Steps to execute if no branch expressions match","items":{"$ref":"#/components/schemas/FlowModule"}},"type":{"type":"string","enum":["branchone"]}},"required":["branches","default","type"]},"BranchAll":{"type":"object","description":"Parallel branching where all branches execute simultaneously. Unlike BranchOne, all branches run regardless of conditions. Useful for executing independent tasks concurrently","properties":{"branches":{"type":"array","description":"Array of branches that all execute (either in parallel or sequentially)","items":{"type":"object","properties":{"summary":{"type":"string","description":"Short description of this branch's purpose"},"skip_failure":{"type":"boolean","description":"If true, failure in this branch doesn't fail the entire flow"},"modules":{"type":"array","description":"Steps to execute in this branch","items":{"$ref":"#/components/schemas/FlowModule"}}},"required":["modules"]}},"type":{"type":"string","enum":["branchall"]},"parallel":{"type":"boolean","description":"If true, all branches execute concurrently. If false, they execute sequentially"}},"required":["branches","type"]},"AgentTool":{"type":"object","description":"A tool available to an AI agent. Can be a flow module or an external MCP (Model Context Protocol) tool","properties":{"id":{"type":"string","description":"Unique identifier for this tool. Cannot contain spaces - use underscores instead (e.g., 'get_user_data' not 'get user data')"},"summary":{"type":"string","description":"Short description of what this tool does (shown to the AI)"},"value":{"$ref":"#/components/schemas/ToolValue"}},"required":["id","value"]},"ToolValue":{"description":"The implementation of a tool. Can be a flow module (script/flow) or an MCP tool reference","oneOf":[{"$ref":"#/components/schemas/FlowModuleTool"},{"$ref":"#/components/schemas/McpToolValue"},{"$ref":"#/components/schemas/WebsearchToolValue"}],"discriminator":{"propertyName":"tool_type","mapping":{"flowmodule":"#/components/schemas/FlowModuleTool","mcp":"#/components/schemas/McpToolValue","websearch":"#/components/schemas/WebsearchToolValue"}}},"FlowModuleTool":{"description":"A tool implemented as a flow module (script, flow, etc.). The AI can call this like any other flow module","allOf":[{"type":"object","properties":{"tool_type":{"type":"string","enum":["flowmodule"]}},"required":["tool_type"]},{"$ref":"#/components/schemas/FlowModuleValue"}]},"WebsearchToolValue":{"type":"object","description":"A tool implemented as a websearch tool. The AI can call this like any other websearch tool","properties":{"tool_type":{"type":"string","enum":["websearch"]}},"required":["tool_type"]},"McpToolValue":{"type":"object","description":"Reference to an external MCP (Model Context Protocol) tool. The AI can call tools from MCP servers","properties":{"tool_type":{"type":"string","enum":["mcp"]},"resource_path":{"type":"string","description":"Path to the MCP resource/server configuration"},"include_tools":{"type":"array","description":"Whitelist of specific tools to include from this MCP server","items":{"type":"string"}},"exclude_tools":{"type":"array","description":"Blacklist of tools to exclude from this MCP server","items":{"type":"string"}}},"required":["tool_type","resource_path"]},"AiAgent":{"type":"object","description":"AI agent step that can use tools to accomplish tasks. The agent receives inputs and can call any of its configured tools to complete the task","properties":{"input_transforms":{"type":"object","description":"Input parameters for the AI agent mapped to their values","properties":{"provider":{"$ref":"#/components/schemas/ProviderTransform"},"output_type":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"Output format type.\\nValid values: 'text' (default) - plain text response, 'image' - image generation\\n"},"user_message":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"The user's prompt/message to the AI agent. Supports variable interpolation with flow.input syntax."},"system_prompt":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"System instructions that guide the AI's behavior, persona, and response style. Optional."},"streaming":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"Boolean. If true, stream the AI response incrementally.\\nStreaming events include: token_delta, tool_call, tool_call_arguments, tool_execution, tool_result\\n"},"memory":{"$ref":"#/components/schemas/MemoryTransform"},"output_schema":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"JSON Schema object defining structured output format. Used when you need the AI to return data in a specific shape.\\nSupports standard JSON Schema properties: type, properties, required, items, enum, pattern, minLength, maxLength, minimum, maximum, etc.\\nExample: { type: 'object', properties: { name: { type: 'string' }, age: { type: 'integer' } }, required: ['name'] }\\n"},"user_images":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"Array of image references for vision-capable models.\\nFormat: Array<{ bucket: string, key: string }> - S3 object references\\nExample: [{ bucket: 'my-bucket', key: 'images/photo.jpg' }]\\n"},"max_completion_tokens":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"Integer. Maximum number of tokens the AI will generate in its response.\\nRange: 1 to 4,294,967,295. Typical values: 256-4096 for most use cases.\\n"},"temperature":{"allOf":[{"$ref":"#/components/schemas/InputTransform"}],"description":"Float. Controls randomness/creativity of responses.\\nRange: 0.0 to 2.0 (provider-dependent)\\n- 0.0 = deterministic, focused responses\\n- 0.7 = balanced (common default)\\n- 1.0+ = more creative/random\\n"}},"required":["provider","user_message","output_type"]},"tools":{"type":"array","description":"Array of tools the agent can use. The agent decides which tools to call based on the task","items":{"$ref":"#/components/schemas/AgentTool"}},"type":{"type":"string","enum":["aiagent"]},"parallel":{"type":"boolean","description":"If true, the agent can execute multiple tool calls in parallel"}},"required":["tools","type","input_transforms"]},"Identity":{"type":"object","description":"Pass-through module that returns its input unchanged. Useful for flow structure or as a placeholder","properties":{"type":{"type":"string","enum":["identity"]},"flow":{"type":"boolean","description":"If true, marks this as a flow identity (special handling)"}},"required":["type"]},"FlowStatus":{"type":"object","properties":{"step":{"type":"integer"},"modules":{"type":"array","items":{"$ref":"#/components/schemas/FlowStatusModule"}},"user_states":{"additionalProperties":true},"preprocessor_module":{"allOf":[{"$ref":"#/components/schemas/FlowStatusModule"}]},"failure_module":{"allOf":[{"$ref":"#/components/schemas/FlowStatusModule"},{"type":"object","properties":{"parent_module":{"type":"string"}}}]},"retry":{"type":"object","properties":{"fail_count":{"type":"integer"},"failed_jobs":{"type":"array","items":{"type":"string","format":"uuid"}}}}},"required":["step","modules","failure_module"]},"FlowStatusModule":{"type":"object","properties":{"type":{"type":"string","enum":["WaitingForPriorSteps","WaitingForEvents","WaitingForExecutor","InProgress","Success","Failure"]},"id":{"type":"string"},"job":{"type":"string","format":"uuid"},"count":{"type":"integer"},"progress":{"type":"integer"},"iterator":{"type":"object","properties":{"index":{"type":"integer"},"itered":{"type":"array","items":{}},"itered_len":{"type":"integer"},"args":{}}},"flow_jobs":{"type":"array","items":{"type":"string"}},"flow_jobs_success":{"type":"array","items":{"type":"boolean"}},"flow_jobs_duration":{"type":"object","properties":{"started_at":{"type":"array","items":{"type":"string"}},"duration_ms":{"type":"array","items":{"type":"integer"}}}},"branch_chosen":{"type":"object","properties":{"type":{"type":"string","enum":["branch","default"]},"branch":{"type":"integer"}},"required":["type"]},"branchall":{"type":"object","properties":{"branch":{"type":"integer"},"len":{"type":"integer"}},"required":["branch","len"]},"approvers":{"type":"array","items":{"type":"object","properties":{"resume_id":{"type":"integer"},"approver":{"type":"string"}},"required":["resume_id","approver"]}},"failed_retries":{"type":"array","items":{"type":"string","format":"uuid"}},"skipped":{"type":"boolean"},"agent_actions":{"type":"array","items":{"type":"object","oneOf":[{"type":"object","properties":{"job_id":{"type":"string","format":"uuid"},"function_name":{"type":"string"},"type":{"type":"string","enum":["tool_call"]},"module_id":{"type":"string"}},"required":["job_id","function_name","type","module_id"]},{"type":"object","properties":{"call_id":{"type":"string","format":"uuid"},"function_name":{"type":"string"},"resource_path":{"type":"string"},"type":{"type":"string","enum":["mcp_tool_call"]},"arguments":{"type":"object"}},"required":["call_id","function_name","resource_path","type"]},{"type":"object","properties":{"type":{"type":"string","enum":["web_search"]}},"required":["type"]},{"type":"object","properties":{"type":{"type":"string","enum":["message"]}},"required":["content","type"]}]}},"agent_actions_success":{"type":"array","items":{"type":"boolean"}}},"required":["type"]}}`, "raw-app": `--- name: raw-app description: MUST use when creating raw apps. @@ -4540,7 +4540,7 @@ description: MUST use when using the CLI. The Windmill CLI (\`wmill\`) provides commands for managing scripts, flows, apps, and other resources. -Current version: 1.624.0 +Current version: 1.642.0 ## Global Options @@ -4566,7 +4566,7 @@ app related commands - \`--json\` - Output as JSON (for piping to jq) - \`app get \` - get an app's details - \`--json\` - Output as JSON (for piping to jq) -- \`app push \` - push a local app +- \`app push \` - push a local app - \`app dev [app_folder:string]\` - Start a development server for building apps with live reload and hot module replacement - \`--port \` - Port to run the dev server on (will find next available port if occupied) - \`--host \` - Host to bind the dev server to @@ -4627,7 +4627,7 @@ flow related commands - \`flow new \` - create a new empty flow - \`--summary \` - flow summary - \`--description \` - flow description -- \`flow bootstrap \` - create a new empty flow (alias for new) +- \`flow bootstrap \` - create a new empty flow (alias for new - \`--summary \` - flow summary - \`--description \` - flow description @@ -4645,7 +4645,10 @@ folder related commands - \`folder get \` - get a folder's details - \`--json\` - Output as JSON (for piping to jq) - \`folder new \` - create a new folder locally -- \`folder push \` - push a local folder spec. This overrides any remote versions. + - \`--summary \` - folder summary +- \`folder push \` - push a local folder to the remote by name. This overrides any remote versions. +- \`folder add-missing\` - create default folder.meta.yaml for all subdirectories of f/ that are missing one + - \`-y, --yes\` - skip confirmation prompt ### gitsync-settings @@ -4724,6 +4727,9 @@ sync local with a remote instance or the opposite (push or pull) - \`--prefix \` - Prefix of the local workspaces folders to push - \`--prefix-settings\` - Store instance yamls inside prefixed folders when using --prefix and --folder-per-instance - \`instance whoami\` - Display information about the currently logged-in user +- \`instance get-config\` - Dump the current instance config (global settings + worker configs) as YAML + - \`-o, --output-file \` - Write YAML to a file instead of stdout + - \`--instance \` - Name of the instance, override the active instance ### jobs @@ -4741,6 +4747,17 @@ Pull completed and queued jobs from workspace - \`jobs pull\` - \`jobs push\` +### lint + +Validate Windmill flow, schedule, and trigger YAML files in a directory + +**Arguments:** \`[directory:string]\` + +**Options:** +- \`--json\` - Output results in JSON format +- \`--fail-on-warn\` - Exit with code 1 when warnings are emitted +- \`--locks-required\` - Fail if scripts or flow inline scripts that need locks have no locks + ### queues List all queues with their metrics @@ -4814,10 +4831,10 @@ script related commands - \`script list\` - list all scripts - \`--show-archived\` - Enable archived scripts in output - \`--json\` - Output as JSON (for piping to jq) +- \`script push \` - push a local script spec. This overrides any remote versions. Use the script file (.ts, .js, .py, .sh - \`script get \` - get a script's details - \`--json\` - Output as JSON (for piping to jq) -- \`script show \` - show a script's content (alias for get) -- \`script push \` - push a local script spec. This overrides any remote versions. Use the script file (.ts, .js, .py, .sh) +- \`script show \` - show a script's content (alias for get - \`script run \` - run a script by path - \`-d --data \` - Inputs specified as a JSON string or a file using @ or stdin using @-. - \`-s --silent\` - Do not output anything other then the final output. Useful for scripting. @@ -4827,10 +4844,10 @@ script related commands - \`script new \` - create a new script - \`--summary \` - script summary - \`--description \` - script description -- \`script bootstrap \` - create a new script (alias for new) +- \`script bootstrap \` - create a new script (alias for new - \`--summary \` - script summary - \`--description \` - 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\`) +- \`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 @@ -4903,6 +4920,8 @@ sync local with a remote workspaces or the opposite (push or pull) - \`--parallel \` - Number of changes to process in parallel - \`--repository \` - Specify repository path (e.g., u/user/repo) when multiple repositories exist - \`--branch \` - Override the current git branch (works even outside a git repository) + - \`--lint\` - Run lint validation before pushing + - \`--locks-required\` - Fail if scripts or flow inline scripts that need locks have no locks ### trigger @@ -4917,7 +4936,7 @@ trigger related commands - \`--json\` - Output as JSON (for piping to jq) - \`trigger get \` - get a trigger's details - \`--json\` - Output as JSON (for piping to jq) - - \`--kind \` - Trigger kind (http, websocket, kafka, nats, postgres, mqtt, sqs, gcp, email) + - \`--kind \` - Trigger kind (http, websocket, kafka, nats, postgres, mqtt, sqs, gcp, email). Recommended for faster lookup - \`trigger new \` - create a new trigger locally - \`--kind \` - Trigger kind (required: http, websocket, kafka, nats, postgres, mqtt, sqs, gcp, email) - \`trigger push \` - push a local trigger spec. This overrides any remote versions. @@ -4999,7 +5018,8 @@ workspace related commands - \`--create-username \` - Specify your own username in the newly created workspace. Ignored if --create is not specified, the workspace already exists or automatic username creation is enabled on the instance. - \`workspace remove \` - Remove a workspace - \`workspace whoami\` - Show the currently active user -- \`workspace list\` - List workspaces on the remote server that you have access to +- \`workspace list\` - List local workspace profiles +- \`workspace list-remote\` - List workspaces on the remote server that you have access to - \`workspace bind\` - Bind the current Git branch to the active workspace - \`--branch \` - Specify branch (defaults to current) - \`workspace unbind\` - Remove workspace binding from the current Git branch @@ -5252,6 +5272,14 @@ properties: items: type: string description: Array of Kafka topic names to subscribe to + filters: + type: array + items: + type: object + properties: + key: + type: string + value: {} error_handler_path: type: string description: Path to a script or flow to run when the triggered job fails @@ -5299,6 +5327,7 @@ required: - kafka_resource_path - group_id - topics +- filters `, "mqtt_trigger": `type: object properties: diff --git a/cli/test/cargo_backend.ts b/cli/test/cargo_backend.ts index 7c85ff5bab..5c9719f6d5 100644 --- a/cli/test/cargo_backend.ts +++ b/cli/test/cargo_backend.ts @@ -232,8 +232,9 @@ export class CargoBackend { */ private getBasePostgresUrl(): string { const url = new URL(this.config.postgresUrl); - // Remove any existing database path + // Remove any existing database path and query params (e.g. ?sslmode=disable) url.pathname = ""; + url.search = ""; return url.toString().replace(/\/$/, ""); // Remove trailing slash } @@ -629,13 +630,13 @@ export class CargoBackend { /** * Create CLI command with proper authentication */ - createCLICommand(args: string[], workingDir: string, workspaceName?: string): { command: string, args: string[], cwd: string, env: Record } { - const workspace = workspaceName || this.config.workspace; + createCLICommand(args: string[], workingDir: string, opts?: { workspace?: string; token?: string }): { command: string, args: string[], cwd: string, env: Record } { + const workspace = opts?.workspace || this.config.workspace; const cliDir = join(dirname(fileURLToPath(import.meta.url)), ".."); const fullArgs = [ "--base-url", this.baseUrl, "--workspace", workspace, - "--token", this.token, + "--token", opts?.token || this.token, "--config-dir", this.config.testConfigDir, ...args, ]; @@ -660,12 +661,12 @@ export class CargoBackend { /** * Run CLI command and return result */ - async runCLICommand(args: string[], workingDir: string, workspaceName?: string): Promise<{ + async runCLICommand(args: string[], workingDir: string, opts?: { workspace?: string; token?: string }): Promise<{ stdout: string; stderr: string; code: number; }> { - const cmd = this.createCLICommand(args, workingDir, workspaceName); + const cmd = this.createCLICommand(args, workingDir, opts); const proc = Bun.spawn([cmd.command, ...cmd.args], { cwd: cmd.cwd, env: cmd.env, diff --git a/cli/test/containerized_backend.ts b/cli/test/containerized_backend.ts index 65c26c8373..90dad6ccdc 100644 --- a/cli/test/containerized_backend.ts +++ b/cli/test/containerized_backend.ts @@ -1020,12 +1020,12 @@ export async function main( /** * Create CLI command with proper authentication */ - createCLICommand(args: string[], workingDir: string, workspaceName?: string): { cmd: string[], cwd: string } { - const workspace = workspaceName || this.config.workspace; + createCLICommand(args: string[], workingDir: string, opts?: { workspace?: string; token?: string }): { cmd: string[], cwd: string } { + const workspace = opts?.workspace || this.config.workspace; const fullArgs = [ '--base-url', this.config.baseUrl, '--workspace', workspace, - '--token', this.config.token, + '--token', opts?.token || this.config.token, '--config-dir', this.config.testConfigDir, ...args ]; @@ -1049,12 +1049,12 @@ export async function main( /** * Run CLI command and return result */ - async runCLICommand(args: string[], workingDir: string, workspaceName?: string): Promise<{ + async runCLICommand(args: string[], workingDir: string, opts?: { workspace?: string; token?: string }): Promise<{ stdout: string; stderr: string; code: number; }> { - const { cmd, cwd } = this.createCLICommand(args, workingDir, workspaceName); + const { cmd, cwd } = this.createCLICommand(args, workingDir, opts); const proc = Bun.spawn(cmd, { stdout: 'pipe', stderr: 'pipe', diff --git a/cli/test/folder_missing_meta.test.ts b/cli/test/folder_missing_meta.test.ts new file mode 100644 index 0000000000..0c95a9ef4f --- /dev/null +++ b/cli/test/folder_missing_meta.test.ts @@ -0,0 +1,400 @@ +/** + * Tests for missing folder.meta.yaml detection during sync push, + * the `folder add-missing` command, and the simplified `folder push` command. + */ + +import { expect, test, describe } from "bun:test"; +import { writeFile, mkdir, readFile, rm, mkdtemp } from "node:fs/promises"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; +import { getTestBackend, createNonAdminUser } from "./test_backend.ts"; + +type IsolatedWorkspaceTestContext = { + backend: any; + tempDir: string; + workspaceId: string; + runCLICommand: ( + args: string[], + opts?: { token?: string } + ) => Promise<{ stdout: string; stderr: string; code: number }>; + apiRequest: (path: string, options?: RequestInit) => Promise; +}; + +async function createWorkspace(backend: any, workspaceId: string): Promise { + const response = await backend.apiRequest!("/api/workspaces/create", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + id: workspaceId, + // Workspace name has a 50-char DB limit; keep it identical to the short ID. + name: workspaceId, + }), + }); + + if (!response.ok) { + const error = await response.text(); + if (!error.includes("already exists") && !error.includes("duplicate")) { + throw new Error(`Failed to create workspace ${workspaceId}: ${error}`); + } + return; + } + await response.text(); +} + +async function withIsolatedWorkspace( + testFn: (ctx: IsolatedWorkspaceTestContext) => Promise +): Promise { + const backend = await getTestBackend(); + const tempDir = await mkdtemp(join(tmpdir(), "windmill_cli_test_")); + const workspaceId = `fmeta_${Date.now().toString(36)}_${Math.random() + .toString(36) + .slice(2, 6)}`; + let workspaceCreated = false; + + try { + await createWorkspace(backend, workspaceId); + workspaceCreated = true; + + await testFn({ + backend, + tempDir, + workspaceId, + runCLICommand: (args: string[], opts?: { token?: string }) => + backend.runCLICommand(args, tempDir, { + workspace: workspaceId, + token: opts?.token, + }), + apiRequest: (path: string, options?: RequestInit) => + backend.apiRequest!(`/api/w/${workspaceId}${path}`, options), + }); + } finally { + if (workspaceCreated) { + try { + const archiveResponse = await backend.apiRequest!( + `/api/w/${workspaceId}/workspaces/archive`, + { method: "POST" } + ); + await archiveResponse.text(); + } catch { + // Best-effort cleanup to avoid exceeding non-enterprise workspace limits. + } + } + await rm(tempDir, { recursive: true, force: true }); + } +} + +function wmillYaml(): string { + return `defaultTs: bun\nincludes:\n - "**"\nexcludes: []\n`; +} + +// ============================================================================= +// folder new — creates folder.meta.yaml with summary and display_name +// ============================================================================= + +describe("folder new", () => { + test("creates folder.meta.yaml with summary and display_name", async () => { + await withIsolatedWorkspace(async ({ tempDir, runCLICommand }) => { + const folderName = `newfolder${Date.now()}`; + const result = await runCLICommand( + ["folder", "new", folderName, "--summary", "My summary"], + ); + + expect(result.code).toEqual(0); + + const metaPath = join(tempDir, "f", folderName, "folder.meta.yaml"); + const content = await readFile(metaPath, "utf-8"); + expect(content).toContain("summary: My summary"); + expect(content).toContain(`display_name: ${folderName}`); + expect(content).toContain("owners:"); + expect(content).toContain("extra_perms:"); + }); + }); + + test("creates folder.meta.yaml with empty summary when none provided", async () => { + await withIsolatedWorkspace(async ({ tempDir, runCLICommand }) => { + const folderName = `nosummary${Date.now()}`; + const result = await runCLICommand( + ["folder", "new", folderName], + ); + + expect(result.code).toEqual(0); + + const content = await readFile( + join(tempDir, "f", folderName, "folder.meta.yaml"), + "utf-8" + ); + expect(content).toContain('summary: ""'); + expect(content).toContain(`display_name: ${folderName}`); + }); + }); + + test("fails if folder.meta.yaml already exists", async () => { + await withIsolatedWorkspace(async ({ runCLICommand }) => { + const folderName = `dupfolder${Date.now()}`; + // Create first + await runCLICommand(["folder", "new", folderName]); + + // Try again — should fail + const result = await runCLICommand( + ["folder", "new", folderName], + ); + expect(result.code).not.toEqual(0); + }); + }); +}); + +// ============================================================================= +// folder add-missing — scaffolds missing folder.meta.yaml files +// ============================================================================= + +describe("folder add-missing", () => { + test("creates folder.meta.yaml for directories missing one", async () => { + await withIsolatedWorkspace(async ({ tempDir, runCLICommand }) => { + // Create two folders: one with meta, one without + const withMeta = `withmeta${Date.now()}`; + const withoutMeta = `withoutmeta${Date.now()}`; + + await mkdir(join(tempDir, "f", withMeta), { recursive: true }); + await writeFile( + join(tempDir, "f", withMeta, "folder.meta.yaml"), + 'summary: ""\ndisplay_name: existing\nowners: []\nextra_perms: {}\n', + "utf-8" + ); + + await mkdir(join(tempDir, "f", withoutMeta), { recursive: true }); + // No folder.meta.yaml for withoutMeta + + const result = await runCLICommand( + ["folder", "add-missing", "-y"], + ); + + expect(result.code).toEqual(0); + + // withoutMeta should now have a folder.meta.yaml + const createdMeta = await readFile( + join(tempDir, "f", withoutMeta, "folder.meta.yaml"), + "utf-8" + ); + expect(createdMeta).toContain(`display_name: ${withoutMeta}`); + expect(createdMeta).toContain("owners:"); + + // withMeta should be unchanged + const existingMeta = await readFile( + join(tempDir, "f", withMeta, "folder.meta.yaml"), + "utf-8" + ); + expect(existingMeta).toContain("display_name: existing"); + }); + }); + + test("reports nothing to do when all folders have meta", async () => { + await withIsolatedWorkspace(async ({ tempDir, runCLICommand }) => { + const folderName = `alldone${Date.now()}`; + await mkdir(join(tempDir, "f", folderName), { recursive: true }); + await writeFile( + join(tempDir, "f", folderName, "folder.meta.yaml"), + 'summary: ""\ndisplay_name: done\nowners: []\nextra_perms: {}\n', + "utf-8" + ); + + const result = await runCLICommand( + ["folder", "add-missing", "-y"], + ); + + expect(result.code).toEqual(0); + expect(result.stdout + result.stderr).toContain("Nothing to do"); + }); + }); + + test("reports nothing to do when no f/ directory exists", async () => { + await withIsolatedWorkspace(async ({ runCLICommand }) => { + const result = await runCLICommand( + ["folder", "add-missing", "-y"], + ); + + expect(result.code).toEqual(0); + expect(result.stdout + result.stderr).toContain("Nothing to do"); + }); + }); +}); + +// ============================================================================= +// folder push — simplified single-arg signature +// ============================================================================= + +describe("folder push", () => { + test("pushes a folder by name", async () => { + await withIsolatedWorkspace(async ({ tempDir, runCLICommand, apiRequest }) => { + const folderName = `pushbyname${Date.now()}`; + + // Create local folder meta + await mkdir(join(tempDir, "f", folderName), { recursive: true }); + await writeFile( + join(tempDir, "f", folderName, "folder.meta.yaml"), + `summary: "pushed"\ndisplay_name: "${folderName}"\nowners:\n - "admin@windmill.dev"\nextra_perms: {}\n`, + "utf-8" + ); + + const result = await runCLICommand( + ["folder", "push", folderName], + ); + + expect(result.code).toEqual(0); + expect(result.stdout + result.stderr).toContain("Folder pushed"); + + // Verify via API + const apiResp = await apiRequest(`/folders/get/${folderName}`); + expect(apiResp.status).toEqual(200); + }); + }); + + test("fails when folder does not exist locally", async () => { + await withIsolatedWorkspace(async ({ runCLICommand }) => { + const result = await runCLICommand( + ["folder", "push", "nonexistent"], + ); + + expect(result.code).not.toEqual(0); + }); + }); +}); + +// ============================================================================= +// sync push — missing folder.meta.yaml detection +// ============================================================================= + +describe("sync push missing folder detection", () => { + test("admin user gets warning but push succeeds", async () => { + await withIsolatedWorkspace(async ({ tempDir, runCLICommand }) => { + const uniqueId = Date.now(); + const folderName = `nometaadmin${uniqueId}`; + + await writeFile(join(tempDir, "wmill.yaml"), wmillYaml(), "utf-8"); + + // Create a script inside a folder WITHOUT folder.meta.yaml + await mkdir(join(tempDir, "f", folderName), { recursive: true }); + await writeFile( + join(tempDir, "f", folderName, "test_script.ts"), + 'export async function main() { return "hello"; }', + "utf-8" + ); + + const result = await runCLICommand( + ["sync", "push", "--yes", "--includes", `f/${folderName}/**`], + ); + + // Admin should get a warning but push succeeds (exit 0) + expect(result.code).toEqual(0); + const output = result.stdout + result.stderr; + expect(output).toContain("Missing folder.meta.yaml"); + expect(output).toContain(folderName); + expect(output).toContain("wmill folder add-missing"); + }); + }); + + test("no warning when folder.meta.yaml exists", async () => { + await withIsolatedWorkspace(async ({ tempDir, runCLICommand }) => { + const uniqueId = Date.now(); + const folderName = `withmeta${uniqueId}`; + + await writeFile(join(tempDir, "wmill.yaml"), wmillYaml(), "utf-8"); + + // Create folder WITH folder.meta.yaml + await mkdir(join(tempDir, "f", folderName), { recursive: true }); + await writeFile( + join(tempDir, "f", folderName, "folder.meta.yaml"), + `summary: ""\ndisplay_name: "${folderName}"\nowners: []\nextra_perms: {}\n`, + "utf-8" + ); + await writeFile( + join(tempDir, "f", folderName, "test_script.ts"), + 'export async function main() { return "hello"; }', + "utf-8" + ); + + const result = await runCLICommand( + ["sync", "push", "--yes", "--includes", `f/${folderName}/**`], + ); + + expect(result.code).toEqual(0); + const output = result.stdout + result.stderr; + expect(output).not.toContain("Missing folder.meta.yaml"); + }); + }); + + test.skipIf(!process.env["EE_LICENSE_KEY"])("non-admin user gets error and exit code 1", async () => { + await withIsolatedWorkspace(async ({ backend, tempDir, workspaceId, runCLICommand, apiRequest }) => { + const nonAdminToken = await createNonAdminUser(backend, workspaceId); + + const uniqueId = Date.now(); + const folderName = `nometanonadmin${uniqueId}`; + + await writeFile(join(tempDir, "wmill.yaml"), wmillYaml(), "utf-8"); + + // Create a script inside a folder WITHOUT folder.meta.yaml + // First create the folder on remote so the non-admin has somewhere to push + await apiRequest( + "/folders/create", + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + name: folderName, + extra_perms: { "g/all": true }, + }), + } + ); + + await mkdir(join(tempDir, "f", folderName), { recursive: true }); + await writeFile( + join(tempDir, "f", folderName, "test_script.ts"), + 'export async function main() { return "hello"; }', + "utf-8" + ); + + const result = await runCLICommand( + ["sync", "push", "--yes", "--includes", `f/${folderName}/**`], + { token: nonAdminToken } + ); + + // Non-admin should get exit code 1 + expect(result.code).toEqual(1); + const output = result.stdout + result.stderr; + expect(output).toContain("Missing folder.meta.yaml"); + expect(output).toContain("wmill folder add-missing"); + }); + }); + + test("no warning for deleted changes without folder.meta.yaml", async () => { + await withIsolatedWorkspace(async ({ tempDir, runCLICommand, apiRequest }) => { + const uniqueId = Date.now(); + const folderName = `delfolder${uniqueId}`; + + // Create folder and script on remote via API + await apiRequest( + "/folders/create", + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ name: folderName }), + } + ); + + await writeFile(join(tempDir, "wmill.yaml"), wmillYaml(), "utf-8"); + + // Pull to get remote state, then delete the folder locally + await runCLICommand(["sync", "pull", "--yes"]); + + // Remove the folder locally to trigger a "deleted" change + await rm(join(tempDir, "f", folderName), { recursive: true, force: true }); + + const result = await runCLICommand( + ["sync", "push", "--yes", "--includes", `f/${folderName}/**`], + ); + + // Should not warn about missing meta for deleted items + const output = result.stdout + result.stderr; + expect(output).not.toContain("Missing folder.meta.yaml"); + }); + }); +}); diff --git a/cli/test/test_backend.ts b/cli/test/test_backend.ts index a3091d3a0e..63c7f3078d 100644 --- a/cli/test/test_backend.ts +++ b/cli/test/test_backend.ts @@ -40,8 +40,8 @@ export interface TestBackend { stop(): Promise; reset(): Promise; - createCLICommand(args: string[], workingDir: string, workspaceName?: string): any; - runCLICommand(args: string[], workingDir: string, workspaceName?: string): Promise<{ + createCLICommand(args: string[], workingDir: string, opts?: { workspace?: string; token?: string }): any; + runCLICommand(args: string[], workingDir: string, opts?: { workspace?: string; token?: string }): Promise<{ stdout: string; stderr: string; code: number; @@ -97,12 +97,12 @@ class CargoBackendAdapter implements TestBackend { await this.backend.reset(); } - createCLICommand(args: string[], workingDir: string, workspaceName?: string): any { - return this.backend.createCLICommand(args, workingDir, workspaceName); + createCLICommand(args: string[], workingDir: string, opts?: { workspace?: string; token?: string }): any { + return this.backend.createCLICommand(args, workingDir, opts); } - async runCLICommand(args: string[], workingDir: string, workspaceName?: string) { - return this.backend.runCLICommand(args, workingDir, workspaceName); + async runCLICommand(args: string[], workingDir: string, opts?: { workspace?: string; token?: string }) { + return this.backend.runCLICommand(args, workingDir, opts); } async apiRequest(path: string, options?: RequestInit): Promise { @@ -369,12 +369,12 @@ class ContainerizedBackendAdapter implements TestBackend { await this.backend.reset(); } - createCLICommand(args: string[], workingDir: string, workspaceName?: string): any { - return this.backend.createCLICommand(args, workingDir, workspaceName); + createCLICommand(args: string[], workingDir: string, opts?: { workspace?: string; token?: string }): any { + return this.backend.createCLICommand(args, workingDir, opts); } - async runCLICommand(args: string[], workingDir: string, workspaceName?: string) { - return this.backend.runCLICommand(args, workingDir, workspaceName); + async runCLICommand(args: string[], workingDir: string, opts?: { workspace?: string; token?: string }) { + return this.backend.runCLICommand(args, workingDir, opts); } async seedTestData(): Promise { @@ -507,6 +507,66 @@ function registerCleanup() { } } +/** + * Create a non-admin user, add them to the workspace, and return their token. + */ +export async function createNonAdminUser( + backend: TestBackend, + workspaceId: string = backend.workspace +): Promise { + if (!backend.apiRequest) { + throw new Error("Backend does not support apiRequest"); + } + + const email = `nonadmin_${Date.now()}@test.dev`; + const password = "testpass123"; + + // Create user globally (as admin) + const createResp = await backend.apiRequest("/api/users/create", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + email, + password, + super_admin: false, + name: "Non-Admin Test User", + }), + }); + if (!createResp.ok) { + throw new Error(`Failed to create user: ${await createResp.text()}`); + } + await createResp.text(); + + // Add user to workspace as non-admin + const addResp = await backend.apiRequest( + `/api/w/${workspaceId}/workspaces/add_user`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + email, + is_admin: false, + operator: false, + }), + } + ); + if (!addResp.ok) { + throw new Error(`Failed to add user to workspace: ${await addResp.text()}`); + } + await addResp.text(); + + // Login as the non-admin user to get a token + const loginResp = await fetch(`${backend.baseUrl}/api/auth/login`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ email, password }), + }); + if (!loginResp.ok) { + throw new Error(`Failed to login as non-admin: ${await loginResp.text()}`); + } + return await loginResp.text(); +} + // Re-export for convenience export type { CargoBackendConfig } from "./cargo_backend.ts"; export type { ContainerConfig } from "./containerized_backend.ts"; diff --git a/system_prompts/auto-generated/cli/cli-commands.md b/system_prompts/auto-generated/cli/cli-commands.md index a40619d665..f26b4443eb 100644 --- a/system_prompts/auto-generated/cli/cli-commands.md +++ b/system_prompts/auto-generated/cli/cli-commands.md @@ -2,7 +2,7 @@ The Windmill CLI (`wmill`) provides commands for managing scripts, flows, apps, and other resources. -Current version: 1.624.0 +Current version: 1.642.0 ## Global Options @@ -19,8 +19,15 @@ Current version: 1.624.0 app related commands +**Options:** +- `--json` - Output as JSON (for piping to jq) + **Subcommands:** +- `app list` - list all apps + - `--json` - Output as JSON (for piping to jq) +- `app get ` - get an app's details + - `--json` - Output as JSON (for piping to jq) - `app push ` - push a local app - `app dev [app_folder:string]` - Start a development server for building apps with live reload and hot module replacement - `--port ` - Port to run the dev server on (will find next available port if occupied) @@ -58,10 +65,16 @@ Launch a dev server that will spawn a webserver with HMR flow related commands **Options:** -- `--show-archived` - Enable archived scripts in output +- `--show-archived` - Enable archived flows in output +- `--json` - Output as JSON (for piping to jq) **Subcommands:** +- `flow list` - list all flows + - `--show-archived` - Enable archived flows in output + - `--json` - Output as JSON (for piping to jq) +- `flow get ` - get a flow's details + - `--json` - Output as JSON (for piping to jq) - `flow push ` - push a local flow spec. This overrides any remote versions. - `flow run ` - run a flow by path. - `-d --data ` - Inputs specified as a JSON string or a file using @ or stdin using @-. @@ -73,17 +86,31 @@ flow related commands - `--yes` - Skip confirmation prompt - `-i --includes ` - Comma separated patterns to specify which file to take into account (among files that are compatible with windmill). Patterns can include * (any string until '/') and ** (any string) - `-e --excludes ` - Comma separated patterns to specify which file to NOT take into account. -- `flow bootstrap ` - create a new empty flow - - `--summary ` - script summary - - `--description ` - script description +- `flow new ` - create a new empty flow + - `--summary ` - flow summary + - `--description ` - flow description +- `flow bootstrap ` - create a new empty flow (alias for new + - `--summary ` - flow summary + - `--description ` - flow description ### folder folder related commands +**Options:** +- `--json` - Output as JSON (for piping to jq) + **Subcommands:** -- `folder push ` - push a local folder spec. This overrides any remote versions. +- `folder list` - list all folders + - `--json` - Output as JSON (for piping to jq) +- `folder get ` - get a folder's details + - `--json` - Output as JSON (for piping to jq) +- `folder new ` - create a new folder locally + - `--summary ` - folder summary +- `folder push ` - push a local folder to the remote by name. This overrides any remote versions. +- `folder add-missing` - create default folder.meta.yaml for all subdirectories of f/ that are missing one + - `-y, --yes` - skip confirmation prompt ### gitsync-settings @@ -162,6 +189,9 @@ sync local with a remote instance or the opposite (push or pull) - `--prefix ` - Prefix of the local workspaces folders to push - `--prefix-settings` - Store instance yamls inside prefixed folders when using --prefix and --folder-per-instance - `instance whoami` - Display information about the currently logged-in user +- `instance get-config` - Dump the current instance config (global settings + worker configs) as YAML + - `-o, --output-file ` - Write YAML to a file instead of stdout + - `--instance ` - Name of the instance, override the active instance ### jobs @@ -179,6 +209,17 @@ Pull completed and queued jobs from workspace - `jobs pull` - `jobs push` +### lint + +Validate Windmill flow, schedule, and trigger YAML files in a directory + +**Arguments:** `[directory:string]` + +**Options:** +- `--json` - Output results in JSON format +- `--fail-on-warn` - Exit with code 1 when warnings are emitted +- `--locks-required` - Fail if scripts or flow inline scripts that need locks have no locks + ### queues List all queues with their metrics @@ -193,18 +234,33 @@ List all queues with their metrics resource related commands +**Options:** +- `--json` - Output as JSON (for piping to jq) + **Subcommands:** +- `resource list` - list all resources + - `--json` - Output as JSON (for piping to jq) +- `resource get ` - get a resource's details + - `--json` - Output as JSON (for piping to jq) +- `resource new ` - create a new resource locally - `resource push ` - push a local resource spec. This overrides any remote versions. ### resource-type resource type related commands +**Options:** +- `--json` - Output as JSON (for piping to jq) + **Subcommands:** - `resource-type list` - list all resource types - `--schema` - Show schema in the output + - `--json` - Output as JSON (for piping to jq) +- `resource-type get ` - get a resource type's details + - `--json` - Output as JSON (for piping to jq) +- `resource-type new ` - create a new resource type locally - `resource-type push ` - 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 @@ -212,8 +268,16 @@ resource type related commands schedule related commands +**Options:** +- `--json` - Output as JSON (for piping to jq) + **Subcommands:** +- `schedule list` - list all schedules + - `--json` - Output as JSON (for piping to jq) +- `schedule get ` - get a schedule's details + - `--json` - Output as JSON (for piping to jq) +- `schedule new ` - create a new schedule locally - `schedule push ` - push a local schedule spec. This overrides any remote versions. ### script @@ -222,18 +286,27 @@ script related commands **Options:** - `--show-archived` - Enable archived scripts in output +- `--json` - Output as JSON (for piping to jq) **Subcommands:** +- `script list` - list all scripts + - `--show-archived` - Enable archived scripts in output + - `--json` - Output as JSON (for piping to jq) - `script push ` - push a local script spec. This overrides any remote versions. Use the script file (.ts, .js, .py, .sh -- `script show ` - show a scripts content +- `script get ` - get a script's details + - `--json` - Output as JSON (for piping to jq) +- `script show ` - show a script's content (alias for get - `script run ` - run a script by path - `-d --data ` - Inputs specified as a JSON string or a file using @ or stdin using @-. - `-s --silent` - Do not output anything other then the final output. Useful for scripting. - `script preview ` - preview a local script without deploying it. Supports both regular and codebase scripts. - `-d --data ` - Inputs specified as a JSON string or a file using @ or stdin using @-. - `-s --silent` - Do not output anything other than the final output. Useful for scripting. -- `script bootstrap ` - create a new script +- `script new ` - create a new script + - `--summary ` - script summary + - `--description ` - script description +- `script bootstrap ` - create a new script (alias for new - `--summary ` - script summary - `--description ` - 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` @@ -309,13 +382,25 @@ sync local with a remote workspaces or the opposite (push or pull) - `--parallel ` - Number of changes to process in parallel - `--repository ` - Specify repository path (e.g., u/user/repo) when multiple repositories exist - `--branch ` - Override the current git branch (works even outside a git repository) + - `--lint` - Run lint validation before pushing + - `--locks-required` - Fail if scripts or flow inline scripts that need locks have no locks ### trigger trigger related commands +**Options:** +- `--json` - Output as JSON (for piping to jq) + **Subcommands:** +- `trigger list` - list all triggers + - `--json` - Output as JSON (for piping to jq) +- `trigger get ` - get a trigger's details + - `--json` - Output as JSON (for piping to jq) + - `--kind ` - Trigger kind (http, websocket, kafka, nats, postgres, mqtt, sqs, gcp, email). Recommended for faster lookup +- `trigger new ` - create a new trigger locally + - `--kind ` - Trigger kind (required: http, websocket, kafka, nats, postgres, mqtt, sqs, gcp, email) - `trigger push ` - push a local trigger spec. This overrides any remote versions. ### user @@ -337,8 +422,16 @@ user related commands variable related commands +**Options:** +- `--json` - Output as JSON (for piping to jq) + **Subcommands:** +- `variable list` - list all variables + - `--json` - Output as JSON (for piping to jq) +- `variable get ` - get a variable's details + - `--json` - Output as JSON (for piping to jq) +- `variable new ` - create a new variable locally - `variable push ` - Push a local variable spec. This overrides any remote versions. - `--plain-secrets` - Push secrets as plain text - `variable add ` - Create a new variable on the remote. This will update the variable if it already exists. @@ -387,7 +480,8 @@ workspace related commands - `--create-username ` - Specify your own username in the newly created workspace. Ignored if --create is not specified, the workspace already exists or automatic username creation is enabled on the instance. - `workspace remove ` - Remove a workspace - `workspace whoami` - Show the currently active user -- `workspace list` - List workspaces on the remote server that you have access to +- `workspace list` - List local workspace profiles +- `workspace list-remote` - List workspaces on the remote server that you have access to - `workspace bind` - Bind the current Git branch to the active workspace - `--branch ` - Specify branch (defaults to current) - `workspace unbind` - Remove workspace binding from the current Git branch diff --git a/system_prompts/auto-generated/skills/cli-commands/SKILL.md b/system_prompts/auto-generated/skills/cli-commands/SKILL.md index c205b55f9f..e3903d7cc2 100644 --- a/system_prompts/auto-generated/skills/cli-commands/SKILL.md +++ b/system_prompts/auto-generated/skills/cli-commands/SKILL.md @@ -7,7 +7,7 @@ description: MUST use when using the CLI. The Windmill CLI (`wmill`) provides commands for managing scripts, flows, apps, and other resources. -Current version: 1.624.0 +Current version: 1.642.0 ## Global Options @@ -24,8 +24,15 @@ Current version: 1.624.0 app related commands +**Options:** +- `--json` - Output as JSON (for piping to jq) + **Subcommands:** +- `app list` - list all apps + - `--json` - Output as JSON (for piping to jq) +- `app get ` - get an app's details + - `--json` - Output as JSON (for piping to jq) - `app push ` - push a local app - `app dev [app_folder:string]` - Start a development server for building apps with live reload and hot module replacement - `--port ` - Port to run the dev server on (will find next available port if occupied) @@ -63,10 +70,16 @@ Launch a dev server that will spawn a webserver with HMR flow related commands **Options:** -- `--show-archived` - Enable archived scripts in output +- `--show-archived` - Enable archived flows in output +- `--json` - Output as JSON (for piping to jq) **Subcommands:** +- `flow list` - list all flows + - `--show-archived` - Enable archived flows in output + - `--json` - Output as JSON (for piping to jq) +- `flow get ` - get a flow's details + - `--json` - Output as JSON (for piping to jq) - `flow push ` - push a local flow spec. This overrides any remote versions. - `flow run ` - run a flow by path. - `-d --data ` - Inputs specified as a JSON string or a file using @ or stdin using @-. @@ -78,17 +91,31 @@ flow related commands - `--yes` - Skip confirmation prompt - `-i --includes ` - Comma separated patterns to specify which file to take into account (among files that are compatible with windmill). Patterns can include * (any string until '/') and ** (any string) - `-e --excludes ` - Comma separated patterns to specify which file to NOT take into account. -- `flow bootstrap ` - create a new empty flow - - `--summary ` - script summary - - `--description ` - script description +- `flow new ` - create a new empty flow + - `--summary ` - flow summary + - `--description ` - flow description +- `flow bootstrap ` - create a new empty flow (alias for new + - `--summary ` - flow summary + - `--description ` - flow description ### folder folder related commands +**Options:** +- `--json` - Output as JSON (for piping to jq) + **Subcommands:** -- `folder push ` - push a local folder spec. This overrides any remote versions. +- `folder list` - list all folders + - `--json` - Output as JSON (for piping to jq) +- `folder get ` - get a folder's details + - `--json` - Output as JSON (for piping to jq) +- `folder new ` - create a new folder locally + - `--summary ` - folder summary +- `folder push ` - push a local folder to the remote by name. This overrides any remote versions. +- `folder add-missing` - create default folder.meta.yaml for all subdirectories of f/ that are missing one + - `-y, --yes` - skip confirmation prompt ### gitsync-settings @@ -167,6 +194,9 @@ sync local with a remote instance or the opposite (push or pull) - `--prefix ` - Prefix of the local workspaces folders to push - `--prefix-settings` - Store instance yamls inside prefixed folders when using --prefix and --folder-per-instance - `instance whoami` - Display information about the currently logged-in user +- `instance get-config` - Dump the current instance config (global settings + worker configs) as YAML + - `-o, --output-file ` - Write YAML to a file instead of stdout + - `--instance ` - Name of the instance, override the active instance ### jobs @@ -184,6 +214,17 @@ Pull completed and queued jobs from workspace - `jobs pull` - `jobs push` +### lint + +Validate Windmill flow, schedule, and trigger YAML files in a directory + +**Arguments:** `[directory:string]` + +**Options:** +- `--json` - Output results in JSON format +- `--fail-on-warn` - Exit with code 1 when warnings are emitted +- `--locks-required` - Fail if scripts or flow inline scripts that need locks have no locks + ### queues List all queues with their metrics @@ -198,18 +239,33 @@ List all queues with their metrics resource related commands +**Options:** +- `--json` - Output as JSON (for piping to jq) + **Subcommands:** +- `resource list` - list all resources + - `--json` - Output as JSON (for piping to jq) +- `resource get ` - get a resource's details + - `--json` - Output as JSON (for piping to jq) +- `resource new ` - create a new resource locally - `resource push ` - push a local resource spec. This overrides any remote versions. ### resource-type resource type related commands +**Options:** +- `--json` - Output as JSON (for piping to jq) + **Subcommands:** - `resource-type list` - list all resource types - `--schema` - Show schema in the output + - `--json` - Output as JSON (for piping to jq) +- `resource-type get ` - get a resource type's details + - `--json` - Output as JSON (for piping to jq) +- `resource-type new ` - create a new resource type locally - `resource-type push ` - 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 @@ -217,8 +273,16 @@ resource type related commands schedule related commands +**Options:** +- `--json` - Output as JSON (for piping to jq) + **Subcommands:** +- `schedule list` - list all schedules + - `--json` - Output as JSON (for piping to jq) +- `schedule get ` - get a schedule's details + - `--json` - Output as JSON (for piping to jq) +- `schedule new ` - create a new schedule locally - `schedule push ` - push a local schedule spec. This overrides any remote versions. ### script @@ -227,18 +291,27 @@ script related commands **Options:** - `--show-archived` - Enable archived scripts in output +- `--json` - Output as JSON (for piping to jq) **Subcommands:** +- `script list` - list all scripts + - `--show-archived` - Enable archived scripts in output + - `--json` - Output as JSON (for piping to jq) - `script push ` - push a local script spec. This overrides any remote versions. Use the script file (.ts, .js, .py, .sh -- `script show ` - show a scripts content +- `script get ` - get a script's details + - `--json` - Output as JSON (for piping to jq) +- `script show ` - show a script's content (alias for get - `script run ` - run a script by path - `-d --data ` - Inputs specified as a JSON string or a file using @ or stdin using @-. - `-s --silent` - Do not output anything other then the final output. Useful for scripting. - `script preview ` - preview a local script without deploying it. Supports both regular and codebase scripts. - `-d --data ` - Inputs specified as a JSON string or a file using @ or stdin using @-. - `-s --silent` - Do not output anything other than the final output. Useful for scripting. -- `script bootstrap ` - create a new script +- `script new ` - create a new script + - `--summary ` - script summary + - `--description ` - script description +- `script bootstrap ` - create a new script (alias for new - `--summary ` - script summary - `--description ` - 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` @@ -314,13 +387,25 @@ sync local with a remote workspaces or the opposite (push or pull) - `--parallel ` - Number of changes to process in parallel - `--repository ` - Specify repository path (e.g., u/user/repo) when multiple repositories exist - `--branch ` - Override the current git branch (works even outside a git repository) + - `--lint` - Run lint validation before pushing + - `--locks-required` - Fail if scripts or flow inline scripts that need locks have no locks ### trigger trigger related commands +**Options:** +- `--json` - Output as JSON (for piping to jq) + **Subcommands:** +- `trigger list` - list all triggers + - `--json` - Output as JSON (for piping to jq) +- `trigger get ` - get a trigger's details + - `--json` - Output as JSON (for piping to jq) + - `--kind ` - Trigger kind (http, websocket, kafka, nats, postgres, mqtt, sqs, gcp, email). Recommended for faster lookup +- `trigger new ` - create a new trigger locally + - `--kind ` - Trigger kind (required: http, websocket, kafka, nats, postgres, mqtt, sqs, gcp, email) - `trigger push ` - push a local trigger spec. This overrides any remote versions. ### user @@ -342,8 +427,16 @@ user related commands variable related commands +**Options:** +- `--json` - Output as JSON (for piping to jq) + **Subcommands:** +- `variable list` - list all variables + - `--json` - Output as JSON (for piping to jq) +- `variable get ` - get a variable's details + - `--json` - Output as JSON (for piping to jq) +- `variable new ` - create a new variable locally - `variable push ` - Push a local variable spec. This overrides any remote versions. - `--plain-secrets` - Push secrets as plain text - `variable add ` - Create a new variable on the remote. This will update the variable if it already exists. @@ -392,7 +485,8 @@ workspace related commands - `--create-username ` - Specify your own username in the newly created workspace. Ignored if --create is not specified, the workspace already exists or automatic username creation is enabled on the instance. - `workspace remove ` - Remove a workspace - `workspace whoami` - Show the currently active user -- `workspace list` - List workspaces on the remote server that you have access to +- `workspace list` - List local workspace profiles +- `workspace list-remote` - List workspaces on the remote server that you have access to - `workspace bind` - Bind the current Git branch to the active workspace - `--branch ` - Specify branch (defaults to current) - `workspace unbind` - Remove workspace binding from the current Git branch diff --git a/windmill-yaml-validator/README.md b/windmill-yaml-validator/README.md index 6a693c8200..e928b4e3b1 100644 --- a/windmill-yaml-validator/README.md +++ b/windmill-yaml-validator/README.md @@ -188,40 +188,16 @@ npm test:watch ### Testing locally with the CLI -The Windmill CLI (`cli/`) is Deno-based and imports this package via `npm:windmill-yaml-validator@1.1.0`. Since Deno's `npm:` specifier always resolves from the npm registry, local testing requires a compatibility script that makes the TypeScript sources directly importable by Deno. - -The `deno-compat.sh` script handles two Deno requirements: -- Adding `.ts` extensions to relative imports -- Adding `with { type: "json" }` assertions to JSON imports - -**Steps:** - -1. Apply Deno compatibility: +To test local changes before publishing, use `npm link`: ```bash -./deno-compat.sh -``` +# In windmill-yaml-validator/ +npm run build +npm link -2. Add the following entries to `cli/deno.json` imports: - -```json -"npm:windmill-yaml-validator@1.1.0": "../windmill-yaml-validator/src/index.ts", -"ajv": "npm:ajv@^8.17.1", -"@stoplight/yaml": "npm:@stoplight/yaml@^4.3.0" -``` - -3. Run the CLI directly with Deno: - -```bash -cd ../cli -deno run -A src/main.ts lint -``` - -4. When done, restore everything: - -```bash -./deno-compat.sh -r # restore original imports -# Remove the 3 import map lines from cli/deno.json +# In cli/ +npm link windmill-yaml-validator +bun run src/main.ts lint ``` ### Schema Generation diff --git a/windmill-yaml-validator/deno-compat.sh b/windmill-yaml-validator/deno-compat.sh deleted file mode 100755 index 154fe72d0b..0000000000 --- a/windmill-yaml-validator/deno-compat.sh +++ /dev/null @@ -1,55 +0,0 @@ -#!/usr/bin/env bash - -# Makes windmill-yaml-validator source files Deno-compatible by: -# 1. Adding .ts extensions to relative imports -# 2. Adding `with { type: "json" }` to JSON imports -# Use -r to restore (undo changes). - -set -e -script_dirpath="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" - -RESTORE_MODE=false -while [[ $# -gt 0 ]]; do - case $1 in - -r) - RESTORE_MODE=true - shift - ;; - *) - echo "Unknown option: $1" - echo "Usage: $0 [-r]" - echo " -r Restore original imports" - exit 1 - ;; - esac -done - -if [[ "$OSTYPE" == "darwin"* ]]; then - SED=gsed - if ! command -v gsed &> /dev/null; then - echo "Error: gsed not found. Run: brew install gnu-sed" - exit 1 - fi -else - SED=sed -fi - -if [[ "$RESTORE_MODE" == true ]]; then - echo "Restoring original imports..." - find "$script_dirpath"/src -name "*.ts" -type f ! -path '*__tests__*' | while read -r file; do - # Remove .ts from relative imports: from "./foo.ts" -> from "./foo" - $SED -E -i 's|(from "\.\.?/[^"]*)\.ts(")|\1\2|g' "$file" - # Remove ` with { type: "json" }` from JSON imports - $SED -E -i 's/ with \{ type: "json" \}//' "$file" - done - echo "✓ Restored original imports" -else - echo "Making sources Deno-compatible..." - find "$script_dirpath"/src -name "*.ts" -type f ! -path '*__tests__*' | while read -r file; do - # Add .ts to relative imports that don't already end in .ts or .json - $SED -E -i '/\.json"/! { /\.ts"/! s|(from "(\.\.?/[^"]*[^/]))"(;?)$|\1.ts"\3|; }' "$file" - # Add `with { type: "json" }` to .json imports that don't already have it - $SED -E -i '/with \{ type: "json" \}/! s/(from "[^"]*\.json")(;?)$/\1 with { type: "json" }\2/' "$file" - done - echo "✓ Sources are now Deno-compatible" -fi