Skip to content

run() vs pipe()

ActionRunner gives you two ways to execute a pipeline. The right choice depends on whether you’re processing one context or many, and how you want errors handled.

Executes the pipeline once with a single context. Returns the final context value directly, and throws if any activity errors.

const builder = new ActionBuilder(new MyAction())
const runner = new ActionRunner(builder)
try {
const result = await runner.run({input: "data"})
console.log(result) // final context value
} catch (error) {
console.error("Pipeline failed:", error)
}

Reach for run() when:

  • You’re processing a single context.
  • You want errors to throw immediately.
  • You prefer traditional try/catch error handling.

pipe(contexts, maxConcurrent) — concurrent batch

Section titled “pipe(contexts, maxConcurrent) — concurrent batch”

Executes the pipeline concurrently across an array of contexts, capped at maxConcurrent (default 10). Returns an array of settled results; an individual context failing is captured as a result rather than thrown. (A batch-level failure — a setup/cleanup hook throwing — still throws; see Lifecycle differences below.)

const builder = new ActionBuilder(new MyAction())
const runner = new ActionRunner(builder)
const contexts = [{id: 1}, {id: 2}, {id: 3}]
const results = await runner.pipe(contexts, 4) // up to 4 concurrent
results.forEach((result, i) => {
if (result.status === "fulfilled") {
console.log(`Context ${i} succeeded:`, result.value)
} else {
console.error(`Context ${i} failed:`, result.reason)
}
})

Reach for pipe() when:

  • You’re processing multiple contexts in parallel.
  • You want to control concurrency.
  • You need all results — successes and failures alike.
  • Error handling belongs at the call site, not inside the framework.

pipe() returns results in the same shape Promise.allSettled() produces — a concurrent worker pool captures each context’s outcome, so every element of the returned array is a settlement object:

  • {status: "fulfilled", value: <result>} — the pipeline succeeded.
  • {status: "rejected", reason: <error>} — the pipeline threw.

This is a deliberate design choice: error-handling responsibility stays at the call site. One failing context never aborts the rest of the batch — you decide what to do with each outcome.

The two entry points don’t just differ in error handling — they run different lifecycles:

  • setup/cleanup hooks are pipe()-only. The setup and cleanup lifecycle hooks belong to the batch: they run once before and once after a pipe() call. A bare run() does not invoke them. If a hook depends on setup to open a resource, drive the runner with pipe() — pass a single-element array (pipe([ctx])) for one item.
  • before$/after$ hooks fire under both. Per-activity hooks run for every activity regardless of which entry point you use.
  • pipe() runs each context through run() internally. So whatever is true of a single run() — including that done() runs once per execution — happens once per context under pipe().
run(context)pipe(contexts, maxConcurrent)
InputOne contextArray of contexts
ConcurrencyN/A (single run)Configurable (default 10)
Return valueFinal context valueArray of settled results
On a context’s errorThrowsCaptured as {status: "rejected", reason}
On a setup/cleanup failureN/A (not invoked)Throws (aborts the batch)
setup/cleanup hooksNot invokedRun once per batch
before$/after$ hooksRunRun
done()OnceOnce per context
Error handlingtry/catchInspect each result’s status