BountyHub Docs
Steps
Running scripts and processes with step.script and step.exec, and moving files between activities with artifacts.
Steps are the units of execution inside an activity. This page covers the two
execution primitives (step.script and step.exec) and artifact plumbing.
Other step kinds are covered in their own pages:
- Concurrency —
step.parallel,step.spawn,step.join - For Each —
step.for_each - Transforms —
step.transform - Modules —
step.module - Agents —
step.agent - Datasets —
step.update_dataset
#Common Step Parameters
Most steps accept:
| Parameter | Default | Meaning |
|---|---|---|
id | — | Stable key for run.steps.<id> lookups and scan.succeeded() predicates |
name | — | Human-facing label shown in the UI (defaults are derived) |
cond | run.ok | Run the step only when this runtime predicate holds |
allow_failure | False | Do not fail the activity if the step fails |
timeout | — | Bound the step's execution time |
step.spawn is the exception: it returns a branch handle, so it accepts
name and timeout only (its allow_failure parameter is accepted but
ignored).
#Conditions: cond, .when(...) and step predicates
A condition is a runtime predicate F(run) -> bool. Write it as a lambda
or pass a step predicate. It is evaluated by the runner just before the step
executes, never while the workflow is being defined:
scan.succeeded()/scan.failed()return a predicaterun -> bool; they require the referenced step to have anid..when(F)andcond = Fare the same mechanism. Static booleans are rejected (cond = True,.when(True)) so a condition can never be evaluated too early.- A step predicate can only test an earlier step in the same activity.
Cross-activity wiring is
trigger.on(scan.succeeded())— that is an activity event, not a step condition.
Steps can be bound to variables so later conditions can inspect them via
run.steps (see the Syntax Reference).
#Shell Scripts: step.script
step.script is the explicit shell API. It runs a script under a shell
(bash by default):
Script content can also be a function, which is evaluated at execution time with the run context — useful for templating from dispatch inputs or earlier results:
#Process Execution: step.exec
step.exec runs a program directly from a complete argv vector. There is no
implicit shell: element 0 is the program, and the process receives exactly what
you declare.
#Explicit everything
step.exec has no implicit workflow data. The process receives exactly the
explicitly provided command, environment, stdin, and working directory:
command— the complete argv vector. Elements may be literal strings, references to files produced by earlier steps,config.*values, module string inputs, or lazy runtime expressions. Module file and directory inputs cannot be used as raw command elements; format them into argv (for example"{}".format(targets)) instead. A nested list is an optional argv group: it expands only when every optional module input it references is bound, otherwise the whole group is dropped. A bare optional module input element is dropped when unbound — so CLI flags vanish instead of being passed empty. Concatenating a string literal with a bool module input using+is rejected; use a conditional instead.env— only the entries you list. Values may be config references; secret values are resolved at execution time and never serialized.stdin/stdout/stderr— optional; each must be apath.file(...). Without an explicitstdout, process output is captured for logs but never becomes workflow data.cwd— optional; must be apath.directory(...).
A non-zero exit code fails the step (unless allow_failure = True). Timeouts
are configured per step with timeout; there is no per-step retry policy.
#Data flow defines ordering
There is no implicit sequencing between independent processes beyond the order
you write them in. When a step's command, stdin, or environment references a
resource produced by an earlier step, that reference is both the data flow and
the scheduling order — the runner waits for the producer first:
Each step consumes the previous step's output file, so they execute strictly in order. Steps whose inputs are unrelated to each other should be declared as parallel branches instead (see Concurrency).
Optional inputs make reusable commands clean. Inside a module, an unbound optional input (and any nested list group containing only unbound optionals) is omitted from argv rather than passed as an empty string:
#Artifacts
Activities are isolated: files produced by one activity are not automatically visible to the next. Artifacts are the way to hand files between activities.
#Uploading: step.upload_artifact
namenames the artifact so downstream activities can reference it viadiscover.artifacts.subdomains.entriesmap files from the activity workspace (source) into the artifact layout (destination).expires_incontrols retention — useful for noisy raw scan output you do not need to keep long.
#Downloading: step.download_artifact
step.download_artifact(artifact)pulls a previously uploaded artifact into the current activity workspace. There is no separate blob-store import primitive: shared wordlists, nuclei templates, or resolver lists travel through artifacts.
#Reading a dataset: step.download_dataset
step.download_datasetexports a project dataset's current logical state into the activity workspace ("csv"by default, or"jsonl"/"json"). Use it to diff new findings against knowledge from earlier runs. See Datasets.
#Choosing Between script and exec
Use step.script when... | Use step.exec when... |
|---|---|
| You want pipes, redirection, variable expansion | You want a single tool invocation |
| Gluing multiple commands together | Precision matters: exact argv, exact env |
| Prototyping a phase quickly | Building reusable, typed steps |
Both fail the activity on non-zero exit unless allow_failure = True.