BountyHub Docs
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 and the pages that follow it.
#Anatomy of a Workflow File
Key rules:
- Every workflow file must produce a
workflow(...)value. - Activities are referenced by the variables they are bound to, not by their
namestrings. step.module(...)returns the invocation; its declared outputs are only reachable through it asinvocation.outputs.<name>(e.g.probe_run.outputs.domains), never as<activity>.artifacts.<name>. Aload()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:
For simple single-activity workflows you can use the shorthand form, which builds one synthetic activity:
#activity(...)
An activity is a named unit of work with its own triggers, condition, environment, steps, and optional timeout:
triggersdecide when the activity starts (see 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.envsets environment entries for the activity. Values are plain strings; useconfig.*references inside individual steps when values must be resolved at execution time.timeoutbounds the whole activity; individual steps accept their owntimeout.
#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.<name> | 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:
#Events: trigger.on(event_expr)
Start when another activity reaches a terminal state:
Events are declarative subscriptions — they describe what to react to, not
when to evaluate. Compose them with event.all(...) and event.any(...):
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:
Inputs are available at runtime through the run context as run.inputs. For
example, build the command line from the dispatched 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.
#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:
.when(F) is the fluent spelling of cond = F and works on both steps and
activities:
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:
#Durations
Timeouts are expressed with the duration namespace. Constructors compose
fluently:
Use them on activities and steps:
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). - Everything is validated when the workflow is saved: unknown names, type mismatches, and unbound required inputs are compile-time errors, not runtime surprises.