# Workflow Syntax Reference > Complete reference for the BountyHub workflow language: workflows, activities, triggers, conditions, inputs, and durations. BountyHub workflows are written in Starlark — a small, deterministic, Python-like configuration language. A workflow file is a Starlark program whose result is a `workflow(...)` definition. This page is the complete reference for the structural parts of the language. Step primitives are covered in [Steps](/workflows/steps) and the pages that follow it. ## Anatomy of a Workflow File ```bhw # Optional: load reusable modules (see /workflows/modules) load("projectdiscover/http-probe", "http_probe") # Activities are bound to variables so they can be referenced by # other activities' triggers and conditions. discover = activity( name = "discover", triggers = [trigger.cron("0 0 * * *"), trigger.dispatch()], steps = [ step.exec(command = ["subfinder", "-d", config.var("target_domain"), "-o", "subs.txt"]), ], ) # `step.module(...)` returns the invocation; its declared outputs are read # through it as `probe_run.outputs.`. probe_run = step.module(http_probe, inputs = {"targets": path.file("subs.txt")}) probe = activity( name = "probe", triggers = [trigger.on(discover.succeeded())], steps = [probe_run], ) # The workflow definition is the value of the program. workflow( name = "subdomain-recon", activities = [discover, probe], ) ``` Key rules: - Every workflow file must produce a `workflow(...)` value. - Activities are referenced by the variables they are bound to, not by their `name` strings. - `step.module(...)` returns the invocation; its declared outputs are only reachable through it as `invocation.outputs.` (e.g. `probe_run.outputs.domains`), never as `.artifacts.`. A `load()` symbol is the module definition and has no outputs. - The language is a strict subset of Starlark plus BountyHub builtins — no arbitrary I/O, no imports beyond `load()` of published modules. ## `workflow(...)` Defines a workflow from a list of activities: ```bhw workflow( name = "recon", # required activities = [a, b, c], # list of activities, default [] ) ``` For simple single-activity workflows you can use the shorthand form, which builds one synthetic activity: ```bhw workflow( name = "quick-nuclei", triggers = [trigger.cron("0 */6 * * *")], steps = [ step.exec(command = ["nuclei", "-l", "live.txt", "-o", "findings.txt"]), ], ) ``` ## `activity(...)` An activity is a named unit of work with its own triggers, condition, environment, steps, and optional timeout: ```bhw activity( name = "scan", # required triggers = [trigger.cron("0 0 * * *")], # list of triggers cond = None, # run-level condition env = {"NUCLEI_SEVERITY": "critical"}, # dict of string -> string steps = [...], # required: list of steps timeout = duration.hours(2), # optional activity timeout ) ``` - `triggers` decide when the activity starts (see [Triggers](#triggers)). An activity with no trigger never starts on its own; it can only be started by an event reference from another activity's trigger. - `env` sets environment entries for the activity. Values are plain strings; use [`config.*`](/workflows/config) references inside individual steps when values must be resolved at execution time. - `timeout` bounds the whole activity; individual steps accept their own `timeout`. ### Activity values Binding an activity to a variable gives you an **activity value** used for wiring:
Expression Meaning
discover.succeeded() Event: activity finished successfully
discover.failed() Event: activity failed
discover.completed() Event: activity reached any terminal state
discover.artifacts. Reference to a named artifact produced by the activity
discover.when(cond) Copy of the activity that only runs when the runtime predicate cond holds ( lambda run: ... )
## Triggers Triggers decide when an activity starts. ### Schedule: `trigger.cron(expr)` Standard cron expression: ```bhw activity( name = "nightly-full-recon", triggers = [trigger.cron("0 0 * * *")], # every day at midnight steps = [...], ) ``` ### Events: `trigger.on(event_expr)` Start when another activity reaches a terminal state: ```bhw probe = activity( name = "probe", triggers = [trigger.on(discover.succeeded())], steps = [...], ) ``` Events are declarative subscriptions — they describe *what to react to*, not *when to evaluate*. Compose them with `event.all(...)` and `event.any(...)`: ```bhw # Run only when BOTH discovery succeeded AND the previous notify completed. report = activity( name = "report", triggers = [ trigger.on(event.all(discover.succeeded(), notify.completed())), ], steps = [...], ) # Run when ANY monitored target finishes scanning. alert = activity( name = "alert", triggers = [ trigger.on(event.any(scan_web.succeeded(), scan_api.succeeded())), ], steps = [...], ), ``` Compositions nest freely (`event.any(a, event.all(b, c))`). ### Manual dispatch: `trigger.dispatch(inputs = {...})` Start the activity manually (from the UI or CLI), optionally declaring inputs the dispatcher must provide: ```bhw scan = activity( name = "on-demand-scan", triggers = [ trigger.dispatch(inputs = { "domain": input.string(required = True, description = "Target domain"), "aggressive": input.bool(default = False, description = "Enable aggressive mode"), }), ], steps = [ step.script(content = "echo starting on-demand scan"), ], ) ``` Inputs are available at runtime through the run context as `run.inputs`. For example, build the command line from the dispatched domain: ```bhw step.script( # Function content is evaluated at execution time with the run context. content = lambda run: "subfinder -d {} -o subs.txt".format(run.inputs["domain"]), ) ``` Dispatch input declarations:
Declaration Parameters
input.string(...) default="" , required=False , description
input.bool(...) default=False , required=False , description
Dispatch inputs are `input.string` / `input.bool` only; there is no `input.file` dispatch input. Module files use a separate namespace with explicit public names (`input.string(name = ...)`, `input.bool(...)`, `input.file(...)`, `input.directory(...)`); see [Modules](/workflows/modules). ## Conditions Conditions gate whether an activity or step runs. The default condition is `run.ok`: run only if everything before it succeeded. You can override it with a lambda that receives the **run context**:
Field Type Meaning
run.ok bool Whether all previous steps succeeded
run.inputs dict Dispatch inputs of the current run
run.steps dict of step_result Results of previous steps, keyed by step id
run.trigger trigger context What started this run ( dispatch , cron , or on )
A `step_result` exposes `status` and `conclusion`, compared against the `steps.Status` and `steps.Conclusion` enums (`SUCCEEDED`, `FAILED`, `SKIPPED`). Steps are addressable by the `id` you give them (`step.exec(..., id = "scan")` is read as `run.steps.scan` or `run.steps["scan"]`). Without an `id`, a step has no stable key in `run.steps`. `name` is the optional human-facing label shown in the UI; `id` is the machine key used by conditions. A step predicate is the concise form of the same check:
Predicate Meaning
scan.succeeded() run -> bool : true when the referenced step succeeded
scan.failed() run -> bool : true when the referenced step failed
Both require the referenced step to have an `id`. They are deferred: the runner evaluates them right before the dependent step runs. Examples: ```bhw scan = step.exec( command = ["nuclei", "-l", "live.txt", "-o", "findings.txt"], id = "scan", ) # Only notify when the scan step actually found something worth paging on. notify = step.script( content = "cat findings.txt | notify -provider slack", cond = scan.succeeded(), ) # Skip the expensive deep scan when the probe step was skipped. deep_scan = step.exec( command = ["nuclei", "-l", "live.txt", "-es", "info", "-o", "deep.txt"], cond = lambda run: run.steps["probe"].status == steps.Status.SUCCEEDED, ) # Run cleanup regardless of earlier failures. cleanup = step.script( content = "rm -rf workdir/", allow_failure = True, cond = lambda run: True, ) ``` `.when(F)` is the fluent spelling of `cond = F` and works on both steps and activities: ```bhw recheck = step.exec(command = ["httpx", "-l", "live.txt"]).when(scan.succeeded()) notify = step.script(content = "notify ...").when(lambda run: run.ok) ``` A condition must be a runtime predicate. Static booleans (`cond = True`, `.when(True)`) are rejected so a condition can never be evaluated while the workflow is being defined. Step predicates only see earlier steps in the same activity; cross-activity wiring uses `trigger.on(scan.succeeded())`. Activities also accept conditions, either directly (`cond = ...`) or fluently via `.when(...)`, which returns a copy of the activity that only runs when the condition holds: ```bhw rescan = activity( name = "rescan", triggers = [trigger.on(scan.failed())], steps = [...], ).when(lambda run: run.ok) ``` ## Durations Timeouts are expressed with the `duration` namespace. Constructors compose fluently: ```bhw duration.seconds(30) duration.milliseconds(500) duration.minutes(15) duration.hours(1).minutes(30) # 1h30m duration.days(7) duration.parse("2h45m") # parse a string ``` Use them on activities and steps: ```bhw activity( name = "portscan", triggers = [trigger.cron("0 3 * * 1")], # Mondays at 03:00 timeout = duration.hours(4), steps = [ step.exec( command = ["naabu", "-host", config.var("target_domain"), "-o", "ports.txt"], timeout = duration.hours(3).minutes(45), ), ], ) ``` Durations must be strictly positive. ## Limits Workflows enforce platform limits on structure (number of activities per workflow, steps per activity, triggers per activity, environment entries per activity). Keep activities focused — split large phases into separate activities wired by events rather than building monolithic step lists. ## Language Notes - Comments start with `#`. - Keyword arguments use `=` (as in Python): `name = "scan"`. - Strings support `"..."` and `'...'`; `.format(...)` works on strings and is commonly used with module inputs (see [Modules](/workflows/modules)). - Everything is validated when the workflow is saved: unknown names, type mismatches, and unbound required inputs are compile-time errors, not runtime surprises.