Private Beta
.md

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:

BHW

Key semantics:

  • The outer list is the concurrency: three branches above run at the same time.
  • Each inner list is sequential: ffuf waits 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.

BHW

#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:

BHW

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

NeedUse
Run independent scans side by sidestep.parallel([...])
Fire off a slow job and keep goinghandle = step.spawn([...])
Wait for spawned jobs before endingstep.join([handle])
Fan out over many targets/itemsstep.for_each