Skip to content

feat: .parallel() — concurrent step groups (closes #20)#31

Merged
danfry1 merged 13 commits into
mainfrom
feat/parallel-steps
May 13, 2026
Merged

feat: .parallel() — concurrent step groups (closes #20)#31
danfry1 merged 13 commits into
mainfrom
feat/parallel-steps

Conversation

@danfry1
Copy link
Copy Markdown
Owner

@danfry1 danfry1 commented May 13, 2026

Summary

  • Adds workflow.parallel({ branchName: handler | config, ... }) for running independent steps concurrently. The next step's prev is a typed merged record of all branch outputs.
  • Per-branch retry and timeout config, fail-fast on first branch failure, per-branch crash recovery (completed branches are skipped on resume — no duplicated side effects), and cause-aware failure reporting (onRunFailed / onFailure surface the branch that caused the abort, not a sibling aborted by signal propagation).
  • New ParallelCompleteError thrown if a branch calls complete().

This closes #20, originally suggested by @brianjenkins94. Thanks again Brian — three of the last four feature ideas have come from you.

Design note — and a question for @brianjenkins94

Your issue referenced GitHub Actions' needs: model, which is an arbitrary DAG. I went with a structured .parallel() group instead. Reasoning:

  • Preserves type narrowing on prev (a free-form DAG breaks that — what's the prev type when multiple disjoint paths could precede a step?).
  • Matches Reflow's linear-builder philosophy.
  • Avoids topological sort, cycle detection, and merge-policy decisions.
  • Covers fan-out / fan-in cleanly: A → (B, C, D) → E, which is ~90% of the "parallel" use cases I could think of.

What it cannot express that needs: could:

  • Non-rectangular DAGs (e.g. D depends only on A, runs alongside an independent B → C chain).
  • Skip-level dependencies.

Does .parallel() cover your actual workload? If you have a case in mind that genuinely needs arbitrary-DAG expressiveness, I'd rather know now than ship .parallel() and have it not fit. Happy to revisit the design before merging.

What's in the diff

  • src/core/workflow.ts.parallel() builder, ExecutionUnit model.
  • src/core/engine.tsexecuteParallelGroup, per-branch resume, Promise.allSettled, cause-tracking, abort-aware retry bail.
  • src/core/__tests__/parallel-engine.test.ts — 25 tests covering builder, types, fail-fast, retries, timeouts, hooks, crash recovery, cancellation, sequential parallel groups.
  • examples/4-parallel-enrichment.ts — runnable fan-out/fan-in example.
  • README.md, docs/docs.html, docs/llms.txt — new "Parallel Steps" section + API reference.
  • CHANGELOG.md — entry under 0.4.0.

Test plan

  • bun run test — 291/291 pass
  • bun run typecheck — clean
  • bun run build — clean
  • oxlint — clean
  • npx tsx examples/4-parallel-enrichment.ts — runs end-to-end in ~1.2s (vs ~3.2s sequential)

danfry1 added 12 commits March 30, 2026 23:09
- Extract 180-line parallel block into executeParallelGroup() with
  typed ParallelGroupResult return, reducing executeRun nesting
- Add single-branch parallel group test
- Add engine.cancel() during parallel execution test
- Add sequential parallel groups prev-chaining test
- Add @internal to BranchFailedError
Resume parallel groups per-branch instead of all-or-nothing: completed
branches are skipped on retry and only the missing ones re-run, so side
effects are not duplicated. Failed/completed rows now upsert in place
rather than appending a new row each attempt.

Track which branch caused the group to abort so onRunFailed / onFailure
report the underlying cause, not a sibling that was aborted by signal
propagation. Switched to Promise.allSettled so concurrent rejections do
not surface as unhandled rejections. Bail out of the retry loop once the
group is aborted to avoid pointless iterations.

Adds tests covering per-branch resume, no-duplicate rows, hook
semantics on resume, cause-of-abort reporting, no-unhandled-rejection,
and no-retry-after-abort.
- README: new "Parallel Steps" section with semantics callouts and a
  workflow.parallel() API reference entry
- docs/docs.html: matching section + API card with nav links
- docs/llms.txt: parallel section + API entry kept in sync
- examples/4-parallel-enrichment.ts: runnable fan-out/fan-in example
- CHANGELOG: entry under 0.3.0 crediting @brianjenkins94 (#20)
0.3.0 has already been released — restore that section to its
released contents and create a new 0.4.0 entry for the .parallel()
work.
@brianjenkins94
Copy link
Copy Markdown

three of the last four feature ideas have come from you.

I am your repository's number 1 fan 🤣

This seems like a solid solution. I generally found needs: [] to be repetitive and unergonomic.

CI's coverage check was failing because engine.ts dropped to ~87% lines
after adding executeParallelGroup. Two small changes get us back over:

- Remove three unreachable defensive blocks: the 'early-complete' branch
  in executeParallelGroup (guardedComplete throws ParallelCompleteError,
  not EarlyCompleteError, so executeStep never returns 'early-complete'
  for parallel branches), the EarlyCompleteError escape check in the
  outer catch (same reason), and the lastError-RunControlError rethrow in
  executeStep (RunControlError is rethrown inside the retry loop, never
  stored in lastError).

- Add three targeted tests for genuinely-reachable paths: lease loss
  during success-path persistence (storage returns false from
  saveStepResult mid-loop), cancellation landing before the parallel
  group starts, and heartbeat loss mid-branch (the cause-branch fallback
  recognising a RunControlError reject).

Coverage: 93.83% lines / 93.68% statements / 84.03% branches.
@danfry1 danfry1 merged commit a193785 into main May 13, 2026
3 checks passed
@danfry1 danfry1 deleted the feat/parallel-steps branch May 13, 2026 21:58
@danfry1
Copy link
Copy Markdown
Owner Author

danfry1 commented May 13, 2026

Correction: it's actually all 5 substantive feature issues on the repo — I miscounted. With this one merged that's 2 of your 5 ideas already shipped (this + the start hooks in 0.3.0), and #21/#23/#24 are sitting in your draft #27. Genuinely appreciate the steady stream of well-thought-out reports — they've been doing a lot of the product-thinking for me.

Thanks for the quick look. Merging now. 🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature Request] Run steps simultaneously

2 participants