From 65f3c5ee5cf0da6a22f770763cf77d5a7bff830d Mon Sep 17 00:00:00 2001 From: Alexander Petric Date: Wed, 28 Oct 2026 14:22:50 -0400 Subject: [PATCH] fix(cli): flow preview hub steps and windmill paths, trigger templates (#12075) * fix(cli): drop deprecated and unused fields from trigger new templates Co-Authored-By: Claude Opus 5.5 * fix(cli): flow preview takes the windmill path and runs hub steps with --step Co-Authored-By: Claude Opus 5.5 * fix(cli): run a deployed script step with its deployed lock in flow preview --step Co-Authored-By: Claude Opus 5.5 * fix(cli): flow preview --remote honors nonDottedPaths when resolving the folder Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Claude Opus 5.5 --- cli/src/commands/flow/flow.ts | 88 +++++++---- cli/src/commands/trigger/trigger.ts | 26 ++-- cli/src/guidance/skills.gen.ts | 2 +- cli/test/flow_preview_unit.test.ts | 137 ++++++++++++++++++ cli/test/trigger_templates_unit.test.ts | 44 ++++++ .../auto-generated/cli/cli-commands.md | 2 +- system_prompts/auto-generated/prompts.ts | 2 +- .../skills/cli-commands/SKILL.md | 2 +- 8 files changed, 255 insertions(+), 48 deletions(-) create mode 100644 cli/test/flow_preview_unit.test.ts create mode 100644 cli/test/trigger_templates_unit.test.ts diff --git a/cli/src/commands/flow/flow.ts b/cli/src/commands/flow/flow.ts index 98339865be..956f2ec5db 100644 --- a/cli/src/commands/flow/flow.ts +++ b/cli/src/commands/flow/flow.ts @@ -9,7 +9,7 @@ import { stringify as yamlStringify } from "yaml"; import { yamlParseFile } from "../../utils/yaml.ts"; import { readTextFile, validateRequiredArgs } from "../../utils/utils.ts"; import * as wmill from "../../../gen/services.gen.ts"; -import { mkdirSync, writeFileSync } from "node:fs"; +import { existsSync, mkdirSync, statSync, writeFileSync } from "node:fs"; import { buildFolderPath, getMetadataFileName, loadNonDottedPathsSetting } from "../../utils/resource_folders.ts"; import { requireLogin } from "../../core/auth.ts"; @@ -651,6 +651,10 @@ async function preview( const cwdBeforeConfig = process.cwd(); if (useLocalPathScripts) { opts = await mergeConfigWithConfigFile(opts); + } else { + // --remote skips the config read, but resolving the flow folder still + // needs the configured suffix (nonDottedPaths). + await loadNonDottedPathsSetting(); } const workspace = await resolveWorkspace(opts); await requireLogin(opts); @@ -660,26 +664,7 @@ async function preview( const argPath = flowPath; flowPath = toSyncRootRelativePath(flowPath, cwdBeforeConfig); - // Normalize path - ensure it's a directory path to a .flow or __flow folder - const isFlowDir = flowPath.endsWith(".flow") || flowPath.endsWith(".flow" + SEP) - || flowPath.endsWith("__flow") || flowPath.endsWith("__flow" + SEP); - if (!isFlowDir) { - // Check if it's a flow.yaml file - if (flowPath.endsWith("flow.yaml") || flowPath.endsWith("flow.json")) { - // Use dirname so a bare "flow.yaml" (no parent dir) becomes "." - // instead of "" — the latter, after appending SEP below, becomes "/" - // and silently reads from filesystem root. - flowPath = dirname(flowPath); - } else { - throw new Error( - "Flow path must be a .flow/__flow directory or a flow.yaml file" - ); - } - } - - if (!flowPath.endsWith(SEP)) { - flowPath += SEP; - } + flowPath = resolveFlowFolderArg(flowPath); // The flow's windmill path (e.g. "f/cli_smoke/myrelflow"). It is what the // preview job runs under, and the anchor for relative-import resolution: @@ -852,20 +837,13 @@ async function previewStep( // Falls through here only when the deployed PathScript is what we want — // either --remote was passed, or no local file exists for this path. log.info(colors.yellow(`Previewing step '${stepId}' (script ${moduleValue.path})...`)); - const script = moduleValue.hash - ? await wmill.getScriptByHash({ - workspace: workspace.workspaceId, - hash: moduleValue.hash, - }) - : await wmill.getScriptByPath({ - workspace: workspace.workspaceId, - path: moduleValue.path, - }); + const script = await loadStepScript(workspace.workspaceId, moduleValue); jobId = await wmill.runScriptPreview({ workspace: workspace.workspaceId, requestBody: { content: script.content, language: script.language as any, + lock: script.lock, // Anchor to the script's own deployed path so its relative imports // resolve against the workspace tree (or temp_script_refs). path: moduleValue.path, @@ -916,6 +894,54 @@ async function previewStep( } } +/** + * Resolve a `flow preview` argument to its local flow folder, with a trailing + * separator. Accepts the folder itself, its flow.yaml, or the flow's Windmill + * path (`f/x/my_flow`), which other commands take without the folder suffix. + */ +export function resolveFlowFolderArg(flowPath: string): string { + const withSep = (p: string) => (p.endsWith(SEP) ? p : p + SEP); + const isFlowDir = flowPath.endsWith(".flow") || flowPath.endsWith(".flow" + SEP) + || flowPath.endsWith("__flow") || flowPath.endsWith("__flow" + SEP); + if (isFlowDir) { + return withSep(flowPath); + } + if (flowPath.endsWith("flow.yaml") || flowPath.endsWith("flow.json")) { + // dirname, so a bare "flow.yaml" becomes "." rather than "", which would + // turn into "/" and read from the filesystem root. + return withSep(dirname(flowPath)); + } + const bare = flowPath.endsWith(SEP) ? flowPath.slice(0, -SEP.length) : flowPath; + // The configured suffix first, then the other one: a workspace can hold both. + const candidates = [...new Set([buildFolderPath(bare, "flow"), bare + ".flow", bare + "__flow"])]; + const found = candidates.find((c) => existsSync(c) && statSync(c).isDirectory()); + if (found) { + return withSep(found); + } + throw new Error( + `Flow path must be a .flow/__flow directory, a flow.yaml file, or the Windmill path of a local flow; found none of ${candidates.join(", ")}` + ); +} + +/** + * Fetch the code and lockfile a `script` step runs, the way a whole-flow run + * resolves them: hub paths (`hub//...`) from the hub, others from the + * workspace. + */ +export async function loadStepScript( + workspaceId: string, + moduleValue: { path: string; hash?: string }, +): Promise<{ content: string; language: string; lock?: string }> { + if (moduleValue.path.startsWith("hub/")) { + const hub = await wmill.getHubScriptByPath({ path: moduleValue.path }); + return { content: hub.content, language: hub.language, lock: hub.lockfile }; + } + const script = moduleValue.hash + ? await wmill.getScriptByHash({ workspace: workspaceId, hash: moduleValue.hash }) + : await wmill.getScriptByPath({ workspace: workspaceId, path: moduleValue.path }); + return { content: script.content, language: script.language, lock: script.lock }; +} + // Strip the `.flow`/`__flow` directory suffix to recover the flow's logical // Windmill path. Workspaces with nonDottedPaths use `__flow`; the default // uses `.flow`. A previous version used `indexOf(".flow")` which returned -1 @@ -1204,7 +1230,7 @@ const command = new Command() .action(run as any) .command( "preview", - "preview a local flow without deploying it. Runs the flow definition from local files and uses local PathScripts by default. Pass --step to run only one module in isolation (resolves nested steps inside branchone/branchall/forloopflow/whileloopflow plus the special preprocessor/failure modules; supported step types: rawscript, script, flow)." + "preview a local flow without deploying it. is the flow's Windmill path (f/x/my_flow), its .flow/__flow folder or its flow.yaml. Runs the flow definition from local files and uses local PathScripts by default. Pass --step to run only one module in isolation (resolves nested steps inside branchone/branchall/forloopflow/whileloopflow plus the special preprocessor/failure modules; supported step types: rawscript, script, flow)." ) .arguments("") .option( diff --git a/cli/src/commands/trigger/trigger.ts b/cli/src/commands/trigger/trigger.ts index b46897b9b5..495249b020 100644 --- a/cli/src/commands/trigger/trigger.ts +++ b/cli/src/commands/trigger/trigger.ts @@ -338,14 +338,14 @@ export async function pushNativeTrigger( } } -const triggerTemplates: Record> = { +// Scaffold only fields the backend reads: `mode` supersedes the deprecated `enabled`, and +// `request_type` the deprecated `is_async`. +export const triggerTemplates: Record> = { http: { script_path: "", is_flow: false, route_path: "", http_method: "get", - is_async: false, - requires_auth: true, request_type: "sync", authentication_method: "none", is_static_website: false, @@ -360,7 +360,7 @@ const triggerTemplates: Record> = { filters: [], can_return_message: false, can_return_error_result: false, - enabled: false, + mode: "disabled", }, kafka: { script_path: "", @@ -369,7 +369,7 @@ const triggerTemplates: Record> = { group_id: "", topics: [], filters: [], - enabled: false, + mode: "disabled", }, nats: { script_path: "", @@ -377,7 +377,7 @@ const triggerTemplates: Record> = { nats_resource_path: "", subjects: [], use_jetstream: false, - enabled: false, + mode: "disabled", }, postgres: { script_path: "", @@ -385,21 +385,21 @@ const triggerTemplates: Record> = { postgres_resource_path: "", publication_name: "", replication_slot_name: "", - enabled: false, + mode: "disabled", }, mqtt: { script_path: "", is_flow: false, mqtt_resource_path: "", subscribe_topics: [], - enabled: false, + mode: "disabled", }, amqp: { script_path: "", is_flow: false, amqp_resource_path: "", queue_name: "", - enabled: false, + mode: "disabled", }, sqs: { script_path: "", @@ -407,7 +407,7 @@ const triggerTemplates: Record> = { queue_url: "", aws_resource_path: "", aws_auth_resource_type: "credentials", - enabled: false, + mode: "disabled", }, gcp: { script_path: "", @@ -417,7 +417,7 @@ const triggerTemplates: Record> = { subscription_id: "", delivery_type: "pull", subscription_mode: "create_update", - enabled: false, + mode: "disabled", }, azure: { script_path: "", @@ -426,13 +426,13 @@ const triggerTemplates: Record> = { azure_mode: "namespace_pull", scope_resource_id: "", subscription_name: "", - enabled: false, + mode: "disabled", }, email: { script_path: "", is_flow: false, local_part: "", - enabled: false, + mode: "disabled", }, }; diff --git a/cli/src/guidance/skills.gen.ts b/cli/src/guidance/skills.gen.ts index 35845437d9..e9eef86056 100644 --- a/cli/src/guidance/skills.gen.ts +++ b/cli/src/guidance/skills.gen.ts @@ -8718,7 +8718,7 @@ flow related commands - \`-d --data \` - Inputs specified as a JSON string or a file using @ or stdin using @-. A resource argument is the bare string $res: as its whole value, and a variable argument is the bare string $var: — not an object wrapper keyed on $res/$var, and not a plain path. - \`-s --silent\` - Do not ouput anything other then the final output. Useful for scripting. - \`--tag \` - Override the worker tag the run is dispatched to (e.g. to route it to dev workers instead of the flow's default tag). -- \`flow preview \` - preview a local flow without deploying it. Runs the flow definition from local files and uses local PathScripts by default. Pass --step to run only one module in isolation (resolves nested steps inside branchone/branchall/forloopflow/whileloopflow plus the special preprocessor/failure modules; supported step types: rawscript, script, flow). +- \`flow preview \` - preview a local flow without deploying it. is the flow's Windmill path (f/x/my_flow), its .flow/__flow folder or its flow.yaml. Runs the flow definition from local files and uses local PathScripts by default. Pass --step to run only one module in isolation (resolves nested steps inside branchone/branchall/forloopflow/whileloopflow plus the special preprocessor/failure modules; supported step types: rawscript, script, flow). - \`-d --data \` - Inputs specified as a JSON string or a file using @ or stdin using @-. A resource argument is the bare string $res: as its whole value, and a variable argument is the bare string $var: — not an object wrapper keyed on $res/$var, and not a plain path. - \`-s --silent\` - Do not output anything other then the final output. Useful for scripting. - \`--remote\` - Use deployed workspace scripts for PathScript steps instead of local files. diff --git a/cli/test/flow_preview_unit.test.ts b/cli/test/flow_preview_unit.test.ts new file mode 100644 index 0000000000..0105ea2e71 --- /dev/null +++ b/cli/test/flow_preview_unit.test.ts @@ -0,0 +1,137 @@ +/** + * `wmill flow preview` argument resolution and `--step` script loading. + */ +import { expect, test, describe, beforeEach, afterEach } from "bun:test"; +import * as fs from "node:fs"; +import * as path from "node:path"; +import * as os from "node:os"; +import { mockServices } from "./mock_services.ts"; + +const calls: { fn: string; args: any }[] = []; +mockServices({ + getHubScriptByPath: async (args: any) => { + calls.push({ fn: "getHubScriptByPath", args }); + return { + content: "export async function main() { return 'hub' }", + language: "deno", + lockfile: "hub-lock", + }; + }, + getScriptByPath: async (args: any) => { + calls.push({ fn: "getScriptByPath", args }); + return { content: "workspace code", language: "bun", lock: "ws-lock" }; + }, + getScriptByHash: async (args: any) => { + calls.push({ fn: "getScriptByHash", args }); + return { content: "hashed code", language: "python3", lock: "hash-lock" }; + }, +}); + +const { resolveFlowFolderArg, loadStepScript } = await import( + "../src/commands/flow/flow.ts" +); +const { setNonDottedPaths } = await import("../src/utils/resource_folders.ts"); + +const SEP = path.sep; + +describe("resolveFlowFolderArg", () => { + let root: string; + let previousCwd: string; + + beforeEach(() => { + previousCwd = process.cwd(); + root = fs.realpathSync( + fs.mkdtempSync(path.join(os.tmpdir(), "wmill_flow_preview_")), + ); + fs.mkdirSync(path.join(root, "f", "t", "dotted.flow"), { recursive: true }); + fs.mkdirSync(path.join(root, "f", "t", "nondotted__flow"), { recursive: true }); + process.chdir(root); + }); + + afterEach(() => { + process.chdir(previousCwd); + fs.rmSync(root, { recursive: true, force: true }); + }); + + test("a Windmill path resolves to its local .flow folder", () => { + expect(resolveFlowFolderArg(`f${SEP}t${SEP}dotted`)).toBe( + `f${SEP}t${SEP}dotted.flow${SEP}`, + ); + }); + + test("a Windmill path resolves to a __flow folder when that is the one on disk", () => { + expect(resolveFlowFolderArg(`f${SEP}t${SEP}nondotted`)).toBe( + `f${SEP}t${SEP}nondotted__flow${SEP}`, + ); + }); + + test("when both folders exist, the configured suffix wins", () => { + fs.mkdirSync(path.join(root, "f", "t", "both.flow"), { recursive: true }); + fs.mkdirSync(path.join(root, "f", "t", "both__flow"), { recursive: true }); + try { + setNonDottedPaths(true); + expect(resolveFlowFolderArg(`f${SEP}t${SEP}both`)).toBe( + `f${SEP}t${SEP}both__flow${SEP}`, + ); + setNonDottedPaths(false); + expect(resolveFlowFolderArg(`f${SEP}t${SEP}both`)).toBe( + `f${SEP}t${SEP}both.flow${SEP}`, + ); + } finally { + setNonDottedPaths(false); + } + }); + + test("the folder and its flow.yaml keep resolving to the folder", () => { + const folder = `f${SEP}t${SEP}dotted.flow${SEP}`; + expect(resolveFlowFolderArg(`f${SEP}t${SEP}dotted.flow`)).toBe(folder); + expect(resolveFlowFolderArg(folder)).toBe(folder); + expect(resolveFlowFolderArg(`${folder}flow.yaml`)).toBe(folder); + expect(resolveFlowFolderArg("flow.yaml")).toBe(`.${SEP}`); + }); + + test("a Windmill path with no local flow folder is refused, naming what was looked for", () => { + expect(() => resolveFlowFolderArg(`f${SEP}t${SEP}missing`)).toThrow( + `found none of f${SEP}t${SEP}missing.flow, f${SEP}t${SEP}missing__flow`, + ); + }); +}); + +describe("loadStepScript", () => { + beforeEach(() => { + calls.length = 0; + }); + + test("a hub path is fetched from the hub, with the hub's lockfile", async () => { + const script = await loadStepScript("ws", { + path: "hub/29624/slack/send_message_to_channel", + }); + expect(calls).toEqual([ + { + fn: "getHubScriptByPath", + args: { path: "hub/29624/slack/send_message_to_channel" }, + }, + ]); + expect(script).toEqual({ + content: "export async function main() { return 'hub' }", + language: "deno", + lock: "hub-lock", + }); + }); + + test("a workspace path is fetched from the workspace, with its deployed lock", async () => { + const script = await loadStepScript("ws", { path: "f/t/helper" }); + expect(calls).toEqual([ + { fn: "getScriptByPath", args: { workspace: "ws", path: "f/t/helper" } }, + ]); + expect(script).toEqual({ content: "workspace code", language: "bun", lock: "ws-lock" }); + }); + + test("a pinned hash is fetched by hash", async () => { + const script = await loadStepScript("ws", { path: "f/t/helper", hash: "abc" }); + expect(calls).toEqual([ + { fn: "getScriptByHash", args: { workspace: "ws", hash: "abc" } }, + ]); + expect(script).toEqual({ content: "hashed code", language: "python3", lock: "hash-lock" }); + }); +}); diff --git a/cli/test/trigger_templates_unit.test.ts b/cli/test/trigger_templates_unit.test.ts new file mode 100644 index 0000000000..736fc7e010 --- /dev/null +++ b/cli/test/trigger_templates_unit.test.ts @@ -0,0 +1,44 @@ +/** + * `wmill trigger new` scaffolds only fields the backend reads: `request_type` + * rather than the deprecated `is_async`, no `requires_auth` (its column was + * renamed to `authentication_method`), and `mode` rather than the deprecated + * `enabled`. + */ +import { expect, test, describe } from "bun:test"; +import { triggerTemplates } from "../src/commands/trigger/trigger.ts"; + +describe("trigger new templates", () => { + test("the http template has no is_async or requires_auth", () => { + const http = triggerTemplates.http; + expect(Object.keys(http)).not.toContain("is_async"); + expect(Object.keys(http)).not.toContain("requires_auth"); + expect(http.request_type).toBe("sync"); + expect(http.authentication_method).toBe("none"); + }); + + test("no template scaffolds the deprecated enabled field", () => { + const withEnabled = Object.entries(triggerTemplates) + .filter(([, template]) => "enabled" in template) + .map(([kind]) => kind); + expect(withEnabled).toEqual([]); + }); + + test("templates that scaffolded a disabled trigger still do, through mode", () => { + const modes = Object.fromEntries( + Object.entries(triggerTemplates).map(([kind, t]) => [kind, t.mode]), + ); + expect(modes).toEqual({ + http: undefined, + websocket: "disabled", + kafka: "disabled", + nats: "disabled", + postgres: "disabled", + mqtt: "disabled", + amqp: "disabled", + sqs: "disabled", + gcp: "disabled", + azure: "disabled", + email: "disabled", + }); + }); +}); diff --git a/system_prompts/auto-generated/cli/cli-commands.md b/system_prompts/auto-generated/cli/cli-commands.md index a40abaf411..892c0fabc5 100644 --- a/system_prompts/auto-generated/cli/cli-commands.md +++ b/system_prompts/auto-generated/cli/cli-commands.md @@ -177,7 +177,7 @@ flow related commands - `-d --data ` - Inputs specified as a JSON string or a file using @ or stdin using @-. A resource argument is the bare string $res: as its whole value, and a variable argument is the bare string $var: — not an object wrapper keyed on $res/$var, and not a plain path. - `-s --silent` - Do not ouput anything other then the final output. Useful for scripting. - `--tag ` - Override the worker tag the run is dispatched to (e.g. to route it to dev workers instead of the flow's default tag). -- `flow preview ` - preview a local flow without deploying it. Runs the flow definition from local files and uses local PathScripts by default. Pass --step to run only one module in isolation (resolves nested steps inside branchone/branchall/forloopflow/whileloopflow plus the special preprocessor/failure modules; supported step types: rawscript, script, flow). +- `flow preview ` - preview a local flow without deploying it. is the flow's Windmill path (f/x/my_flow), its .flow/__flow folder or its flow.yaml. Runs the flow definition from local files and uses local PathScripts by default. Pass --step to run only one module in isolation (resolves nested steps inside branchone/branchall/forloopflow/whileloopflow plus the special preprocessor/failure modules; supported step types: rawscript, script, flow). - `-d --data ` - Inputs specified as a JSON string or a file using @ or stdin using @-. A resource argument is the bare string $res: as its whole value, and a variable argument is the bare string $var: — not an object wrapper keyed on $res/$var, and not a plain path. - `-s --silent` - Do not output anything other then the final output. Useful for scripting. - `--remote` - Use deployed workspace scripts for PathScript steps instead of local files. diff --git a/system_prompts/auto-generated/prompts.ts b/system_prompts/auto-generated/prompts.ts index cfdd54ef5b..f4f6c1516a 100644 --- a/system_prompts/auto-generated/prompts.ts +++ b/system_prompts/auto-generated/prompts.ts @@ -4048,7 +4048,7 @@ flow related commands - \`-d --data \` - Inputs specified as a JSON string or a file using @ or stdin using @-. A resource argument is the bare string $res: as its whole value, and a variable argument is the bare string $var: — not an object wrapper keyed on $res/$var, and not a plain path. - \`-s --silent\` - Do not ouput anything other then the final output. Useful for scripting. - \`--tag \` - Override the worker tag the run is dispatched to (e.g. to route it to dev workers instead of the flow's default tag). -- \`flow preview \` - preview a local flow without deploying it. Runs the flow definition from local files and uses local PathScripts by default. Pass --step to run only one module in isolation (resolves nested steps inside branchone/branchall/forloopflow/whileloopflow plus the special preprocessor/failure modules; supported step types: rawscript, script, flow). +- \`flow preview \` - preview a local flow without deploying it. is the flow's Windmill path (f/x/my_flow), its .flow/__flow folder or its flow.yaml. Runs the flow definition from local files and uses local PathScripts by default. Pass --step to run only one module in isolation (resolves nested steps inside branchone/branchall/forloopflow/whileloopflow plus the special preprocessor/failure modules; supported step types: rawscript, script, flow). - \`-d --data \` - Inputs specified as a JSON string or a file using @ or stdin using @-. A resource argument is the bare string $res: as its whole value, and a variable argument is the bare string $var: — not an object wrapper keyed on $res/$var, and not a plain path. - \`-s --silent\` - Do not output anything other then the final output. Useful for scripting. - \`--remote\` - Use deployed workspace scripts for PathScript steps instead of local files. diff --git a/system_prompts/auto-generated/skills/cli-commands/SKILL.md b/system_prompts/auto-generated/skills/cli-commands/SKILL.md index 7770d98ea9..1dd1adf793 100644 --- a/system_prompts/auto-generated/skills/cli-commands/SKILL.md +++ b/system_prompts/auto-generated/skills/cli-commands/SKILL.md @@ -182,7 +182,7 @@ flow related commands - `-d --data ` - Inputs specified as a JSON string or a file using @ or stdin using @-. A resource argument is the bare string $res: as its whole value, and a variable argument is the bare string $var: — not an object wrapper keyed on $res/$var, and not a plain path. - `-s --silent` - Do not ouput anything other then the final output. Useful for scripting. - `--tag ` - Override the worker tag the run is dispatched to (e.g. to route it to dev workers instead of the flow's default tag). -- `flow preview ` - preview a local flow without deploying it. Runs the flow definition from local files and uses local PathScripts by default. Pass --step to run only one module in isolation (resolves nested steps inside branchone/branchall/forloopflow/whileloopflow plus the special preprocessor/failure modules; supported step types: rawscript, script, flow). +- `flow preview ` - preview a local flow without deploying it. is the flow's Windmill path (f/x/my_flow), its .flow/__flow folder or its flow.yaml. Runs the flow definition from local files and uses local PathScripts by default. Pass --step to run only one module in isolation (resolves nested steps inside branchone/branchall/forloopflow/whileloopflow plus the special preprocessor/failure modules; supported step types: rawscript, script, flow). - `-d --data ` - Inputs specified as a JSON string or a file using @ or stdin using @-. A resource argument is the bare string $res: as its whole value, and a variable argument is the bare string $var: — not an object wrapper keyed on $res/$var, and not a plain path. - `-s --silent` - Do not output anything other then the final output. Useful for scripting. - `--remote` - Use deployed workspace scripts for PathScript steps instead of local files.