when returned false, or a dependency was missing).
Skipped vs failed
Skipped is intentional (branch not taken). Failed is an error you should handle or fix.
Step failure
When a step handler raises, Timbal records the error on that step’s span and marks the workflow failed:Dependent steps
A downstream step is blocked when any prerequisite fails. This applies to explicitdepends_on links and dependencies inferred from step_span() calls, and propagates through the dependency chain. Conditions and input resolvers are not evaluated for blocked steps:
step_span("name", default=None) when a step might have been skipped (see Control flow), not to recover from a failed prerequisite. A failed step may retain partial output for inspection, but dependent steps do not run.
Timeouts
A step’s owntimeout ends that call with status.code == "timeout" and a RunTimeout error. Its dependents are blocked, and the workflow reports status.code == "error" with reason step_failed and the original timeout details.
A timeout on the workflow instead bounds the entire foreground run. It cancels and joins active step tasks and closes their streams before the workflow reports status.code == "timeout". Nested agents and workflows follow the same cleanup path. Cancellation is cooperative: cleanup may take longer than the deadline, already-completed side effects are not rolled back, and detached children use their separate background_timeout.
Skipped branches
Whenwhen is false, the step is skipped and dependents that require its output are skipped too:
process and save_results are skipped.
Steps that only need ordering (not data) can use depends_on to run after a branch resolves:
Pauses (approval / suspend)
When a step hits an approval gate orsuspend(), the workflow pauses with status.code == "cancelled" and a reason like approval_required or input_required. Parallel steps that also pause emit their events before the workflow stops, so you can resume multiple pending ids in one call.
Pauses are not failures. Resume with parent_id and resume={...} as documented in Resuming a paused run.
Cycle detection
Linking steps that would create a cycle raises immediately when you call.step():
depends_on, when callables, and lambda parameters that call step_span().
Tracing
Every step gets its own span under the workflow span. Inspect per-step input, output, and errors viaget_run_context()._trace or your tracing provider. Failed steps record error on their span even when the workflow’s final status is error.
See also
- Control flow — optional steps with
default=None - Branching — conditional execution
- Human in the loop — pauses on workflow steps