Skip to main content

Control Flow Patterns

The workflows library provides four essential control flow patterns to structure your automation logic.

Sequential

Execute steps one after another in a defined order

Conditionals

Branch execution based on runtime conditions

Loops

Process collections of items with repeated logic

Parallel

Execute multiple branches simultaneously

Sequential

Sequential execution runs steps one after another in order. Use the .then() method to chain steps together.
Each step can access the results of previous steps using ctx.get_step_result("step-id").

Conditionals

Conditional branching lets your workflow make different choices based on data. Use .if_condition(), .elif_branch(), and .else_branch() to create decision logic.
Make sure your condition functions handle missing or invalid data gracefully.

Loops

Loops process a list of items by running the same workflow logic for each item. Use ctx.scope["item"] to access the current item and ctx.scope["i"] to get the index (0-based).
Loop iterations run in parallel by default.

Batching Loop Iterations

Because loop iterations run in parallel, looping over a large list can fire many API calls at once. When the work inside the loop is rate limited (for example, an external integration or a high-volume endpoint), a common pattern is to loop over batches instead of individual items, and stagger each batch with a delay based on its index.
Batch 0 processes immediately, batch 1 after one gap, batch 2 after two gaps, and so on. Suspending during the delay is durable, so this works for gaps of minutes or hours. See Delays and Scheduled Resumes for how the sleep pattern works.

Parallel

Parallel execution runs multiple workflow branches at the same time. All branches must complete before the workflow continues to the next step.
Parallel branches are perfect for independent tasks that can run simultaneously.

Retries

You can configure automatic retries for individual steps using the options parameter on Step.
  • num_retries: Number of retry attempts after the initial failure. Default is 0 (no retries).
  • backoff: Initial delay in milliseconds before the first retry. The delay doubles after each failed attempt (exponential backoff). Default is 1000ms. Must be >= 0.
  • Partial config: You may specify only one key:
    • If you set only num_retries, the backoff defaults to 1000ms.
    • If you set only backoff, retries will not happen unless num_retries > 0.
Available in workflows-py >= 0.1.21.

Error Handling

You can define custom error handling logic for individual steps using the on_failure option. This handler is called when a step fails after all retry attempts have been exhausted.
The on_failure handler receives two arguments:
  • ctx: The WorkflowRunContext with access to get_step_result(), get_start_data(), etc.
  • error: The error information (may be None in some cases)
Combine on_failure with num_retries to first attempt automatic recovery, then execute custom logic if all retries fail.
The on_failure handler must accept exactly two arguments: (ctx, error). A handler with only one argument will raise a TypeError.
Available in workflows-py >= 0.1.28.

Next Task Routing

You can override which task Sample opens next after a step completes by using the next_task option. This is useful when a workflow fans out into multiple manual tasks and you want to send the user to a specific one, such as the most recently created task in the workflow run or the latest task in the task list.
In practice, query_tasks(...) should wrap whatever SQL access your workflow already uses, such as client.v1.sql_execute(...). The next_task handler receives the normal WorkflowRunContext and should return one of:
  • A task ID like "tsk_123" to route to that task
  • None to fall back to Sample’s default next-task routing
next_task is a routing override only. It does not change the workflow graph or which step executes next. The workflow still advances normally. This only controls which task ID is returned to the UI or API after the current step completes.
For “send me to the latest task in this run” behavior, query tasks using ctx.workflow_run_id. The next_task callback is evaluated after the workflow advances, so downstream suspended tasks already exist when this lookup runs.
You can return a task ID from another workflow run. If the task ID is invalid, outside your org, or does not point to a suspended screen task, Sample falls back to the default routing behavior.
Available in workflows-py >= 0.1.28-beta.6.

Combining Control Flow Patterns

You can combine different control flow patterns to create more complex workflows:
Mix and match control flow patterns to handle complex business logic.