Skip to content

Parallel Execution and Dependencies

Leonard Ramminger edited this page Aug 9, 2026 · 1 revision

Parallel Execution and Dependencies

Beez runs work at two levels: within a phase+scope (among steps) and within a workflow (among phase+scope blocks). Understanding both helps you get speed without breaking ordering assumptions.

Parallelism inside a phase+scope

When Beez runs a phase+scope pair, it gathers all matching steps and splits them into levels:

  1. Steps with no dependency between them (and no mutate conflict on the same files) are in the same level and may run in parallel.
  2. Steps in different levels run one level after another.

If a level contains only one step, or if Beez is limited to one thread (-j 1), steps run sequentially.

phase=build, scope=default

  Level 0 (parallel)     Level 1 (parallel)
  +----------+             +--------+
  | compile  |             | link   |
  +----------+             +--------+
  | gen-code |
  +----------+

In this example compile and gen-code have no ordering constraint, so they can run together. link waits until level 0 finishes.

Declaring order with order()

Use order(before, after) when one step must finish before another starts. The first argument runs before the second:

order("configure:setup", "build:compile")
order("build:compile", "test:unit")

Step names in order() are the step name field, not phase:scope. Step names are globally unique in the registry; registering the same name again replaces the earlier step. order() only affects steps that run together in the same phase+scope group.

Typical chain for a build pipeline:

order("configure:setup", "build:compile")
order("build:compile", "test:unit")
order("build:compile", "test:integration")

Here test:unit and test:integration both depend on build:compile. After compile finishes, those two tests can run in parallel in the same level.

Automatic ordering from mutate

Beez also infers dependencies when two steps in the same phase+scope mutate overlapping files (via mutate glob patterns). That prevents parallel runs from stomping the same paths.

If Beez cannot resolve ordering (for example a cycle in order()), the run fails with a step ordering error.

Parallelism inside workflows

Workflow steps run sequentially by default. Each entry waits for the previous one to finish.

To run several phase+scope pairs at the same time, wrap them in parallel:

workflow("ci", {
    { parallel = {
        { phase = "generate", scope = "docs" },
        { phase = "generate", scope = "code" },
    }},
    { phase = "compile", scope = "code" },
})

Timeline:

Workflow step 1 (parallel)
  generate:docs  -----|
  generate:code  -----|  (both at once)
                      v
Workflow step 2
  compile:code   -----

Parallel workflow steps are independent subtrees. Ordering between them is only what the workflow sequence defines.

Tasks are always sequential

A task with multiple actions runs them one by one:

task("check", {
    "echo lint",
    { name = "compile" },
    { name = "test:unit" },
})

There is no parallel form for tasks. Use a workflow if you need parallel phase+scope execution.

Thread count

By default Beez uses one thread per CPU core. Override with -j / --threads:

beez -j 4 build
beez -j 1 build    # force sequential execution inside each level

Project and user config can also set performance.max_threads.

Lua callback steps

Steps with a Lua run function (instead of a shell string) are executed like any other step regarding levels and order(). Keep callbacks short and predictable; heavy parallelism inside a callback is your responsibility (for example via ctx:spawn).

Summary

Mechanism What runs in parallel
Same level, same phase+scope Steps without ordering or mutate conflict
order(before, after) Forces before before after (different levels)
mutate overlap Implicit ordering between conflicting steps
Workflow { parallel = { ... } } Multiple phase+scope pairs at once
Workflow list (no parallel) Phase+scope pairs one after another
Task action list Always sequential

Next steps

Clone this wiki locally