Skip to main content
Workflows execute steps based on their dependencies. By understanding how Timbal resolves these dependencies, you can build efficient pipelines that run steps in parallel when possible and sequentially when needed.

Parallel by Default

When steps have no relationship between them, they run concurrently. Timbal doesn’t wait for one to finish before starting the next:

Context Access

The input and output from each step is stored in its Run Context, accessible via get_run_context(). Each step exposes two built-in variables:
  • .input: Contains a dictionary of all the parameters passed to the step. They are accessed through their name.
  • .output: Contains the value(s) returned by the step. Can be a single value, dictionary, array, or custom class.
Steps can access data from their own context, the parent workflow, or any sibling step:

Custom Variables

You can also create your own custom variables to share data between steps:
After the workflow runs, each step has the following variables in their context: process_user:
check_status:

Data Dependencies

When a step parameter references another step’s output via a lambda, Timbal automatically creates a dependency and enforces sequential execution:
Here:
  • fetch_users and fetch_orders run in parallel (no dependencies between them)
  • merge_results waits for both to complete (its parameters reference their outputs)
The dependency is resolved automatically. You don’t need to declare it explicitly. Even though fetch_users and fetch_orders haven’t executed when merge_results is defined, Timbal detects the step_span() references and knows to wait.

Handling Optional Steps

When a step might be skipped (e.g., due to a when condition), you need to safely handle cases where that step didn’t execute. Use default=None with step_span() to check if a step ran before accessing its output.

Execution Flow

Consider a workflow that validates data and then processes it differently based on the validation result:
The Problem: When merge_results tries to access process_valid or process_invalid, one of them won’t exist because it was skipped. Without default=None, accessing a skipped step would raise an error. The Solution: Use step_span("name", default=None) which returns None if the step was skipped, allowing you to check which step actually ran.

Example

How it works:
  1. validate_data always executes and returns {"valid": True} or {"valid": False}
  2. Based on the validation result, either process_valid or process_invalid runs (but never both)
  3. merge_results uses step_span("name", default=None) to safely check which processing step ran:
    • If step_span("process_valid", default=None) returns a span (not None), that step ran
    • If it returns None, the step was skipped, so we check process_invalid instead
Key Point: step_span("name", default=None) returns None if the step was skipped, allowing you to handle optional dependencies gracefully. Without default=None, accessing a skipped step would raise an error.

Explicit Dependencies

Use depends_on when you need ordering without a data dependency:
process_data waits for init_database even though it doesn’t use its output.

Summary

  • Steps run in parallel by default
  • Access sibling step data via step_span(), custom variables via current_span()
  • Lambda parameters automatically create dependencies — even if the referenced step hasn’t executed yet
  • Use depends_on for explicit ordering without data dependency