fix(workflows): persist structuredOutput on NodeOutput so $node.output.field works for Pi#1637
Conversation
…t.field works for Pi When a provider parses fence-wrapped or preamble-prefixed JSON onto the result chunk (Pi/Minimax via tryParseStructuredOutput), the executor captured it locally but never persisted it onto NodeOutput. Downstream consumers (substituteNodeOutputRefs, condition-evaluator) then JSON.parse(output)'d the original prose-prefixed text, which threw, and $node.output.field resolved to empty. This persists structuredOutput on NodeOutput (single-shot and loop-terminal-iteration success paths) and teaches both consumers to prefer the parsed object over re-parsing prose. Falls back to JSON.parse(output) when structuredOutput is absent so Claude/Codex output_format-encoded NodeOutput rows (and older rows written before this field existed) keep working. Cross-resume rehydration of structuredOutput from event_data is out of scope here; resumed runs that re-execute downstream nodes will fall through to the JSON.parse path, which matches existing behavior. Closes coleam00#1571
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
This is targeting main, please reopen targeting dev |
Scope-fixed re-open of #1636 (closed by @Wirasm with "more changes than the pr body claims"). The branch was rebased off downstream dev which carried 179 unmerged-to-main commits; force-pushed onto upstream/main. Diff now matches the body: 5 files, +283/-9, single commit.
bun test src/condition-evaluator.test.ts src/dag-executor.test.tspasses 263/0 locally.Summary
tryParseStructuredOutput), the DAG executor captures it into a local variable but never persists it ontoNodeOutput. Downstream$node.output.fieldsubstitution andwhen:conditions then tryJSON.parse(output)on the original prose-prefixed text, throw, and resolve to empty.$classifier.output.type(or any other field) silently routes to the wrong branch or drops the value.structuredOutput?: unknownadded to the completed/running and failed NodeOutput branches; producer call sites persist it; consumers (substituteNodeOutputRefs,condition-evaluator.resolveOutputRef) prefer it overJSON.parse(output)and fall back to the existing path when absent.$node.output(unfielded) still readsoutputtext. Cross-resume rehydration fromevent_datais out of scope (resumed runs that re-execute downstream nodes fall through to JSON.parse). Providers that already overwriteoutputwith a JSON.stringify of structuredOutput (Claude/Codex withoutput_format) are unaffected because theiroutputis already JSON.UX Journey
Before
After
Architecture Diagram
Before
After
Connection inventory:
dag-executor(single-shot success return)NodeOutput.structuredOutputdag-executor(loop terminal-iteration return)NodeOutput.structuredOutputlastIterationStructuredOutputlocalsubstituteNodeOutputRefsNodeOutput.structuredOutputcondition-evaluator.resolveOutputRefNodeOutput.structuredOutputnodeOutputSchema(completed/running, failed)z.unknown().optional()Label Snapshot
risk: lowsize: Sworkflowsworkflows:executor,workflows:condition-evaluator,workflows:schemasChange Metadata
bugworkflowsLinked Issue
Validation Evidence (required)
New tests added:
condition-evaluator.test.ts: 12 new tests covering structuredOutput preference, Claude/Codex JSON.parse fallback regression, numeric/boolean/object/array field coercion, null/top-level-array/primitive fall-through, missing-field semantics, and bare$node.output(no field) still uses output text.dag-executor.test.ts: 13 new tests forsubstituteNodeOutputRefscovering the same matrix plus shell-escaping safety on structuredOutput fields.Security Impact (required)
NoNoNoNoCompatibility / Migration
Yes.structuredOutputis optional on the schema; producers populate it only when the provider emits one; consumers fall through to the existingJSON.parse(output)path when it is absent. Workflows that depend on the JSON.parse path keep working unchanged.NoNo. NodeOutput is stored inline in workflow event data; older rows withoutstructuredOutputdeserialize asundefinedand hit the JSON.parse fallback.NodeOutputfromevent_data. If the original run was on this version, the field is preserved; if older, it is absent and the JSON.parse fallback runs. Cross-version resume of Pi workflows that previously routed wrong will route correctly once the upstream re-runs on the new version.Human Verification (required)
$node.output(no field) ignores structuredOutput and reads output text.escapedForBash=true) wraps structuredOutput strings/objects in single quotes; numbers/booleans stay unquoted.Side Effects / Blast Radius (required)
$node.output.fieldsubstitution or awhen:expression that uses dot notation.JSON.parse(output)returning a different shape than what the provider's parsed structuredOutput contains (e.g. provider parses one JSON object out of multiple in the prose) would see a behavior change. In practice the providers that emitstructuredOutputare emitting the canonical structured payload that the prompt asked for, so the change is the intended fix, not a surprise.condition_json_parse_failed,dag_node_output_ref_json_parse_failed) still fire on the fallback path; no new error paths introduced.Rollback Plan (required)
$node.output.fieldwould resolve to JSON-stringified content where it previously resolved to a plain string (would surface as condition mismatches or wrongly-formatted prompts on Claude/Codex output_format workflows).Risks and Mitigations
$node.output.fieldon a Pi/Minimax node (because the JSON.parse failed) might now see the resolved value and take a previously-unreachable branch. This is the intended behavior change for Parsed structured output not persisted to NodeOutput — condition evaluator and $node.output.field re-parse raw text, breaking Pi/Minimax #1571 but could surprise authors who built around the bug.''when the structuredOutput object doesn't contain the requested key). The fix only changes the case where the provider actually parsed the structured payload the prompt requested.