From 192866d5197c74ec930d6fe7bf9234fac76763f4 Mon Sep 17 00:00:00 2001 From: Ruben Fiszel Date: Mon, 4 May 2026 16:54:53 +0000 Subject: [PATCH] fix(flows): don't bubble error when continue_on_error is on the last step (#9029) * fix(flows): don't bubble error when continue_on_error is on the last step When the last step of a flow (or branch/forloop) failed with continue_on_error or skip_failures enabled, should_continue_flow resolved to false (because the flow was at its last step), and the flow was completed with success=false. This made parent flows / subflows treat the run as a failure even though the user explicitly asked to continue past errors. Detect this case and set success=true so the failure is captured in the result but not propagated. Co-Authored-By: Claude Opus 4.7 (1M context) * docs: explain why success is overridden post should_continue_flow Co-Authored-By: Claude Opus 4.7 (1M context) --------- Co-authored-by: Claude Opus 4.7 (1M context) --- backend/windmill-worker/src/worker_flow.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/backend/windmill-worker/src/worker_flow.rs b/backend/windmill-worker/src/worker_flow.rs index 1577d108e8..c21f22045a 100644 --- a/backend/windmill-worker/src/worker_flow.rs +++ b/backend/windmill-worker/src/worker_flow.rs @@ -1587,6 +1587,16 @@ pub async fn update_flow_status_after_job_completion_internal( Ok(should_retry) }; + // For a regular module with continue_on_error at the last position, nothing else + // overrides `success` — `flow_jobs` is None so the loop/branchall override above + // doesn't fire, and `should_continue_flow` resolves to `!is_last_step = false`, + // letting the flow complete with success=false and bubble the error up to the + // enclosing job/subflow. Detect that case and treat the flow as successful. + let recoverable_failure_at_last_step = !success + && is_last_step + && !unrecoverable + && (skip_seq_branch_failure || skip_loop_failures || continue_on_error); + let should_continue_flow = match success { _ if stop_early => stop_early_err_msg.is_some() && flow_value.failure_module.is_some(), // if stop_early_err_msg some, we want to trigger the error handler before stopping the flow, if any _ if flow_job.is_canceled() => false, @@ -1606,6 +1616,10 @@ pub async fn update_flow_status_after_job_completion_internal( false => false, }; + if recoverable_failure_at_last_step { + success = true; + } + tracing::info!(id = %flow_job.id, root_id = %job_root, success = %success, stop_early = %stop_early, is_last_step = %is_last_step, unrecoverable = %unrecoverable, skip_seq_branch_failure = %skip_seq_branch_failure, skip_loop_failures = %skip_loop_failures, current_module_id = %current_module.map(|x| x.id.clone()).unwrap_or_default(),