Worker processes every UpdateFlow wrap-up with a hardcoded unrecoverable=true (benign wrap-ups log as errors; success=false wrap-ups skip retry and continue_on_error) #11498
Description
Describe the bug
Every flow wrap-up that travels through the SendResultPayload::UpdateFlow channel is processed by the worker with a hardcoded unrecoverable: true argument. This produces two problems:
-
Benign completions are logged as unrecoverable. A
branchonewithout adefaultbranch, when no branch expression matches, completes its router module asSuccess { job: null }(viaNextFlowTransform::EmptyInnerFlows). The wrap-up then goes through theUpdateFlowchannel and the worker logs a line that reads as a severe error:success = true, unrecoverable = true, continue_on_error = false, should_continue_flow = trueThe run succeeds. We chased this in production as a false alarm.
-
success = falsewrap-ups skip retry andcontinue_on_error. The suspend disapproval/timeout path (worker_flow.rs, thePushNextFlowJob::Done(Some(UpdateFlow { success: false, ... }))sender forSuspendedDisapproved/SuspendedTimedOut) sends a failure through the same channel. At the receiver,unrecoverable = truemakes thefalse if unrecoverablearm of theshould_continue_flowmatch win before thecontinue_on_error/skip_failure/ retry arms, so a failure on that path is never retried andcontinue_on_erroris silently ignored. Whether a suspend timeout should retry is a product question, but the decision currently happens as a side effect of a hardcoded flag whose own comments define it as "worker crash/OOM" semantics, and the sender cannot express the choice.
To Reproduce
- Create a flow with a
branchonemodule that has nodefaultbranch (onlybranches). - Run the flow so that no branch expression matches.
- The run succeeds, but the worker log shows the
success = true, unrecoverable = true, ...line above.
Expected behavior
unrecoverable should be chosen at the send site, or derived, rather than hardcoded at the receiver. The UpdateFlow message already carries success and stop_early_override; an explicit unrecoverable field (defaulting to false) would let the suspend timeout sender decide its own semantics, and would stop benign wrap-ups from logging as unrecoverable.
Screenshots
Not applicable; the log line above is the observable behavior.
Version (payload)
Windmill v1.775.2, self-hosted (deployed via Helm chart). Relevant code at that tag:
backend/windmill-worker/src/result_processor.rs, inprocess_jc, theSendResultPayload::UpdateFlowarm: the call toupdate_flow_status_after_job_completionpassessuccessfrom the message but hardcodestruefor theunrecoverableparameter (line 480).backend/windmill-worker/src/worker_flow.rs: theshould_continue_flowmatch, where thefalse if unrecoverablearm pre-empts thecontinue_on_error/ retry arms, and the suspend timeout/disapprove sender that emitsUpdateFlow { success: false, ... }.
Activity
A-P-Shukla commentedon Oct 5, 2026
@andrewle21 Traced this through commit history, this is already resolved on HEAD (v1.823.1).
The exact behavior you described was refactored in PR #10552 (commit c59b60c):
1. Typed State: Added step_failure: StepFailureKind to UpdateFlow, replacing the hardcoded unrecoverable: true at the receiver in result_processor.rs.
2. Explicit Semantics: The suspend timeout/disapproval path now explicitly emits StepFailureKind::SuspendNotApproved instead of inheriting generic worker-crash semantics.
3. Derived Recovery: should_continue_flow in worker_flow.rs derives the unrecoverable flag directly from the StepFailureKind variant. Benign completions (BranchOne empty match) no longer log false-alarm errors, and suspend timeouts correctly reach the retry/continue_on_error arms.
If you upgrade your deployment from v1.775.2 to v1.823.1, both symptoms are fixed.
@rubenfiszel Looks like this issue can be closed as resolved in v1.823.1.