Skip to main content
Workflows run steps concurrently but respect dependencies. When something goes wrong, behavior depends on whether the step failed (raised an error) or was skipped (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:
If multiple steps run in parallel and one fails, unrelated branches may finish. The workflow reports the original step failure after those branches finish; it does not cancel all branches on the first error. An agent step can handle errors from its own tools or sub-agents. Those descendant events remain visible in the stream, but only the enclosing step’s own result determines whether that step failed. A tool timeout does not fail the workflow if its agent recovers and completes successfully.

Dependent steps

A downstream step is blocked when any prerequisite fails. This applies to explicit depends_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:
Use 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 own timeout 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

When when is false, the step is skipped and dependents that require its output are skipped too:
If validation fails, both process and save_results are skipped. Steps that only need ordering (not data) can use depends_on to run after a branch resolves:
See Branching for the full pattern.

Pauses (approval / suspend)

When a step hits an approval gate or suspend(), 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():
Dependencies come from 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 via get_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