BountyHub Docs
Concurrency
Run steps in parallel with step.parallel, step.spawn, and step.join.
BountyHub provides structured concurrency: branches are declared in the workflow, their lifecycle is tracked durably by the runner, and synchronization is explicit. There are three primitives:
step.parallel(branches)— run groups of steps concurrently and wait for all.step.spawn(steps)— start an independent branch without blocking.step.join(handles)— block until one or more spawned branches finish.
#Parallel Branches: step.parallel
step.parallel takes an outer list of branches. Each branch is a list of
steps that run sequentially; all branches run concurrently. The step completes
when every branch reaches a terminal state:
Key semantics:
- The outer list is the concurrency: three branches above run at the same time.
- Each inner list is sequential:
ffufwaits for the wordlist download within its own branch only. parallel([])is an immediate no-op.- A flat (non-nested) list is rejected — wrap single steps in their own lists.
#Failure behavior
If a branch fails, step.parallel fails — but running siblings are not
cancelled. They finish naturally; the join simply reports failure. Use
allow_failure = True on steps inside branches so a noisy optional phase
cannot fail the whole scan. cond and allow_failure are also accepted on step.parallel itself
to gate or soften the entire barrier. Per-branch name/timeout cannot be
set on step.parallel; set timeout on the steps inside each branch.
#Spawn And Join: step.spawn / step.join
When you need to keep working while a long-running phase executes in the
background, use step.spawn. It returns a handle — an execution reference,
never data. Bind the handle to a variable, keep going with other work, then
synchronize with step.join:
step.join(handles) blocks until every joined branch reaches a terminal state.
It succeeds only if all joined branches succeed; if one fails, the join
fails without cancelling any still-running siblings. An empty handle list is
rejected.
step.spawn accepts name and timeout. Its allow_failure parameter is
accepted but ignored: branch failure always propagates to step.join.
Because handles are synchronization references only, spawn/join never merge data between branches. If branches need to exchange files, do it through the workspace or artifacts.
#Choosing a Primitive
| Need | Use |
|---|---|
| Run independent scans side by side | step.parallel([...]) |
| Fire off a slow job and keep going | handle = step.spawn([...]) |
| Wait for spawned jobs before ending | step.join([handle]) |
| Fan out over many targets/items | step.for_each |