BountyHub Docs
Workflow Quick Reference
Copy-paste BHW patterns for AI assistants and humans: workflow skeleton, steps, transforms, fan-out, modules, agents, datasets, decisions, and config.
This page is the machine-readable cheat sheet for the BHW workflow language. Every snippet below is canonical syntax. Prefer these shapes when suggesting workflow edits. Details live on the linked pages.
#Skeleton
- File must produce a
workflow(...)value. Single-activity shorthand:workflow(name = ..., triggers = [...], steps = [...]). - Activities are wired by variable, not by
namestring:trigger.on(discover.succeeded()),discover.failed(),discover.completed(),discover.artifacts.<name>,discover.when(cond). - Events compose:
event.all(a, b),event.any(a, b), nestable.
#Step identity and conditions
idis the machine key (run.steps.<id>/run.steps["<id>"]);nameis the UI label. Noidmeans no stablerun.stepskey.runfields:run.ok(bool),run.inputs(dict),run.steps(dict ofstep_resultwithstatus/conclusion),run.trigger.- Compare against
steps.Status/steps.Conclusion(SUCCEEDED,FAILED,SKIPPED). Default condition isrun.ok. .when(F)is the fluent form ofcond = F;Fis a runtime predicaterun -> bool— a lambda or a step predicate such asscan.succeeded().- Most steps accept
id,name,cond,allow_failure,timeout.step.spawnacceptsnameandtimeoutonly; itsallow_failureparameter is accepted but ignored.
#step.script vs step.exec
script= explicit shell (shell = "bash"default).exec= direct argv, no shell;command[0]is the program.execreceives exactly what is declared: no implicit env, stdin, or prior stdout.stdin/stdout/stderrmust bepath.file(...);cwdmust bepath.directory(...). Non-zero exit fails unlessallow_failure = True.commandelements: strings, step-output paths,config.*refs, module input refs, lazy expressions. Nested list = optional argv group (expands only when all referenced optional module inputs are bound). Bare optional module input = dropped when unbound.
#Step outputs
A step declares what it publishes; consumers use the step result, never the producer's file name. See Step Outputs.
run.outputis the single declared output;.outputs.<name>addresses several.stdout = path.file(...),step.transform(destination = ...)andstep.agent(output = ...)expose that file asrun.output.outputs = {...}declares files a tool writes itself; directory and glob values capture trees and sets.- Module outputs are read through the invocation, e.g.
scan.outputs.findings(orscan.outputwhen the module declares one). workflow(isolated_steps = True)gives each data step its own private filesystem; outputs are then the only data that crosses a step boundary.
#Durations
Strictly positive. Used for activity/step timeout and artifact expires_in.
#Dispatch inputs
Dispatch inputs are input.string / input.bool only. There is no
input.file dispatch input.
Read at runtime as run.inputs["domain"] (or
lambda run: ...run.inputs["domain"]...).
#Transforms
- Sources:
.lines()→Items<string>,.text()→string,.json()→json,.jsonl()→Items<record>. Barepath.file("x")without a parse is rejected ("raw file"). Explicit form:source = path.file("x")+pipeline = transform.lines().... - String ops (element-wise):
trim,lower,upper,replace(from, to),regex(pattern),regex_replace(pattern, replacement). - Collection ops:
unique,take(n),drop(n),sort,reverse,split(sep),join(sep). - Lambda ops (deferred, lambda or named
def):map(lambda h: "{}:8080".format(h)),filter(lambda row: row.severity == "high"),flat_map(lambda row: row.urls). - Memory model: element-wise/streaming/stateful are streaming;
sort/reverse/joinbuffer the whole sequence. - Partitioning (terminal; only
partition.count(n)/partition.items(n), bare ints rejected):transform.trim().unique().partition(partition.items(500))withdestination = path.file("batch-{partition_seq}.txt"). Partitioned output requires{partition_seq}; non-partitioned forbids it. Result types asItems<Batch<T>>; materialize withbatch.as_file(). - Records:
row.field,row.host.lower(),row.urls[0].
#step.for_each
items: list,path.glob(base, pattern), or anyItems<T>expression.do: one-parameter function, deferred per item; no magic variable.Batch<T>is execution-local, not a file — call.as_file()for tools.- Concurrency guide: CPU-bound ~5, network probes ~50, aggressive scanners moderate.
#Concurrency
paralleltakes an outer list of branches (each an inner sequential list); flat lists rejected;parallel([])is a no-op. Fails if any branch fails; siblings are never cancelled.cond/allow_failureallowed on the barrier.spawnreturns a handle (sync reference, not data);joinblocks until all succeed.step.spawnacceptsnameandtimeout; itsallow_failureparameter is accepted but ignored. Handles never merge data — share files via workspace/artifacts.
#Modules
step.module only invokes modules imported with load() from the module
registry. A local module(...) value defined in the same file cannot be
passed to step.module.
- Module outputs are read through the invocation value using a declared
output name:
enum_run.outputs.subs. Aload()symbol is the module definition, not an invocation, and has no.outputs; bind the invocation first (enum_run = step.module(...)). Outputs are file references, usable as module inputs,step.execargv elements, andartifact_entry(source = ...)sources. They are not<activity>.artifacts.<name>; that namespace only exists afterstep.upload_artifact. - A module runs in an isolated workspace: its
file/directoryinputs are copied in, it cannot read the caller's other files, and only its declared outputs are copied back (as<module-path>.<name>). Never read a producer's internal file name; use the invocation's.outputs.<name>. input.string/bool/file/directory(name, ...)are public refs;output.filelikewise.input.string(..., secret = True)marks a credential, and callers bind string inputs from literals orconfig.var(...)/config.secret(...). Each declared output assigned exactly once viastep.output.- Module files compile in module mode: no
workflow/activityglobals and no project-context namespaces (config,dataset,mcp). Modules are self-contained and bind such values through their declared inputs; agent steps stay available (provider.key) but are explicit parameters. Unbound optional inputs use theirdefault.
#Agents
provideris required and must be enabled for the project (unknown_provider_keyotherwise).modelis optional.contextis a list ofagent.context.*(dict form rejected):text(description, value),path(description, path),artifact(description, artifact),dataset(description, dataset). Resources pass by reference, never injected wholesale.tools:tool.shell(),mcp.server("name")(preconfigured connections). Output defaults to free-form text.
#Datasets
mappingkeys are columns; values evaluate againstrow(row.valuefor lines,row.fieldfor JSONL).tablenames an existing project table (ordataset.table("name")reference elsewhere).step.download_datasetexports the dataset's current logical state into the workspace ("csv"default, or"jsonl"/"json"); the step exposes it asrun.output.modeis the string"upsert"or"sync":"upsert"accumulates;"sync"marks absent identities stale. Identity/reconciliation owned by the dataset service. Thedataset.upsert()/dataset.sync()values are not accepted here.
#Decisions
- Question types:
decision.noul(question)(binary, optional confidence),decision.choice(question, options = [...])(one of a fixed set),decision.score(question, min, max)(bounded integer). provideris optional and selects the implementation:"jevos"(the default, the built-in live jevos decision server reached atendpoint) or"mock"(deterministic, no network, for tests). Any other name is rejected.endpoint/model/api_keyare optional and resolved at execution time;api_keymust be aconfig.secret(...)reference.- jevos limits: a
choiceneeds 2–26 options and ascoreneeds 2–10 levels. - The result is written to
outputas a typed JSON document keyed by question name. It is model-produced data, not proof: answers are validated against the declared questions before the result is written, and a self-reportedprobabilityis not a calibrated probability.
#Config
- References only; resolved at execution time, never serialized. Secrets stay sensitive through concat/trim/casing. Scope (global/org/project/environment) hidden from the DSL.
#Artifacts and blobs