@withone/cli 1.47.8 → 1.47.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -12,7 +12,7 @@ import {
12
12
  stripStepsAlias,
13
13
  summarizeFlowInputs,
14
14
  walkSteps
15
- } from "./chunk-KWGN3RJR.js";
15
+ } from "./chunk-4AYHJFH3.js";
16
16
  import "./chunk-44CV5IMX.js";
17
17
  import "./chunk-K6MWE2ZH.js";
18
18
  export {
package/dist/index.js CHANGED
@@ -29,8 +29,9 @@ import {
29
29
  saveFlow,
30
30
  searchCachePath,
31
31
  validateActionInput,
32
+ walkSteps,
32
33
  writeCache
33
- } from "./chunk-KWGN3RJR.js";
34
+ } from "./chunk-4AYHJFH3.js";
34
35
  import {
35
36
  memSqlCommand
36
37
  } from "./chunk-QV3Y5N5G.js";
@@ -3597,7 +3598,7 @@ function validateOutputSchemas(flow2) {
3597
3598
  walkValue(v, `${pathPrefix}.${k}`);
3598
3599
  }
3599
3600
  }
3600
- function walkSteps(steps, pathPrefix) {
3601
+ function walkSteps2(steps, pathPrefix) {
3601
3602
  steps.forEach((step, i) => {
3602
3603
  const stepPath = `${pathPrefix}[${i}]`;
3603
3604
  if (step.if) checkText(step.if, `${stepPath}.if`);
@@ -3612,13 +3613,13 @@ function validateOutputSchemas(flow2) {
3612
3613
  for (const { configKey, fieldName } of nestedKeys) {
3613
3614
  const c = step[configKey];
3614
3615
  if (c && Array.isArray(c[fieldName])) {
3615
- walkSteps(c[fieldName], `${stepPath}.${configKey}.${fieldName}`);
3616
+ walkSteps2(c[fieldName], `${stepPath}.${configKey}.${fieldName}`);
3616
3617
  }
3617
3618
  }
3618
3619
  }
3619
3620
  });
3620
3621
  }
3621
- walkSteps(flow2.steps, "steps");
3622
+ walkSteps2(flow2.steps, "steps");
3622
3623
  return errors;
3623
3624
  }
3624
3625
  function validateOutputSchemaShape(schema, location) {
@@ -3652,6 +3653,7 @@ function validateFlow(flow2, rootDir) {
3652
3653
  ...validateStepIds(f),
3653
3654
  ...validateSelectorReferences(f, rootDir),
3654
3655
  ...validateOutputSchemas(f),
3656
+ ...validateFileReadSchemas(f),
3655
3657
  ...rootDir ? validateCodeModules(f, rootDir) : []
3656
3658
  ];
3657
3659
  }
@@ -3692,6 +3694,108 @@ function validateCodeModules(flow2, rootDir) {
3692
3694
  walk(flow2.steps, "steps");
3693
3695
  return errors;
3694
3696
  }
3697
+ var VALID_FILE_READ_SCHEMA_TYPES = /* @__PURE__ */ new Set(["string", "number", "boolean", "object", "array", "null", "unknown"]);
3698
+ function isPlainObject(v) {
3699
+ return !!v && typeof v === "object" && !Array.isArray(v);
3700
+ }
3701
+ function validateFileReadFieldRule(raw, fieldPath, errors) {
3702
+ if (typeof raw === "string") {
3703
+ if (!VALID_FILE_READ_SCHEMA_TYPES.has(raw)) {
3704
+ errors.push({ path: fieldPath, message: `unknown type "${raw}". Allowed: ${[...VALID_FILE_READ_SCHEMA_TYPES].join(", ")}.` });
3705
+ }
3706
+ return;
3707
+ }
3708
+ if (!isPlainObject(raw)) {
3709
+ errors.push({ path: fieldPath, message: 'field rule must be a type string or an object (e.g. { "type": "string", "required": true })' });
3710
+ return;
3711
+ }
3712
+ const rule = raw;
3713
+ if (rule.type !== void 0 && (typeof rule.type !== "string" || !VALID_FILE_READ_SCHEMA_TYPES.has(rule.type))) {
3714
+ errors.push({ path: `${fieldPath}.type`, message: `unknown type "${String(rule.type)}". Allowed: ${[...VALID_FILE_READ_SCHEMA_TYPES].join(", ")}.` });
3715
+ }
3716
+ if (rule.required !== void 0 && typeof rule.required !== "boolean") {
3717
+ errors.push({ path: `${fieldPath}.required`, message: '"required" must be a boolean' });
3718
+ }
3719
+ if (rule.enum !== void 0) {
3720
+ if (!Array.isArray(rule.enum) || rule.enum.length === 0) {
3721
+ errors.push({ path: `${fieldPath}.enum`, message: '"enum" must be a non-empty array of allowed values' });
3722
+ } else if (rule.enum.some((e) => e !== null && typeof e === "object")) {
3723
+ errors.push({ path: `${fieldPath}.enum`, message: '"enum" entries must be primitives (string/number/boolean) or null' });
3724
+ }
3725
+ }
3726
+ for (const k of ["minItems", "maxItems", "length"]) {
3727
+ const n = rule[k];
3728
+ if (n !== void 0 && (typeof n !== "number" || !Number.isInteger(n) || n < 0)) {
3729
+ errors.push({ path: `${fieldPath}.${k}`, message: `"${k}" must be a non-negative integer` });
3730
+ }
3731
+ }
3732
+ if (typeof rule.minItems === "number" && typeof rule.maxItems === "number" && rule.minItems > rule.maxItems) {
3733
+ errors.push({ path: `${fieldPath}.minItems`, message: '"minItems" must be <= "maxItems"' });
3734
+ }
3735
+ if (rule.length !== void 0 && (rule.minItems !== void 0 || rule.maxItems !== void 0)) {
3736
+ errors.push({ path: `${fieldPath}.length`, message: '"length" cannot be combined with "minItems"/"maxItems"' });
3737
+ }
3738
+ const hasArrayConstraint = rule.items !== void 0 || rule.minItems !== void 0 || rule.maxItems !== void 0 || rule.length !== void 0;
3739
+ if (hasArrayConstraint && rule.type !== "array") {
3740
+ errors.push({ path: `${fieldPath}.type`, message: '"items"/"minItems"/"maxItems"/"length" require "type": "array"' });
3741
+ }
3742
+ if (rule.items !== void 0) {
3743
+ if (typeof rule.items === "string") {
3744
+ if (!VALID_FILE_READ_SCHEMA_TYPES.has(rule.items)) {
3745
+ errors.push({ path: `${fieldPath}.items`, message: `unknown item type "${rule.items}". Allowed: ${[...VALID_FILE_READ_SCHEMA_TYPES].join(", ")}.` });
3746
+ }
3747
+ } else if (isPlainObject(rule.items)) {
3748
+ validateFileReadFieldRule(rule.items, `${fieldPath}.items`, errors);
3749
+ } else {
3750
+ errors.push({ path: `${fieldPath}.items`, message: '"items" must be a type string or a field rule object' });
3751
+ }
3752
+ }
3753
+ if (rule.properties !== void 0) {
3754
+ if (rule.type !== "object") {
3755
+ errors.push({ path: `${fieldPath}.properties`, message: '"properties" requires "type": "object"' });
3756
+ }
3757
+ errors.push(...validateFileReadSchemaShape(rule.properties, `${fieldPath}.properties`));
3758
+ }
3759
+ }
3760
+ function validateFileReadSchemaShape(schema, location) {
3761
+ const errors = [];
3762
+ if (!isPlainObject(schema)) {
3763
+ errors.push({ path: location, message: '"schema" must be an object mapping field names to type/rules' });
3764
+ return errors;
3765
+ }
3766
+ for (const [field, raw] of Object.entries(schema)) {
3767
+ validateFileReadFieldRule(raw, `${location}.${field}`, errors);
3768
+ }
3769
+ return errors;
3770
+ }
3771
+ function validateFileReadSchemas(flow2) {
3772
+ const errors = [];
3773
+ const nestedKeys = getNestedStepsKeys();
3774
+ function walk(steps, pathPrefix) {
3775
+ for (let i = 0; i < steps.length; i++) {
3776
+ const step = steps[i];
3777
+ const stepPath = `${pathPrefix}[${i}]`;
3778
+ if (step.type === "file-read" && step.fileRead && step.fileRead.schema !== void 0) {
3779
+ const fr = step.fileRead;
3780
+ if (fr.parseJson !== true) {
3781
+ errors.push({
3782
+ path: `${stepPath}.fileRead.schema`,
3783
+ message: "fileRead.schema is only checked when parseJson:true \u2014 set parseJson:true or remove schema."
3784
+ });
3785
+ }
3786
+ errors.push(...validateFileReadSchemaShape(fr.schema, `${stepPath}.fileRead.schema`));
3787
+ }
3788
+ for (const { configKey, fieldName } of nestedKeys) {
3789
+ const config2 = step[configKey];
3790
+ if (config2 && Array.isArray(config2[fieldName])) {
3791
+ walk(config2[fieldName], `${stepPath}.${configKey}.${fieldName}`);
3792
+ }
3793
+ }
3794
+ }
3795
+ }
3796
+ walk(flow2.steps, "steps");
3797
+ return errors;
3798
+ }
3695
3799
 
3696
3800
  // src/commands/flow.ts
3697
3801
  import fs5 from "fs";
@@ -3716,6 +3820,49 @@ async function writeFlowResultFile(filePath, meta, steps) {
3716
3820
  await done;
3717
3821
  return abs;
3718
3822
  }
3823
+ function previewValue(value, max = 120) {
3824
+ if (value === void 0) return "<undefined>";
3825
+ let str;
3826
+ try {
3827
+ str = typeof value === "string" ? JSON.stringify(value) : JSON.stringify(value) ?? String(value);
3828
+ } catch {
3829
+ str = String(value);
3830
+ }
3831
+ if (str === void 0) str = String(value);
3832
+ return str.length > max ? `${str.slice(0, max - 1)}\u2026` : str;
3833
+ }
3834
+ function renderDryRef(ref) {
3835
+ if (ref.status === "resolved") {
3836
+ return `${pc7.green("\u2713")} ${ref.selector} ${pc7.dim("\u2192")} ${previewValue(ref.value)}`;
3837
+ }
3838
+ if (ref.status === "deferred") {
3839
+ return `${pc7.dim("\u25CB")} ${ref.selector} ${pc7.dim("\u2192 pending (produced by a later step)")}`;
3840
+ }
3841
+ return `${pc7.yellow("!")} ${ref.selector} ${pc7.yellow("\u2192 unresolved \u2014 check the input/env name")}`;
3842
+ }
3843
+ function renderDryResolution(steps) {
3844
+ const isExpr = (t) => t === "transform" || t === "condition" || t === "while";
3845
+ for (const s of steps) {
3846
+ const label = s.name ? `${s.stepId} ${pc7.dim(`"${s.name}"`)}` : s.stepId;
3847
+ console.log(` ${pc7.cyan("\u25B8")} ${label} ${pc7.dim(`(${s.type})`)}`);
3848
+ if (s.error !== void 0) {
3849
+ console.log(` ${pc7.red("error")} ${s.error}`);
3850
+ } else if (isExpr(s.type) && s.deferred) {
3851
+ const deps = s.references.map((r) => r.selector).join(", ");
3852
+ console.log(` ${pc7.dim("\u25CB pending \u2014 depends on")} ${deps} ${pc7.dim("(produced by a later step)")}`);
3853
+ } else if (isExpr(s.type)) {
3854
+ console.log(` ${pc7.dim("=")} ${previewValue(s.resolved)}`);
3855
+ }
3856
+ if (!(isExpr(s.type) && s.deferred)) {
3857
+ for (const ref of s.references) {
3858
+ console.log(` ${renderDryRef(ref)}`);
3859
+ }
3860
+ }
3861
+ if (!isExpr(s.type) && s.references.length === 0 && s.error === void 0) {
3862
+ console.log(` ${pc7.dim("(no interpolations)")}`);
3863
+ }
3864
+ }
3865
+ }
3719
3866
  function getConfig2() {
3720
3867
  const apiKey = getApiKey();
3721
3868
  if (!apiKey) {
@@ -3870,6 +4017,20 @@ ${preflightErrors.map((e) => ` ${e.path}: ${e.message}`).join("\n")}`);
3870
4017
  }
3871
4018
  error(msg);
3872
4019
  }
4020
+ if (options.stopAfter) {
4021
+ const ids = /* @__PURE__ */ new Set();
4022
+ walkSteps(flow2.steps, (s) => {
4023
+ ids.add(s.id);
4024
+ });
4025
+ if (!ids.has(options.stopAfter)) {
4026
+ const msg = `--stop-after target "${options.stopAfter}" is not a step in workflow "${flow2.key}". Known step ids: ${[...ids].join(", ")}`;
4027
+ if (isAgentMode()) {
4028
+ json({ error: msg, unknownStopAfter: options.stopAfter, stepIds: [...ids] });
4029
+ process.exit(1);
4030
+ }
4031
+ error(msg);
4032
+ }
4033
+ }
3873
4034
  const inputs = parseInputs(options.input || []);
3874
4035
  const resolvedInputs = await autoResolveConnectionInputs(flow2, inputs, api);
3875
4036
  const runner = new FlowRunner(flow2, resolvedInputs);
@@ -3883,7 +4044,17 @@ ${pc7.yellow("Pausing after current step completes...")} (run ID: ${runId})`);
3883
4044
  }
3884
4045
  };
3885
4046
  process.on("SIGINT", sigintHandler);
4047
+ let dryRunSteps;
4048
+ let dryResolveTarget;
4049
+ let stoppedAfter;
3886
4050
  const onEvent = (event) => {
4051
+ if (event.event === "flow:dry-run") {
4052
+ dryRunSteps = event.steps;
4053
+ } else if (event.event === "step:dry-resolve") {
4054
+ dryResolveTarget = event;
4055
+ } else if (event.event === "flow:stopped") {
4056
+ stoppedAfter = event.stoppedAfter;
4057
+ }
3887
4058
  if (isAgentMode()) {
3888
4059
  json(event);
3889
4060
  } else if (options.verbose) {
@@ -3909,23 +4080,46 @@ ${pc7.yellow("Pausing after current step completes...")} (run ID: ${runId})`);
3909
4080
  verbose: options.verbose,
3910
4081
  allowBash: options.allowBash,
3911
4082
  skipValidation: options.skipValidation,
4083
+ stopAfter: options.stopAfter,
3912
4084
  rootDir,
3913
4085
  onEvent
3914
4086
  });
3915
4087
  process.off("SIGINT", sigintHandler);
3916
4088
  if (!options.verbose && !isAgentMode()) {
3917
- execSpinner.stop("Workflow completed");
4089
+ execSpinner.stop(stoppedAfter ? "Workflow stopped" : dryRunSteps ? "Dry run complete" : "Workflow completed");
3918
4090
  }
3919
4091
  const resultFile = options.outputFile ? await writeFlowResultFile(options.outputFile, { runId, logFile: logPath, status: "success" }, context.steps) : void 0;
4092
+ const finalStatus = stoppedAfter ? "stopped" : "success";
3920
4093
  if (isAgentMode()) {
3921
- json(
3922
- resultFile ? { event: "workflow:result", runId, logFile: logPath, status: "success", outputFile: resultFile } : { event: "workflow:result", runId, logFile: logPath, status: "success", steps: context.steps }
3923
- );
4094
+ const envelope = {
4095
+ event: "workflow:result",
4096
+ runId,
4097
+ logFile: logPath,
4098
+ statePath: runner.getStatePath(),
4099
+ status: finalStatus
4100
+ };
4101
+ if (resultFile) envelope.outputFile = resultFile;
4102
+ else envelope.steps = context.steps;
4103
+ if (options.dryRun) envelope.dryRun = true;
4104
+ if (stoppedAfter) envelope.stoppedAfter = stoppedAfter;
4105
+ json(envelope);
3924
4106
  return;
3925
4107
  }
3926
4108
  if (resultFile) {
3927
4109
  note(`Full result written to ${resultFile}`, "Output");
3928
4110
  }
4111
+ if (dryRunSteps && !stoppedAfter) {
4112
+ console.log();
4113
+ renderDryResolution(dryRunSteps);
4114
+ console.log();
4115
+ const missing = dryRunSteps.reduce((n, s) => n + s.references.filter((r) => r.status === "missing").length, 0);
4116
+ note(
4117
+ `Dry run \u2014 no steps executed. Resolved ${dryRunSteps.length} step(s)` + (missing > 0 ? `; ${pc7.yellow(`${missing} unresolved input/env reference(s)`)}` : "") + `.
4118
+ ${pc7.dim("$.steps.* refs resolve at runtime \u2014 re-run with --stop-after=<stepId> to resolve them against real output.")}`,
4119
+ "Dry Run"
4120
+ );
4121
+ return;
4122
+ }
3929
4123
  const stepEntries = Object.entries(context.steps);
3930
4124
  const succeeded = stepEntries.filter(([, r]) => r.status === "success").length;
3931
4125
  const failed = stepEntries.filter(([, r]) => r.status === "failed").length;
@@ -3934,7 +4128,17 @@ ${pc7.yellow("Pausing after current step completes...")} (run ID: ${runId})`);
3934
4128
  console.log(` ${pc7.green("\u2713")} ${succeeded} succeeded ${failed > 0 ? pc7.red(`\u2717 ${failed} failed`) : ""} ${skipped > 0 ? pc7.dim(`\u25CB ${skipped} skipped`) : ""}`);
3935
4129
  console.log(` ${pc7.dim(`Run ID: ${runId}`)}`);
3936
4130
  console.log(` ${pc7.dim(`Log: ${logPath}`)}`);
3937
- if (options.dryRun) {
4131
+ if (dryResolveTarget) {
4132
+ console.log();
4133
+ console.log(` ${pc7.dim(`Resolved (not executed) "${dryResolveTarget.stepId}":`)}`);
4134
+ renderDryResolution([dryResolveTarget]);
4135
+ }
4136
+ if (stoppedAfter) {
4137
+ note(
4138
+ `Stopped after step "${stoppedAfter}". Inspect step outputs with: ${pc7.cyan(`one flow inspect ${runId}`)}`,
4139
+ "Stopped"
4140
+ );
4141
+ } else if (options.dryRun) {
3938
4142
  note("Dry run \u2014 no steps were executed", "Dry Run");
3939
4143
  }
3940
4144
  } catch (error2) {
@@ -4126,6 +4330,64 @@ async function flowRunsCommand(flowKey) {
4126
4330
  );
4127
4331
  console.log();
4128
4332
  }
4333
+ async function flowInspectCommand(runId, options = {}) {
4334
+ intro(pc7.bgCyan(pc7.black(" One Workflow ")));
4335
+ const state = FlowRunner.loadRunState(runId);
4336
+ if (!state) {
4337
+ const msg = `No run found for id "${runId}". List runs with: one flow runs`;
4338
+ if (isAgentMode()) {
4339
+ json({ error: msg, runId });
4340
+ process.exit(1);
4341
+ }
4342
+ error(msg);
4343
+ return;
4344
+ }
4345
+ const statePath = FlowRunner.statePathFor(state.flowKey, state.runId);
4346
+ const stepEntries = Object.entries(state.context.steps || {});
4347
+ if (isAgentMode()) {
4348
+ json({
4349
+ runId: state.runId,
4350
+ flowKey: state.flowKey,
4351
+ status: state.status,
4352
+ startedAt: state.startedAt,
4353
+ completedAt: state.completedAt,
4354
+ pausedAt: state.pausedAt,
4355
+ currentStepId: state.currentStepId,
4356
+ inputs: state.inputs,
4357
+ steps: state.context.steps,
4358
+ statePath
4359
+ });
4360
+ return;
4361
+ }
4362
+ console.log();
4363
+ console.log(` ${pc7.bold(state.flowKey)} ${pc7.dim(`run ${state.runId}`)} ${colorStatus(state.status)}`);
4364
+ console.log(` ${pc7.dim(`Started: ${state.startedAt}${state.completedAt ? ` \xB7 Ended: ${state.completedAt}` : ""}`)}`);
4365
+ if (state.currentStepId) console.log(` ${pc7.dim(`Current step: ${state.currentStepId}`)}`);
4366
+ console.log();
4367
+ if (stepEntries.length === 0) {
4368
+ note("No step outputs recorded yet for this run.", "Steps");
4369
+ } else {
4370
+ for (const [id, result] of stepEntries) {
4371
+ const icon = result.status === "success" ? pc7.green("\u2713") : result.status === "skipped" ? pc7.dim("\u25CB") : result.status === "timeout" ? pc7.yellow("\u29D6") : pc7.red("\u2717");
4372
+ const dur = result.durationMs !== void 0 ? pc7.dim(` ${result.durationMs}ms`) : "";
4373
+ const retries = result.retries ? pc7.dim(` (${result.retries} retr${result.retries === 1 ? "y" : "ies"})`) : "";
4374
+ console.log(` ${icon} ${id} ${pc7.dim(`[${result.status}]`)}${dur}${retries}`);
4375
+ if (result.error) {
4376
+ console.log(` ${pc7.red("error")} ${result.error}${result.errorCode ? pc7.dim(` (${result.errorCode})`) : ""}`);
4377
+ }
4378
+ if (result.output !== void 0) {
4379
+ const json2 = JSON.stringify(result.output, null, options.full ? 2 : 0) ?? String(result.output);
4380
+ const shown = options.full || json2.length <= 240 ? json2 : `${json2.slice(0, 239)}\u2026 ${pc7.dim("(--full for all)")}`;
4381
+ const indented = options.full ? shown.split("\n").map((l) => ` ${l}`).join("\n") : ` ${pc7.dim("output")} ${shown}`;
4382
+ console.log(indented);
4383
+ }
4384
+ }
4385
+ }
4386
+ console.log();
4387
+ console.log(` ${pc7.dim(`State: ${statePath}`)}`);
4388
+ console.log(` ${pc7.dim(`Log: ${path5.join(".one/flows/.logs", `${state.flowKey}-${state.runId}.log`)}`)}`);
4389
+ console.log();
4390
+ }
4129
4391
  function colorStatus(status) {
4130
4392
  switch (status) {
4131
4393
  case "completed":
@@ -9452,6 +9714,9 @@ Chain actions across platforms as JSON workflow files with conditions, loops, pa
9452
9714
  one --agent flow create <key> --definition '<json>' # Create a workflow
9453
9715
  one --agent flow validate <key> # Validate it
9454
9716
  one --agent flow execute <key> -i param=value # Execute it
9717
+ one --agent flow execute <key> --dry-run -i ... # Resolve interpolations, run nothing
9718
+ one --agent flow execute <key> --stop-after <stepId> # Run up to a step, then stop
9719
+ one --agent flow inspect <runId> # Per-step outputs of a past run
9455
9720
  one --agent flow list # List all workflows
9456
9721
  \`\`\`
9457
9722
 
@@ -9465,6 +9730,7 @@ one --agent flow list # List all workflows
9465
9730
  - Use \`--skip-validation\` to bypass input validation on action steps
9466
9731
  - Use \`--output-file <path>\` to stream the full result to a file instead of stdout \u2014 for large results that would otherwise be truncated or hit the JSON string-size limit; stdout (and \`--agent\` output) then carries an \`outputFile\` pointer instead of inline \`steps\`
9467
9732
  - Step-level \`if\`/\`unless\` (and \`while\`/\`condition\` steps) are null-safe: a condition referencing a skipped or not-yet-run step's output (e.g. \`$.steps.maybeSkipped.output.x\`) evaluates to \`false\` instead of crashing the flow
9733
+ - Debugging: \`--dry-run\` resolves each step's interpolations and shows what they evaluate to without executing (\`$.steps.*\` refs are reported as deferred); \`--stop-after <stepId>\` runs up to a step then stops; \`--dry-run --stop-after <stepId>\` runs earlier steps for real then dry-resolves the target against their output; \`flow inspect <runId>\` shows a past run's per-step outputs (post-mortem, no re-run; \`--full\` for untruncated)
9468
9734
 
9469
9735
  ### 3. Relay \u2014 Webhook event forwarding between platforms
9470
9736
  Receive webhooks from platforms (Stripe, GitHub, Airtable, Attio, Google Calendar) and forward event data to any connected platform using passthrough actions with Handlebars templates. No middleware, no code.
@@ -11361,7 +11627,7 @@ var flow = program.command("flow").alias("f").description("Create, execute, and
11361
11627
  flow.command("create [key]").description("Create a new workflow from JSON definition").option("--definition <json>", "Workflow definition as JSON string").option("-o, --output <path>", "Custom output path (default .one/flows/<key>/flow.json)").action(async (key, options) => {
11362
11628
  await flowCreateCommand(key, options);
11363
11629
  });
11364
- flow.command("execute <keyOrPath>").alias("x").description("Execute a workflow by key or file path").option("-i, --input <name=value>", "Input parameter (repeatable)", collect, []).option("--dry-run", "Validate and show execution plan without running").option("--mock", "With --dry-run: execute transforms/code with realistic mock API responses").option("--skip-validation", "Skip input validation against action schemas").option("--allow-bash", "Allow bash step execution (disabled by default for security)").option("-v, --verbose", "Show full request/response for each step").option("--output-file <path>", "Write the full result to a file (streamed) instead of stdout \u2014 avoids truncation/string-limit errors for large results; stdout/agent output then carries an outputFile pointer").action(async (keyOrPath, options) => {
11630
+ flow.command("execute <keyOrPath>").alias("x").description("Execute a workflow by key or file path").option("-i, --input <name=value>", "Input parameter (repeatable)", collect, []).option("--dry-run", "Resolve each step's interpolations and show what they evaluate to, without executing any step ($.steps.* refs resolve at runtime)").option("--mock", "With --dry-run: execute transforms/code with realistic mock API responses").option("--stop-after <stepId>", "Execute steps up to and including <stepId>, then stop (later steps are not run). With --dry-run, runs earlier steps for real and dry-resolves <stepId> against their output without executing it. See: one flow inspect").option("--skip-validation", "Skip input validation against action schemas").option("--allow-bash", "Allow bash step execution (disabled by default for security)").option("-v, --verbose", "Show full request/response for each step").option("--output-file <path>", "Write the full result to a file (streamed) instead of stdout \u2014 avoids truncation/string-limit errors for large results; stdout/agent output then carries an outputFile pointer").action(async (keyOrPath, options) => {
11365
11631
  await flowExecuteCommand(keyOrPath, options);
11366
11632
  });
11367
11633
  flow.command("list").alias("ls").description("List all workflows in .one/flows/").action(async () => {
@@ -11376,6 +11642,9 @@ flow.command("resume <runId>").description("Resume a paused or failed workflow r
11376
11642
  flow.command("runs [flowKey]").description("List workflow runs (optionally filtered by flow key)").action(async (flowKey) => {
11377
11643
  await flowRunsCommand(flowKey);
11378
11644
  });
11645
+ flow.command("inspect <runId>").description("Inspect a past run's per-step outputs (post-mortem, no re-run) from its saved state file").option("--full", "Show full step outputs (default truncates large values)").action(async (runId, options) => {
11646
+ await flowInspectCommand(runId, options);
11647
+ });
11379
11648
  flow.command("scaffold [template]").description("Generate a workflow scaffold (templates: basic, conditional, loop, ai)").action(async (template) => {
11380
11649
  await flowScaffoldCommand(template);
11381
11650
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@withone/cli",
3
- "version": "1.47.8",
3
+ "version": "1.47.10",
4
4
  "description": "CLI for managing One",
5
5
  "type": "module",
6
6
  "files": [
@@ -261,7 +261,7 @@ Without declared paths, the default walker concatenates every string in the reco
261
261
  One also supports more advanced patterns. Read the relevant reference file before using these:
262
262
 
263
263
  - **Webhook Relay** — Receive webhooks from a platform and forward to another (e.g., Stripe event -> Slack message). Read `references/relay.md` in this skill's directory for the full workflow.
264
- - **Multi-step Workflows** — Chain actions across platforms as JSON workflow files (like n8n/Zapier but file-based). Read `references/flows.md` in this skill's directory for the schema and examples.
264
+ - **Multi-step Workflows** — Chain actions across platforms as JSON workflow files (like n8n/Zapier but file-based). Read `references/flows.md` in this skill's directory for the schema and examples. To debug: `flow execute <key> --dry-run` (resolve interpolations without running), `--stop-after <stepId>` (run up to a step then stop), and `flow inspect <runId>` (a past run's per-step outputs).
265
265
 
266
266
  ## Adding New Connections
267
267
 
@@ -8,7 +8,7 @@ Nothing about a flow's runtime requirements is guessable from its name. Before `
8
8
 
9
9
  1. **Recommended:** `one --agent flow list` — the JSON output includes `requiresBash`, `usesCodeModules`, `inputs` (with `autoResolvable`), `stepTypes`, and the flow's `description`. Fastest path to knowing what you need.
10
10
  2. Read the flow's `description` field from the JSON. Authors are required (see "Author conventions" below) to state `--allow-bash` requirements and non-auto-resolving inputs there.
11
- 3. `one --agent flow execute <key> --dry-run` to see resolved inputs and step plan without side effects.
11
+ 3. `one --agent flow execute <key> --dry-run` to resolve every step's interpolations and see what they evaluate to — without side effects (see "Debugging a flow" below).
12
12
 
13
13
  If you skip this, you will hit errors like *"Workflow X contains bash steps. Re-run with --allow-bash."* — the CLI pre-flights and fails fast, but the error is entirely avoidable by reading first.
14
14
 
@@ -400,6 +400,28 @@ After a parallel step, access each substep's output by its `id`: `$.steps.fetchE
400
400
  { "id": "write", "type": "file-write", "fileWrite": { "path": "./output/results.json", "content": "$.steps.transform.output" } }
401
401
  ```
402
402
 
403
+ **Validate a read config (`fileRead.schema`).** With `parseJson: true` you can add an optional `schema` that is **enforced at runtime** — a mismatch throws (`errorCode: "SCHEMA_VALIDATION"`) and is handled by the step's `onError`, so a bad config file fails fast with a clear message instead of producing garbage downstream. (This is distinct from `step.outputSchema`, which is documentation/wiring only and never checked at runtime.)
404
+
405
+ ```json
406
+ {
407
+ "id": "read", "type": "file-read",
408
+ "fileRead": {
409
+ "path": "./data/config.json", "parseJson": true,
410
+ "schema": {
411
+ "name": { "type": "string", "required": true },
412
+ "mode": { "type": "string", "required": true, "enum": ["dev", "staging", "prod"] },
413
+ "retries": { "type": "number" },
414
+ "tags": { "type": "array", "items": "string", "minItems": 1, "maxItems": 10 },
415
+ "coords": { "type": "array", "length": 2, "items": "number" },
416
+ "owner": { "type": "object", "required": true,
417
+ "properties": { "id": "string", "email": { "type": "string", "required": true } } }
418
+ }
419
+ }
420
+ }
421
+ ```
422
+
423
+ Field rules (a richer field-rule format inspired by `outputSchema`): a value is a bare type string (`"string"`, optional) or `{ type, required, enum, items, minItems, maxItems, length, properties }`. Types: `string`/`number`/`boolean`/`object`/`array`/`null`/`unknown` (`unknown` passes anything). **Array constraints (`items`/`minItems`/`maxItems`/`length`) require `type: "array"`, and `properties` requires `type: "object"`** — `flow validate` rejects them otherwise (so they can't silently no-op). All violations are reported in one error; paths are dotted/indexed (`owner.email`, `tags[3]`); a non-object root reports `expected an object at the root but got <type>`. `flow validate` checks the schema's shape (and that `parseJson:true` is set). With `onError: { strategy: "continue" }` the step lands as `{ status: "failed", errorCode: "SCHEMA_VALIDATION" }` so downstream steps can branch on it; `retry`/`fallback` and `retryOn:["SCHEMA_VALIDATION"]` work too.
424
+
403
425
  ### `while` — Condition-driven loop (do-while)
404
426
 
405
427
  ```json
@@ -655,12 +677,24 @@ one --agent flow validate <key>
655
677
  one --agent flow execute <key> -i key=value
656
678
  one --agent flow execute <key> --dry-run -i key=value
657
679
  one --agent flow execute <key> --dry-run --mock -i key=value
680
+ one --agent flow execute <key> --stop-after <stepId> -i key=value
681
+ one --agent flow execute <key> --dry-run --stop-after <stepId> -i key=value
658
682
  one --agent flow execute <key> --skip-validation -i key=value
659
683
  one --agent flow execute <key> --allow-bash -i key=value
660
684
  one --agent flow runs [flowKey]
685
+ one --agent flow inspect <runId> # per-step outputs of a past run (add --full for untruncated)
661
686
  one --agent flow resume <runId>
662
687
  ```
663
688
 
689
+ ## Debugging a flow
690
+
691
+ Three tools that turn "re-run the whole 40-step flow to debug step 30" into near-zero-cost inspection:
692
+
693
+ - **`--dry-run`** — resolves every step's interpolations and shows what they evaluate to, *without executing any step*. `$.input.*` / `$.env.*` resolve immediately; `$.steps.*` references are reported as `deferred` (they're produced at runtime). Transform/condition/while steps show their *evaluated* result; if a transform can't evaluate yet because it reads a step that hasn't run, it's marked `deferred` (depends on a later step) rather than shown as an error — genuine problems (syntax errors, missing inputs, a missing field on a step that *did* run) still surface as errors. Catches wiring bugs (typo'd input names, wrong field paths) with zero API spend. In `--agent` mode this is the `flow:dry-run` event whose `steps[]` carry a `references` table (`{selector, value, status}` where status is `resolved` | `deferred` | `missing`), an evaluated `resolved` value, and a `deferred` / `error` flag for expression steps.
694
+ - **`--stop-after <stepId>`** — executes steps up to and including `<stepId>`, then stops; later steps don't run. Isolates where a long flow breaks. Emits a `flow:stopped` event; the run's partial state is saved.
695
+ - **`--dry-run --stop-after <stepId>`** — runs the steps *before* `<stepId>` for real, then dry-resolves `<stepId>` against that **real** accumulated context (so its `$.steps.*` references resolve to actual upstream output) and stops without executing it. The honest way to preview "what will step N actually receive". Emits a `step:dry-resolve` event for the target.
696
+ - **`flow inspect <runId>`** — prints a past run's per-step status, durations, errors, and outputs from its persisted state file (post-mortem, no re-run). Use after `--stop-after`, or on any failed run, to see exactly what each step produced. `--full` disables output truncation.
697
+
664
698
  ## Important Notes
665
699
 
666
700
  - **Prefer passthrough actions over custom actions.** Custom actions add server-side fan-out that causes timeouts at scale. The flow runner handles pagination, retries, and rate limiting locally. Search with `-t knowledge` to find passthrough endpoints (e.g. GET `/gmail/v1/users/{userId}/threads` instead of POST `/gmail/get-threads`)