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) <noreply@anthropic.com>

* docs: explain why success is overridden post should_continue_flow

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ruben Fiszel
2026-05-04 16:54:53 +00:00
committed by GitHub
co-authored by Claude Opus 4.7
parent 505f78bd29
commit 192866d519
@@ -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(),