# For Each > Bounded data-parallel execution over targets with step.for_each. Recon often means doing the same thing thousands of times: probe every subdomain, screenshot every host, fuzz every parameter. `step.for_each` is the bounded data-parallel primitive for exactly that. ## Basic Shape ```bhw step.for_each( items = ..., do = lambda item: [...], # returns a list of steps for one item concurrency = 20, # max iterations in flight, default 20 ) ``` - `items` — what to iterate over (see below). - `do` — an ordinary one-parameter function. It is **deferred**: stored as a template and invoked once per item at runtime. The item is bound to its single parameter; there is no magic `item` variable. Lambdas and named `def` functions both work. - `concurrency` — upper bound on parallel iterations, so you do not get your IP rate-limited or your runner OOM-killed at 3 AM. ```bhw activity( name = "probe-subdomains", triggers = [trigger.on(discover.succeeded())], steps = [ step.for_each( items = path.file("subs-clean.txt").lines(), do = lambda sub: [ step.exec(command = ["httpx", "-u", sub, "-title", "-status-code"]), ], concurrency = 50, ), ], ) ``` A named function is equivalent and reads better for multi-step bodies: ```bhw def probe(target): return [ step.exec(command = ["httpx", "-u", target, "-title", "-status-code"]), ] step.for_each( items = path.file("subs-clean.txt").lines(), do = probe, concurrency = 50, ) ``` ## What Can Be Items ### Lists Plain Starlark lists work: ```bhw step.for_each( items = ["dirs-common.txt", "dirs-api.txt", "dirs-backup.txt"], do = lambda wl: [ step.exec(command = ["ffuf", "-w", wl, "-u", "https://acme-example.com/FUZZ"]), ], ) ``` ### Files and globs `path.glob(base, pattern)` enumerates files matching a pattern — this is how you fan out over partitioned transform output or per-target result files. Both arguments are required; prefer the keyword form: ```bhw step.for_each( items = path.glob(base = "batches", pattern = "scan-*.txt"), do = lambda batch_file: [ step.exec(command = ["nuclei", "-l", batch_file, "-severity", "critical,high"]), ], ) ``` ### Typed transform expressions Any typed `Items` expression can be iterated directly, including partitioned transforms (see [Transforms](/workflows/transforms#partitioning)): ```bhw step.for_each( # ~20 balanced batches of unique live hosts. items = path.file("live.txt").lines().unique().partition(partition.count(20)), do = lambda batch: [ step.exec(command = ["nuclei", "-l", batch.as_file(), "-o", "findings.txt"]), ], ) ``` A `Batch` is an execution-local group of items, not a file. Call `.as_file()` explicitly when a tool needs the batch materialized on disk. ## Multi-Step Bodies The body returns a list of steps, so each iteration can be a small sequential pipeline of its own: ```bhw step.for_each( items = path.file("targets.txt").lines(), do = lambda target: [ step.exec(command = ["katana", "-u", target, "-d", "3", "-o", "urls-{}.txt".format(target)]), step.exec(command = ["dalfox", "file", "urls-{}.txt".format(target), "-o", "xss-{}.txt".format(target)]), ], concurrency = 10, ) ``` Iterations are independent: one target failing does not stop the others. The whole `for_each` step fails only after all iterations complete if any failed (unless `allow_failure = True`). ## Conditions Inside Bodies Steps inside the body support the usual `cond`, so you can branch per item: ```bhw step.for_each( items = path.file("hosts.txt").lines(), do = lambda host: [ step.exec( command = ["nuclei", "-u", host, "-tags", "exposure"], allow_failure = True, ), step.script(content = "echo done with {}".format(host)), ], ) ``` ## Tuning Concurrency Pick `concurrency` like you would pick threads for your own tooling: - CPU-bound tools (screenshots, headless browsers): low, e.g. `5`. - Network-bound probes (`httpx`, `subfinder`): higher, e.g. `50`. - Aggressive scanners (`nuclei` with all templates): moderate, and respect the program's scope and rate limits — being first to report is worthless if you get banned for hammering infrastructure.