Skip to content

fix(workflows): surface condition_json_parse_failed as workflow error instead of silent skip (#1673)#1694

Merged
Wirasm merged 1 commit into
coleam00:devfrom
kagura-agent:fix/condition-json-parse-fail-loud
May 15, 2026
Merged

fix(workflows): surface condition_json_parse_failed as workflow error instead of silent skip (#1673)#1694
Wirasm merged 1 commit into
coleam00:devfrom
kagura-agent:fix/condition-json-parse-fail-loud

Conversation

@kagura-agent

@kagura-agent kagura-agent commented May 15, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Problem: When a when: condition references $nodeId.output.field and the node output is not valid JSON (e.g. Pi/Minimax wrapping JSON in markdown fences or prepending reasoning prose), the condition evaluator returns parsed: true with result: false. The DAG executor treats this as a legitimate "condition not met" skip. The workflow exits 0 — no error, no review posted, silent failure.
  • Why it matters: A 5-minute workflow "succeeds" without doing the work. Users have no way to detect the failure from the CLI exit code. This violates the "Fail Fast + Explicit Errors" principle. Observed in ~17% of maintainer-review-pr runs with Pi.
  • What changed: resolveOutputRef() now throws OutputRefParseError when JSON parse fails (instead of returning empty string). evaluateAtom() catches it and returns {parsed: false}. Also strips common markdown fences (\x60\x60\x60json blocks) before attempting JSON.parse.
  • What did not change (scope boundary): No changes to dag-executor.ts — it already correctly handles parsed: false (surfaces error message, logs properly, exits non-zero). No changes to structuredOutput paths — they are unaffected since the JSON parse fallback is only reached when structured output is absent.

UX Journey

Before

User                       Archon                       Pi
────                       ──────                       ──
runs maintainer-review-pr ▶ executes gate node ────────▶ returns "Let me think...\n\x60\x60\x60json\n{...}\n\x60\x60\x60"
                            condition_json_parse_failed (warn log only)
                            $gate.output.verdict == 'review' → false (parsed: true)
                            every downstream node → Skipped (when_condition)
sees "Workflow completed   ◀── exit 0, no review posted
successfully" (WRONG)

After

User                       Archon                       Pi
────                       ──────                       ──
runs maintainer-review-pr ▶ executes gate node ────────▶ returns "Let me think...\n\x60\x60\x60json\n{...}\n\x60\x60\x60"
                            strips markdown fences ✓
                            JSON.parse succeeds ✓
                            $gate.output.verdict == 'review' → true
                            downstream nodes execute normally ✓
sees review posted         ◀── exit 0, review posted (CORRECT)

--- If JSON is truly unparseable (no fences, just prose): ---

runs maintainer-review-pr ▶ executes gate node ────────▶ returns "Sure, here is my analysis..."
                            fence strip: no fences found
                            JSON.parse fails
                            condition_json_parse_failed (warn log)
                            resolveOutputRef throws OutputRefParseError
                            evaluateAtom returns {parsed: false}
sees "⚠️ Node 'gate':  ◀── exit non-zero, error surfaced (CORRECT)
unparseable when: ..."

Architecture Diagram

Before

evaluateAtom → resolveOutputRef → JSON.parse fails → returns ""
     ↓                                                    ↓
  actual="" vs expected="review" → {result: false, parsed: true}
     ↓
  DAG: legitimate skip → exit 0  ← WRONG

After

evaluateAtom → resolveOutputRef → strip fences → JSON.parse
     ↓                                              ↓ success → return value
     ↓                                              ↓ fail → throw OutputRefParseError
  catch OutputRefParseError → {result: false, parsed: false}
     ↓
  DAG: when_condition_parse_error → surface message → exit non-zero  ← CORRECT

Connection inventory:

From To Status Notes
evaluateAtom resolveOutputRef modified Now catches OutputRefParseError
resolveOutputRef JSON.parse modified Strips fences first; throws on failure
dag-executor evaluateCondition unchanged Already handles parsed:false correctly

Label Snapshot

  • Risk: risk: low
  • Size: size: S
  • Scope: workflows
  • Module: workflows:condition-evaluator

Change Metadata

  • Change type: bug
  • Primary scope: workflows

Linked Issue

Validation Evidence (required)

bun run test    # 54 pass, 0 fail (condition-evaluator.test.ts)
bun run type-check  # all 5 packages pass
# lint-staged ran eslint + prettier on commit
  • Evidence provided: All 54 condition evaluator tests pass, including 4 new tests for the fix.
  • If any command is intentionally skipped: bun run format:check covered by lint-staged on commit.

Security Impact (required)

  • New permissions/capabilities? No
  • New external network calls? No
  • Secrets/tokens handling changed? No
  • File system access scope changed? No

Compatibility / Migration

  • Backward compatible? Partially — workflows that previously "succeeded silently" will now surface an error and exit non-zero. This is the intended fix; the silent success was a bug.
  • Config/env changes? No
  • Database migration needed? No

Human Verification (required)

  • Verified scenarios: (1) Invalid JSON output → parsed:false returned (2) Markdown-fenced JSON → fences stripped, parsed correctly (3) Compound AND with one unparseable atom → parsed:false propagates (4) All 50 existing tests still pass (no regressions)
  • Edge cases checked: Plain \x60\x60\x60 fences (no language tag), prose before/after fences, multiple fence blocks (first match used)
  • What was not verified: Live Pi/Minimax output (no access to those providers locally)

Side Effects / Blast Radius (required)

  • Affected subsystems/workflows: Any workflow with when: clauses referencing $node.output.field where the producing node is an LLM
  • Potential unintended effects: Workflows that relied on the silent-skip behavior will now error out. This is correct — the silent skip was masking failures.
  • Guardrails/monitoring: The condition_json_parse_failed warn log already exists; the new behavior just escalates it from "silent skip" to "error + exit non-zero"

Rollback Plan (required)

  • Fast rollback command/path: Revert the commit (single file change)
  • Feature flags or config toggles: None needed — the change is in the condition evaluator return contract
  • Observable failure symptoms: Workflows that previously "completed successfully" with skipped nodes will now show when_condition_parse_error messages

Risks and Mitigations

  • Risk: Workflows with noisy LLM outputs may fail more frequently
    • Mitigation: The fence-stripping logic handles the most common Pi/Minimax pattern. For truly unparseable output, the correct behavior IS to fail and retry, not to silently skip.

Summary by CodeRabbit

  • Bug Fixes

    • Improved error handling for JSON parsing failures in condition evaluation, ensuring errors are properly reported rather than silently ignored.
    • Added support for JSON values wrapped in markdown code fences.
    • Enhanced error propagation through compound conditions when JSON parsing fails.
  • Tests

    • Added test coverage for JSON parsing failures and markdown code fence handling in condition evaluation.

Review Change Stack

… instead of silent skip (coleam00#1673)

When a when: condition references $nodeId.output.field and the node's
output text is not valid JSON, the condition evaluator now returns
parsed:false so the DAG executor treats it as an error (instead of
silently skipping downstream nodes and exiting 0).

Additionally, strip common markdown fences (e.g. ```json blocks)
from output text before attempting JSON.parse, handling the common
Pi/Minimax pattern of wrapping JSON in fences.
@coderabbitai

coderabbitai Bot commented May 15, 2026

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c89bccee-9ace-4c98-872b-581ce40f2abb

📥 Commits

Reviewing files that changed from the base of the PR and between e60af1d and 19cbda3.

📒 Files selected for processing (2)
  • packages/workflows/src/condition-evaluator.test.ts
  • packages/workflows/src/condition-evaluator.ts

📝 Walkthrough

Walkthrough

The PR hardens condition evaluation by introducing explicit JSON parsing failure handling in condition-evaluator.ts. The evaluator now strips markdown code fences from output before parsing, throws OutputRefParseError on failure, catches that error in atom evaluation to return { result: false, parsed: false }, and includes comprehensive test coverage for invalid JSON, fenced JSON extraction, and compound-expression propagation.

Changes

JSON Parse Error Handling

Layer / File(s) Summary
Parse error class and fence-stripping logic
packages/workflows/src/condition-evaluator.ts
Introduces OutputRefParseError class to carry node and field context. Updates resolveOutputRef fallback path to strip markdown JSON code fences (\``jsonand```) from output text before JSON.parse, and throws OutputRefParseError` on parse failure instead of silently returning an empty string.
Error handling in atom evaluation
packages/workflows/src/condition-evaluator.ts
Wraps resolveOutputRef in evaluateAtom with try/catch: catches OutputRefParseError and returns { result: false, parsed: false } to fail-close the atom without attempting field comparison; other errors are rethrown.
Test coverage for parse failures
packages/workflows/src/condition-evaluator.test.ts
Adds regression test for invalid (non-JSON) output with dot-field access, tests stripping of both \``json-tagged and plain ```fences before successful parsing, and validates thatparsed: falsepropagates through compound&&` expressions when a later atom's JSON parse fails.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related PRs

  • coleam00/Archon#1654: Both PRs modify resolveOutputRef and evaluateAtom logic in condition-evaluator — one prefers structuredOutput fallback, while this one adds fenced JSON stripping and fail-closed parse-error handling for the text-based fallback.
  • coleam00/Archon#1482: Both PRs modify output-ref resolution in condition-evaluator around JSON-derived field extraction — one adds fail-closed parse-error handling, the other changes how parsed array/object fields are stringified.

Poem

🐰 Fences fall and JSON flows,
Parse errors clear the tangled rows,
No silent skips on broken braces—
Just parsed: false in the right places!
~Archon's helpful rabbit

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main fix: surfacing condition JSON parse failures as workflow errors instead of silent skips, which directly matches the code changes.
Description check ✅ Passed The description follows the template structure comprehensively, covering all key sections including Summary, UX Journey (before/after), Architecture Diagram, validation evidence, security impact, compatibility notes, and rollback plan with clear detail.
Linked Issues check ✅ Passed The PR implementation fully addresses #1673 objectives: (1) OutputRefParseError now surfaces JSON parse failures instead of silent skips [#1673], (2) markdown fence stripping prevents legitimate JSON wrapped in fences from failing [#1673], (3) parsed:false return propagates to DAG executor to surface error + exit non-zero [#1673], (4) existing DAG executor contract unchanged [#1673].
Out of Scope Changes check ✅ Passed All changes are scoped to condition-evaluator implementation and tests. No modifications to dag-executor or structuredOutput paths. Changes directly align with linked issue #1673 objectives and stated scope boundaries.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@Wirasm

Wirasm commented May 15, 2026

Copy link
Copy Markdown
Collaborator

Review Summary

Verdict: ready-to-merge

This PR fixes #1673 by surfacing JSON parse failures as parsed: false instead of silently returning ''. Callers can now distinguish "condition skipped because output couldn't be parsed" from "condition skipped because comparison failed." The change is correct and well-tested; the code is clean and focused.

Minor / nice-to-have

  • condition-evaluator.ts:22-26OutputRefParseError omits the outputPreview field that was present in the original warn log. A future caller logging or reporting this error won't have the truncated output snippet for debugging. Consider attaching it:

    class OutputRefParseError extends Error {
      constructor(nodeId: string, field: string, outputPreview: string) {
        super(`Cannot parse output of node '${nodeId}' as JSON for field '${field}'`);
        this.name = 'OutputRefParseError';
        this.outputPreview = outputPreview; // for callers who re-log
      }
    }
    // In resolveOutputRef:
    throw new OutputRefParseError(nodeId, field, nodeOutput.output.slice(0, 100));
  • condition-evaluator.test.ts — Test comment // --- #1673: ... references an issue number that will be meaningless over time. Your test descriptions (it('returns parsed:false when output text is not valid JSON ...')) are already self-documenting — drop the issue reference.

Compliments

  • The markdown fence stripping comment (// Strip common markdown fences that LLMs (Pi/Minimax) wrap around JSON) is the gold standard: it explains the why that the code can't. Well done.
  • The backward-compat comment on the text fallback is equally solid — future readers will understand the context immediately.
  • Four targeted tests covering non-JSON → parsed: false, fenced JSON → parsed: true, and compound AND propagation. No over-testing.

Reviewed via maintainer-review-pr workflow (Pi/Minimax). Aspects run: code-review, error-handling, test-coverage, comment-quality.

@Wirasm Wirasm merged commit 7f6f639 into coleam00:dev May 15, 2026
4 checks passed
cropse pushed a commit to cropse/Archon that referenced this pull request May 19, 2026
… instead of silent skip (coleam00#1673) (coleam00#1694)

When a when: condition references $nodeId.output.field and the node's
output text is not valid JSON, the condition evaluator now returns
parsed:false so the DAG executor treats it as an error (instead of
silently skipping downstream nodes and exiting 0).

Additionally, strip common markdown fences (e.g. ```json blocks)
from output text before attempting JSON.parse, handling the common
Pi/Minimax pattern of wrapping JSON in fences.
@Wirasm Wirasm mentioned this pull request May 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Workflow exits success when condition_json_parse_failed cascades to silent node skips

2 participants