Repository navigation
fix(workflows): report a run failed by a raised step error under --json - #5015
Conversation
`workflow run --json` and `workflow resume --json` printed nothing on stdout when a step raised instead of returning a failed result, for example `from_json` on invalid JSON in a condition, or an exception from a custom step. The engine had already saved the run as `failed` with that error before re-raising it, and `workflow status --json` reported it that way, but the CLI's ValueError and catch-all handlers only printed the message (to stderr under --json). A caller parsing stdout got an empty string and never learned the run ID it needs to resume. An I/O failure at the same point already prints the failed run's payload through `_fail_workflow_execution`. Send an exception through that reporter when the engine recorded it as the run's failure: the state is `failed` and its error is this exception's message. Errors raised before the run is recorded, such as a missing required input on `run` or an invalid `--input` on `resume`, keep their current output. In text mode the recorded failure is now labelled "Workflow failed:" or "Resume failed:", as I/O failures are, instead of "Error:". Assisted-by: Claude Code (model: Claude Opus 5.5, autonomous) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
🟡 Changes recommended
Matching error text can misreport a rejected resume as a newly recorded execution failure.
1 open issue
What changed in this PR
Updates workflow CLI failure reporting so raised step errors produce saved run details under --json.
Changes:
- Shares recorded-failure reporting between
runandresume. - Adds regression tests for raised errors and pre-execution rejections.
Validation used an isolated helper check; project tests and baseline comparisons were not rerun.
AI disclosure: GitHub Copilot (model unknown, autonomous) generated this review for the requester.
| File | Description |
|---|---|
| tests/specify_cli/workflows/test_command_run.py | Tests raised errors, text output, and missing inputs. |
| tests/specify_cli/workflows/test_command_resume.py | Tests resumed errors and rejected input updates. |
| src/specify_cli/workflows/command_run.py | Routes recorded exceptions through shared reporting. |
| src/specify_cli/workflows/command_resume.py | Applies shared reporting to resumed failures. |
| src/specify_cli/workflows/_commands.py | Adds the recorded-failure reporting helper. |
Review effort: Max
…essage resume() exposes the loaded failed state before validating updated inputs, so a refused resume whose error text matched the previous failure (for example the same "Input 'count' expected a number" message) was reported as the run's outcome: the old run's JSON on stdout and no diagnostic on stderr. Have execute() and resume() mark the exception they record as the run's failure on the in-memory state (never persisted), and report a run outcome only when that marker is the exception being handled. Assisted-by: Claude Code (model: Claude Opus 5.5, autonomous) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Review round 1, posted on behalf of @yxshee. Addressed the Copilot finding that a refused resume with a matching error message was reported as the run's outcome (commit 109d147):
AI disclosure: written and pushed by Claude Code (model: Claude Opus 5.5, max reasoning effort, autonomous agent mode); the code, test and this comment are AI-generated. 🤖 Addressed by Claude Code |
|
Thank you! |

Description
docs/reference/workflows.mdsaysspecify workflow run --jsonprints a single object that "always parses". Forfailedruns, that object includes anerrorfield. When a step raises instead of returning a failed result,run --jsonandresume --jsonprint nothing on stdout. Examples:from_jsonon invalid JSON in a condition, or an exception from a custom step.The engine has already saved the run as
failedwith that error, soworkflow status --jsonreports it. But a caller parsing stdout gets an empty string and never learns the run ID it needs to resume.Root cause:
WorkflowEngine.execute()/resume()persist an exception raised while steps run as the run'sfailedstatus and error, then re-raise it.except ValueErrorand catch-all handlers incommand_run.pyandcommand_resume.pyonly print the message, which goes to stderr under--json.OSErrorat the same point already goes through_fail_workflow_execution(), which prints the failed run's payload.The fix:
_fail_recorded_run()inworkflows/_commands.py, called from both handlers inrunandresume.failedand its error equals the exception's message, it reports through_fail_workflow_execution().run, or an invalid--inputonresumeof an already-failed run.Testing
uv run specify --helpuv sync && uv run pytestmainand on this branchNew tests in
tests/specify_cli/workflows/test_command_run.py:test_run_json_step_exception_reports_failed_runtest_run_json_non_value_error_reports_failed_run(a step raisingRuntimeError)test_run_step_exception_reports_failed_run_in_texttest_run_json_missing_required_input_is_not_a_run_outcomeNew tests in
tests/specify_cli/workflows/test_command_resume.py:test_resume_json_step_exception_reports_failed_runtest_resume_json_refused_before_running_is_not_a_run_outcomeOn
main, the 4 positive tests fail: the JSON ones withjson.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0), the text one because "Workflow failed:" is missing. The 2 negative tests pass. With the change both files give 91 passed.tests/test_*_python_parity.pyfiles occur identically on unmodifiedmainon this machine under full-suite load; those files pass when run on their own (257 passed, 112 skipped).uvx ruff@0.15.0 check src tests: cleanAI Disclosure
AI disclosure: Claude Code (model: Claude Opus 5.5, max reasoning effort, autonomous agent mode) opened this PR on behalf of @yxshee. A Claude Code subagent (same model, autonomous) found the bug and wrote the fix and tests. The lead agent independently re-ran the new tests against
mainand the branch, reproduced the transcripts above with the real CLI, reviewed the diff, and wrote this description. No human reviewed the change line by line before it was opened.🤖 Generated with Claude Code