@withone/cli 1.47.8 → 1.47.11

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-VW7J2RQW.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-VW7J2RQW.js";
34
35
  import {
35
36
  memSqlCommand
36
37
  } from "./chunk-QV3Y5N5G.js";
@@ -2739,7 +2740,13 @@ async function actionsExecuteCommand(platform, actionId, connectionKey, options)
2739
2740
  } else {
2740
2741
  console.log();
2741
2742
  console.log(pc6.bold("Response:"));
2742
- console.log(JSON.stringify(result.responseData, null, 2));
2743
+ const rd = result.responseData;
2744
+ if (rd && typeof rd === "object" && typeof rd.text === "string" && "contentType" in rd) {
2745
+ if (rd.contentType) console.log(pc6.dim(`(${rd.contentType})`));
2746
+ console.log(rd.text);
2747
+ } else {
2748
+ console.log(JSON.stringify(result.responseData, null, 2));
2749
+ }
2743
2750
  }
2744
2751
  } catch (error2) {
2745
2752
  spinner5.stop("Execution failed");
@@ -3597,7 +3604,7 @@ function validateOutputSchemas(flow2) {
3597
3604
  walkValue(v, `${pathPrefix}.${k}`);
3598
3605
  }
3599
3606
  }
3600
- function walkSteps(steps, pathPrefix) {
3607
+ function walkSteps2(steps, pathPrefix) {
3601
3608
  steps.forEach((step, i) => {
3602
3609
  const stepPath = `${pathPrefix}[${i}]`;
3603
3610
  if (step.if) checkText(step.if, `${stepPath}.if`);
@@ -3612,13 +3619,13 @@ function validateOutputSchemas(flow2) {
3612
3619
  for (const { configKey, fieldName } of nestedKeys) {
3613
3620
  const c = step[configKey];
3614
3621
  if (c && Array.isArray(c[fieldName])) {
3615
- walkSteps(c[fieldName], `${stepPath}.${configKey}.${fieldName}`);
3622
+ walkSteps2(c[fieldName], `${stepPath}.${configKey}.${fieldName}`);
3616
3623
  }
3617
3624
  }
3618
3625
  }
3619
3626
  });
3620
3627
  }
3621
- walkSteps(flow2.steps, "steps");
3628
+ walkSteps2(flow2.steps, "steps");
3622
3629
  return errors;
3623
3630
  }
3624
3631
  function validateOutputSchemaShape(schema, location) {
@@ -3652,6 +3659,7 @@ function validateFlow(flow2, rootDir) {
3652
3659
  ...validateStepIds(f),
3653
3660
  ...validateSelectorReferences(f, rootDir),
3654
3661
  ...validateOutputSchemas(f),
3662
+ ...validateFileReadSchemas(f),
3655
3663
  ...rootDir ? validateCodeModules(f, rootDir) : []
3656
3664
  ];
3657
3665
  }
@@ -3692,6 +3700,108 @@ function validateCodeModules(flow2, rootDir) {
3692
3700
  walk(flow2.steps, "steps");
3693
3701
  return errors;
3694
3702
  }
3703
+ var VALID_FILE_READ_SCHEMA_TYPES = /* @__PURE__ */ new Set(["string", "number", "boolean", "object", "array", "null", "unknown"]);
3704
+ function isPlainObject(v) {
3705
+ return !!v && typeof v === "object" && !Array.isArray(v);
3706
+ }
3707
+ function validateFileReadFieldRule(raw, fieldPath, errors) {
3708
+ if (typeof raw === "string") {
3709
+ if (!VALID_FILE_READ_SCHEMA_TYPES.has(raw)) {
3710
+ errors.push({ path: fieldPath, message: `unknown type "${raw}". Allowed: ${[...VALID_FILE_READ_SCHEMA_TYPES].join(", ")}.` });
3711
+ }
3712
+ return;
3713
+ }
3714
+ if (!isPlainObject(raw)) {
3715
+ errors.push({ path: fieldPath, message: 'field rule must be a type string or an object (e.g. { "type": "string", "required": true })' });
3716
+ return;
3717
+ }
3718
+ const rule = raw;
3719
+ if (rule.type !== void 0 && (typeof rule.type !== "string" || !VALID_FILE_READ_SCHEMA_TYPES.has(rule.type))) {
3720
+ errors.push({ path: `${fieldPath}.type`, message: `unknown type "${String(rule.type)}". Allowed: ${[...VALID_FILE_READ_SCHEMA_TYPES].join(", ")}.` });
3721
+ }
3722
+ if (rule.required !== void 0 && typeof rule.required !== "boolean") {
3723
+ errors.push({ path: `${fieldPath}.required`, message: '"required" must be a boolean' });
3724
+ }
3725
+ if (rule.enum !== void 0) {
3726
+ if (!Array.isArray(rule.enum) || rule.enum.length === 0) {
3727
+ errors.push({ path: `${fieldPath}.enum`, message: '"enum" must be a non-empty array of allowed values' });
3728
+ } else if (rule.enum.some((e) => e !== null && typeof e === "object")) {
3729
+ errors.push({ path: `${fieldPath}.enum`, message: '"enum" entries must be primitives (string/number/boolean) or null' });
3730
+ }
3731
+ }
3732
+ for (const k of ["minItems", "maxItems", "length"]) {
3733
+ const n = rule[k];
3734
+ if (n !== void 0 && (typeof n !== "number" || !Number.isInteger(n) || n < 0)) {
3735
+ errors.push({ path: `${fieldPath}.${k}`, message: `"${k}" must be a non-negative integer` });
3736
+ }
3737
+ }
3738
+ if (typeof rule.minItems === "number" && typeof rule.maxItems === "number" && rule.minItems > rule.maxItems) {
3739
+ errors.push({ path: `${fieldPath}.minItems`, message: '"minItems" must be <= "maxItems"' });
3740
+ }
3741
+ if (rule.length !== void 0 && (rule.minItems !== void 0 || rule.maxItems !== void 0)) {
3742
+ errors.push({ path: `${fieldPath}.length`, message: '"length" cannot be combined with "minItems"/"maxItems"' });
3743
+ }
3744
+ const hasArrayConstraint = rule.items !== void 0 || rule.minItems !== void 0 || rule.maxItems !== void 0 || rule.length !== void 0;
3745
+ if (hasArrayConstraint && rule.type !== "array") {
3746
+ errors.push({ path: `${fieldPath}.type`, message: '"items"/"minItems"/"maxItems"/"length" require "type": "array"' });
3747
+ }
3748
+ if (rule.items !== void 0) {
3749
+ if (typeof rule.items === "string") {
3750
+ if (!VALID_FILE_READ_SCHEMA_TYPES.has(rule.items)) {
3751
+ errors.push({ path: `${fieldPath}.items`, message: `unknown item type "${rule.items}". Allowed: ${[...VALID_FILE_READ_SCHEMA_TYPES].join(", ")}.` });
3752
+ }
3753
+ } else if (isPlainObject(rule.items)) {
3754
+ validateFileReadFieldRule(rule.items, `${fieldPath}.items`, errors);
3755
+ } else {
3756
+ errors.push({ path: `${fieldPath}.items`, message: '"items" must be a type string or a field rule object' });
3757
+ }
3758
+ }
3759
+ if (rule.properties !== void 0) {
3760
+ if (rule.type !== "object") {
3761
+ errors.push({ path: `${fieldPath}.properties`, message: '"properties" requires "type": "object"' });
3762
+ }
3763
+ errors.push(...validateFileReadSchemaShape(rule.properties, `${fieldPath}.properties`));
3764
+ }
3765
+ }
3766
+ function validateFileReadSchemaShape(schema, location) {
3767
+ const errors = [];
3768
+ if (!isPlainObject(schema)) {
3769
+ errors.push({ path: location, message: '"schema" must be an object mapping field names to type/rules' });
3770
+ return errors;
3771
+ }
3772
+ for (const [field, raw] of Object.entries(schema)) {
3773
+ validateFileReadFieldRule(raw, `${location}.${field}`, errors);
3774
+ }
3775
+ return errors;
3776
+ }
3777
+ function validateFileReadSchemas(flow2) {
3778
+ const errors = [];
3779
+ const nestedKeys = getNestedStepsKeys();
3780
+ function walk(steps, pathPrefix) {
3781
+ for (let i = 0; i < steps.length; i++) {
3782
+ const step = steps[i];
3783
+ const stepPath = `${pathPrefix}[${i}]`;
3784
+ if (step.type === "file-read" && step.fileRead && step.fileRead.schema !== void 0) {
3785
+ const fr = step.fileRead;
3786
+ if (fr.parseJson !== true) {
3787
+ errors.push({
3788
+ path: `${stepPath}.fileRead.schema`,
3789
+ message: "fileRead.schema is only checked when parseJson:true \u2014 set parseJson:true or remove schema."
3790
+ });
3791
+ }
3792
+ errors.push(...validateFileReadSchemaShape(fr.schema, `${stepPath}.fileRead.schema`));
3793
+ }
3794
+ for (const { configKey, fieldName } of nestedKeys) {
3795
+ const config2 = step[configKey];
3796
+ if (config2 && Array.isArray(config2[fieldName])) {
3797
+ walk(config2[fieldName], `${stepPath}.${configKey}.${fieldName}`);
3798
+ }
3799
+ }
3800
+ }
3801
+ }
3802
+ walk(flow2.steps, "steps");
3803
+ return errors;
3804
+ }
3695
3805
 
3696
3806
  // src/commands/flow.ts
3697
3807
  import fs5 from "fs";
@@ -3716,6 +3826,49 @@ async function writeFlowResultFile(filePath, meta, steps) {
3716
3826
  await done;
3717
3827
  return abs;
3718
3828
  }
3829
+ function previewValue(value, max = 120) {
3830
+ if (value === void 0) return "<undefined>";
3831
+ let str;
3832
+ try {
3833
+ str = typeof value === "string" ? JSON.stringify(value) : JSON.stringify(value) ?? String(value);
3834
+ } catch {
3835
+ str = String(value);
3836
+ }
3837
+ if (str === void 0) str = String(value);
3838
+ return str.length > max ? `${str.slice(0, max - 1)}\u2026` : str;
3839
+ }
3840
+ function renderDryRef(ref) {
3841
+ if (ref.status === "resolved") {
3842
+ return `${pc7.green("\u2713")} ${ref.selector} ${pc7.dim("\u2192")} ${previewValue(ref.value)}`;
3843
+ }
3844
+ if (ref.status === "deferred") {
3845
+ return `${pc7.dim("\u25CB")} ${ref.selector} ${pc7.dim("\u2192 pending (produced by a later step)")}`;
3846
+ }
3847
+ return `${pc7.yellow("!")} ${ref.selector} ${pc7.yellow("\u2192 unresolved \u2014 check the input/env name")}`;
3848
+ }
3849
+ function renderDryResolution(steps) {
3850
+ const isExpr = (t) => t === "transform" || t === "condition" || t === "while";
3851
+ for (const s of steps) {
3852
+ const label = s.name ? `${s.stepId} ${pc7.dim(`"${s.name}"`)}` : s.stepId;
3853
+ console.log(` ${pc7.cyan("\u25B8")} ${label} ${pc7.dim(`(${s.type})`)}`);
3854
+ if (s.error !== void 0) {
3855
+ console.log(` ${pc7.red("error")} ${s.error}`);
3856
+ } else if (isExpr(s.type) && s.deferred) {
3857
+ const deps = s.references.map((r) => r.selector).join(", ");
3858
+ console.log(` ${pc7.dim("\u25CB pending \u2014 depends on")} ${deps} ${pc7.dim("(produced by a later step)")}`);
3859
+ } else if (isExpr(s.type)) {
3860
+ console.log(` ${pc7.dim("=")} ${previewValue(s.resolved)}`);
3861
+ }
3862
+ if (!(isExpr(s.type) && s.deferred)) {
3863
+ for (const ref of s.references) {
3864
+ console.log(` ${renderDryRef(ref)}`);
3865
+ }
3866
+ }
3867
+ if (!isExpr(s.type) && s.references.length === 0 && s.error === void 0) {
3868
+ console.log(` ${pc7.dim("(no interpolations)")}`);
3869
+ }
3870
+ }
3871
+ }
3719
3872
  function getConfig2() {
3720
3873
  const apiKey = getApiKey();
3721
3874
  if (!apiKey) {
@@ -3870,6 +4023,20 @@ ${preflightErrors.map((e) => ` ${e.path}: ${e.message}`).join("\n")}`);
3870
4023
  }
3871
4024
  error(msg);
3872
4025
  }
4026
+ if (options.stopAfter) {
4027
+ const ids = /* @__PURE__ */ new Set();
4028
+ walkSteps(flow2.steps, (s) => {
4029
+ ids.add(s.id);
4030
+ });
4031
+ if (!ids.has(options.stopAfter)) {
4032
+ const msg = `--stop-after target "${options.stopAfter}" is not a step in workflow "${flow2.key}". Known step ids: ${[...ids].join(", ")}`;
4033
+ if (isAgentMode()) {
4034
+ json({ error: msg, unknownStopAfter: options.stopAfter, stepIds: [...ids] });
4035
+ process.exit(1);
4036
+ }
4037
+ error(msg);
4038
+ }
4039
+ }
3873
4040
  const inputs = parseInputs(options.input || []);
3874
4041
  const resolvedInputs = await autoResolveConnectionInputs(flow2, inputs, api);
3875
4042
  const runner = new FlowRunner(flow2, resolvedInputs);
@@ -3883,7 +4050,17 @@ ${pc7.yellow("Pausing after current step completes...")} (run ID: ${runId})`);
3883
4050
  }
3884
4051
  };
3885
4052
  process.on("SIGINT", sigintHandler);
4053
+ let dryRunSteps;
4054
+ let dryResolveTarget;
4055
+ let stoppedAfter;
3886
4056
  const onEvent = (event) => {
4057
+ if (event.event === "flow:dry-run") {
4058
+ dryRunSteps = event.steps;
4059
+ } else if (event.event === "step:dry-resolve") {
4060
+ dryResolveTarget = event;
4061
+ } else if (event.event === "flow:stopped") {
4062
+ stoppedAfter = event.stoppedAfter;
4063
+ }
3887
4064
  if (isAgentMode()) {
3888
4065
  json(event);
3889
4066
  } else if (options.verbose) {
@@ -3909,23 +4086,46 @@ ${pc7.yellow("Pausing after current step completes...")} (run ID: ${runId})`);
3909
4086
  verbose: options.verbose,
3910
4087
  allowBash: options.allowBash,
3911
4088
  skipValidation: options.skipValidation,
4089
+ stopAfter: options.stopAfter,
3912
4090
  rootDir,
3913
4091
  onEvent
3914
4092
  });
3915
4093
  process.off("SIGINT", sigintHandler);
3916
4094
  if (!options.verbose && !isAgentMode()) {
3917
- execSpinner.stop("Workflow completed");
4095
+ execSpinner.stop(stoppedAfter ? "Workflow stopped" : dryRunSteps ? "Dry run complete" : "Workflow completed");
3918
4096
  }
3919
4097
  const resultFile = options.outputFile ? await writeFlowResultFile(options.outputFile, { runId, logFile: logPath, status: "success" }, context.steps) : void 0;
4098
+ const finalStatus = stoppedAfter ? "stopped" : "success";
3920
4099
  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
- );
4100
+ const envelope = {
4101
+ event: "workflow:result",
4102
+ runId,
4103
+ logFile: logPath,
4104
+ statePath: runner.getStatePath(),
4105
+ status: finalStatus
4106
+ };
4107
+ if (resultFile) envelope.outputFile = resultFile;
4108
+ else envelope.steps = context.steps;
4109
+ if (options.dryRun) envelope.dryRun = true;
4110
+ if (stoppedAfter) envelope.stoppedAfter = stoppedAfter;
4111
+ json(envelope);
3924
4112
  return;
3925
4113
  }
3926
4114
  if (resultFile) {
3927
4115
  note(`Full result written to ${resultFile}`, "Output");
3928
4116
  }
4117
+ if (dryRunSteps && !stoppedAfter) {
4118
+ console.log();
4119
+ renderDryResolution(dryRunSteps);
4120
+ console.log();
4121
+ const missing = dryRunSteps.reduce((n, s) => n + s.references.filter((r) => r.status === "missing").length, 0);
4122
+ note(
4123
+ `Dry run \u2014 no steps executed. Resolved ${dryRunSteps.length} step(s)` + (missing > 0 ? `; ${pc7.yellow(`${missing} unresolved input/env reference(s)`)}` : "") + `.
4124
+ ${pc7.dim("$.steps.* refs resolve at runtime \u2014 re-run with --stop-after=<stepId> to resolve them against real output.")}`,
4125
+ "Dry Run"
4126
+ );
4127
+ return;
4128
+ }
3929
4129
  const stepEntries = Object.entries(context.steps);
3930
4130
  const succeeded = stepEntries.filter(([, r]) => r.status === "success").length;
3931
4131
  const failed = stepEntries.filter(([, r]) => r.status === "failed").length;
@@ -3934,7 +4134,17 @@ ${pc7.yellow("Pausing after current step completes...")} (run ID: ${runId})`);
3934
4134
  console.log(` ${pc7.green("\u2713")} ${succeeded} succeeded ${failed > 0 ? pc7.red(`\u2717 ${failed} failed`) : ""} ${skipped > 0 ? pc7.dim(`\u25CB ${skipped} skipped`) : ""}`);
3935
4135
  console.log(` ${pc7.dim(`Run ID: ${runId}`)}`);
3936
4136
  console.log(` ${pc7.dim(`Log: ${logPath}`)}`);
3937
- if (options.dryRun) {
4137
+ if (dryResolveTarget) {
4138
+ console.log();
4139
+ console.log(` ${pc7.dim(`Resolved (not executed) "${dryResolveTarget.stepId}":`)}`);
4140
+ renderDryResolution([dryResolveTarget]);
4141
+ }
4142
+ if (stoppedAfter) {
4143
+ note(
4144
+ `Stopped after step "${stoppedAfter}". Inspect step outputs with: ${pc7.cyan(`one flow inspect ${runId}`)}`,
4145
+ "Stopped"
4146
+ );
4147
+ } else if (options.dryRun) {
3938
4148
  note("Dry run \u2014 no steps were executed", "Dry Run");
3939
4149
  }
3940
4150
  } catch (error2) {
@@ -4126,6 +4336,64 @@ async function flowRunsCommand(flowKey) {
4126
4336
  );
4127
4337
  console.log();
4128
4338
  }
4339
+ async function flowInspectCommand(runId, options = {}) {
4340
+ intro(pc7.bgCyan(pc7.black(" One Workflow ")));
4341
+ const state = FlowRunner.loadRunState(runId);
4342
+ if (!state) {
4343
+ const msg = `No run found for id "${runId}". List runs with: one flow runs`;
4344
+ if (isAgentMode()) {
4345
+ json({ error: msg, runId });
4346
+ process.exit(1);
4347
+ }
4348
+ error(msg);
4349
+ return;
4350
+ }
4351
+ const statePath = FlowRunner.statePathFor(state.flowKey, state.runId);
4352
+ const stepEntries = Object.entries(state.context.steps || {});
4353
+ if (isAgentMode()) {
4354
+ json({
4355
+ runId: state.runId,
4356
+ flowKey: state.flowKey,
4357
+ status: state.status,
4358
+ startedAt: state.startedAt,
4359
+ completedAt: state.completedAt,
4360
+ pausedAt: state.pausedAt,
4361
+ currentStepId: state.currentStepId,
4362
+ inputs: state.inputs,
4363
+ steps: state.context.steps,
4364
+ statePath
4365
+ });
4366
+ return;
4367
+ }
4368
+ console.log();
4369
+ console.log(` ${pc7.bold(state.flowKey)} ${pc7.dim(`run ${state.runId}`)} ${colorStatus(state.status)}`);
4370
+ console.log(` ${pc7.dim(`Started: ${state.startedAt}${state.completedAt ? ` \xB7 Ended: ${state.completedAt}` : ""}`)}`);
4371
+ if (state.currentStepId) console.log(` ${pc7.dim(`Current step: ${state.currentStepId}`)}`);
4372
+ console.log();
4373
+ if (stepEntries.length === 0) {
4374
+ note("No step outputs recorded yet for this run.", "Steps");
4375
+ } else {
4376
+ for (const [id, result] of stepEntries) {
4377
+ const icon = result.status === "success" ? pc7.green("\u2713") : result.status === "skipped" ? pc7.dim("\u25CB") : result.status === "timeout" ? pc7.yellow("\u29D6") : pc7.red("\u2717");
4378
+ const dur = result.durationMs !== void 0 ? pc7.dim(` ${result.durationMs}ms`) : "";
4379
+ const retries = result.retries ? pc7.dim(` (${result.retries} retr${result.retries === 1 ? "y" : "ies"})`) : "";
4380
+ console.log(` ${icon} ${id} ${pc7.dim(`[${result.status}]`)}${dur}${retries}`);
4381
+ if (result.error) {
4382
+ console.log(` ${pc7.red("error")} ${result.error}${result.errorCode ? pc7.dim(` (${result.errorCode})`) : ""}`);
4383
+ }
4384
+ if (result.output !== void 0) {
4385
+ const json2 = JSON.stringify(result.output, null, options.full ? 2 : 0) ?? String(result.output);
4386
+ const shown = options.full || json2.length <= 240 ? json2 : `${json2.slice(0, 239)}\u2026 ${pc7.dim("(--full for all)")}`;
4387
+ const indented = options.full ? shown.split("\n").map((l) => ` ${l}`).join("\n") : ` ${pc7.dim("output")} ${shown}`;
4388
+ console.log(indented);
4389
+ }
4390
+ }
4391
+ }
4392
+ console.log();
4393
+ console.log(` ${pc7.dim(`State: ${statePath}`)}`);
4394
+ console.log(` ${pc7.dim(`Log: ${path5.join(".one/flows/.logs", `${state.flowKey}-${state.runId}.log`)}`)}`);
4395
+ console.log();
4396
+ }
4129
4397
  function colorStatus(status) {
4130
4398
  switch (status) {
4131
4399
  case "completed":
@@ -9437,7 +9705,7 @@ one --agent actions execute <platform> <actionId> <key> -d '{}' # Execute it
9437
9705
  - \`--dry-run\` \u2014 Preview request without executing
9438
9706
  - \`--mock\` \u2014 Return example response without making an API call (useful for building UI against a response shape)
9439
9707
  - \`--skip-validation\` \u2014 Skip input validation against the action schema
9440
- - \`--output <path>\` \u2014 Save response to a file (for binary downloads like PDFs, images, documents)
9708
+ - \`--output <path>\` \u2014 Save response to a file (for binary downloads like PDFs, images, documents). Text responses (text/plain, HTML, CSV, XML) render inline automatically \u2014 \`--output\` is only needed for genuinely binary payloads.
9441
9709
  - \`--no-cache\` \u2014 Bypass the cached action details and re-fetch them; the fresh details still refresh the cache (execution itself is never cached)
9442
9710
 
9443
9711
  The CLI validates required parameters against the action schema before executing. If you're missing a required path variable, query param, or body field, you'll get a clear error listing what's missing and which flag to use. Pass \`--skip-validation\` to bypass.
@@ -9452,6 +9720,9 @@ Chain actions across platforms as JSON workflow files with conditions, loops, pa
9452
9720
  one --agent flow create <key> --definition '<json>' # Create a workflow
9453
9721
  one --agent flow validate <key> # Validate it
9454
9722
  one --agent flow execute <key> -i param=value # Execute it
9723
+ one --agent flow execute <key> --dry-run -i ... # Resolve interpolations, run nothing
9724
+ one --agent flow execute <key> --stop-after <stepId> # Run up to a step, then stop
9725
+ one --agent flow inspect <runId> # Per-step outputs of a past run
9455
9726
  one --agent flow list # List all workflows
9456
9727
  \`\`\`
9457
9728
 
@@ -9465,6 +9736,7 @@ one --agent flow list # List all workflows
9465
9736
  - Use \`--skip-validation\` to bypass input validation on action steps
9466
9737
  - 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
9738
  - 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
9739
+ - 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
9740
 
9469
9741
  ### 3. Relay \u2014 Webhook event forwarding between platforms
9470
9742
  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.
@@ -9565,7 +9837,7 @@ one --agent actions execute <platform> <actionId> <connectionKey> [options]
9565
9837
  - \`--dry-run\` \u2014 Preview without executing
9566
9838
  - \`--mock\` \u2014 Return example response without making an API call
9567
9839
  - \`--skip-validation\` \u2014 Skip input validation against the action schema
9568
- - \`--output <path>\` \u2014 Save response to a file (for binary downloads like PDFs, images, documents)
9840
+ - \`--output <path>\` \u2014 Save response to a file (for binary downloads like PDFs, images, documents). Text responses (text/plain, HTML, CSV, XML) render inline automatically \u2014 \`--output\` is only needed for genuinely binary payloads.
9569
9841
  - \`--no-cache\` \u2014 Bypass the cached action details and re-fetch them; the fresh details still refresh the cache (execution itself is never cached)
9570
9842
 
9571
9843
  Execute reuses the action details cached by \`actions knowledge\` (method, path, schema), so in the standard search \u2192 knowledge \u2192 execute flow it makes a single API call \u2014 the action being executed. The live response is never cached. In \`--agent\` mode the response includes \`"_preflight": {"cache": "hit"|"miss"}\` showing whether the lookup was served from disk.
@@ -11361,7 +11633,7 @@ var flow = program.command("flow").alias("f").description("Create, execute, and
11361
11633
  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
11634
  await flowCreateCommand(key, options);
11363
11635
  });
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) => {
11636
+ 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
11637
  await flowExecuteCommand(keyOrPath, options);
11366
11638
  });
11367
11639
  flow.command("list").alias("ls").description("List all workflows in .one/flows/").action(async () => {
@@ -11376,6 +11648,9 @@ flow.command("resume <runId>").description("Resume a paused or failed workflow r
11376
11648
  flow.command("runs [flowKey]").description("List workflow runs (optionally filtered by flow key)").action(async (flowKey) => {
11377
11649
  await flowRunsCommand(flowKey);
11378
11650
  });
11651
+ 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) => {
11652
+ await flowInspectCommand(runId, options);
11653
+ });
11379
11654
  flow.command("scaffold [template]").description("Generate a workflow scaffold (templates: basic, conditional, loop, ai)").action(async (template) => {
11380
11655
  await flowScaffoldCommand(template);
11381
11656
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@withone/cli",
3
- "version": "1.47.8",
3
+ "version": "1.47.11",
4
4
  "description": "CLI for managing One",
5
5
  "type": "module",
6
6
  "files": [
@@ -89,7 +89,7 @@ Options:
89
89
  - `--dry-run` — Preview the request without executing
90
90
  - `--mock` — Return example response without making an API call (useful for building UI)
91
91
  - `--skip-validation` — Skip input validation against the action schema
92
- - `--output <path>` — Save response to a file (for binary downloads like PDFs, images, documents)
92
+ - `--output <path>` — Save response to a file (for binary downloads like PDFs, images, documents). Text responses (text/plain, HTML, CSV, XML) render inline automatically; `--output` is only needed for genuinely binary payloads.
93
93
  - `--no-cache` — Bypass the cached action details and re-fetch them; the fresh details still refresh the cache (execution itself is never cached)
94
94
 
95
95
  The CLI validates required parameters before executing. Missing params return a structured error with the flag name, parameter name, and description. Pass `--skip-validation` to bypass.
@@ -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`)