SIGN IN SIGN UP

feat(core): user-registerable workflow lifecycle hooks (registerLifecycleHooks) (#3678)

* feat(core): user-registerable workflow lifecycle hooks

Adds registerLifecycleHooks (exported from workflow/api) so apps can
observe run terminal transitions from one central place — e.g. report
every failed run to Sentry from instrumentation.ts — without wrapping
each workflow body.

- onRunCompleted/onRunFailed handlers receive the lazily-hydrated Run
  instance; onRunFailed additionally receives a WorkflowRunFailedError
  whose errorCode carries the classification and whose cause is the
  hydrated thrown value (round-tripped through the run-error
  serialization pipeline, so VM-realm throws surface as host-realm
  Errors with class identity preserved — same shape run.returnValue
  rejects with).
- Registry lives on globalThis under Symbol.for so every bundled copy
  of @workflow/core shares one list; multiple registrations allowed,
  handlers run in registration order, and registration returns an
  unregister function.
- Dispatch is fire-and-forget via safeWaitUntil: handlers can't delay
  or change the run outcome, failures are logged and swallowed, and
  serverless invocations stay alive while handlers finish.
- Wired into every terminal writer that lands a run_completed or
  run_failed event: the happy-path completion, the terminal catch, the
  suspension-commit failure, recordFatalRunError, the max-deliveries
  gate, replay-budget exhaustion, the deployment guard, and both
  QuickJS entrypoint writers — and only on the invocation whose write
  actually succeeded (never on EntityConflict/RunExpired).
- e2e coverage registers handlers in the Next.js workbenches'
  instrumentation.ts and reports observations by resuming a durable
  hook, so the channel works across serverless instances.
- Docs: observability guide with a Sentry example + workflow/api
  reference page.

Co-Authored-By: Nathan Rajlich <71256+TooTallNate@users.noreply.github.com>

* Load lifecycle-hooks-e2e via guarded dynamic import; comment fixes

The static top-level import in instrumentation.ts pulled workflow/api →
world-init → @workflow/world-local → proper-lockfile → fs into every
compile target of instrumentation.ts, and the non-node ones cannot
resolve fs — 500ing every request in the nextjs-webpack lanes. The
NEXT_RUNTIME guard only helps at runtime; the canonical Next.js pattern
is a dynamic import inside the guard so each compile target dead-code-
eliminates the branch. Verified: lifecycle + pages-router e2e slices
green against local nextjs-webpack and nextjs-turbopack dev servers.

Also: clarify the 'describe block' doc comment and correct the
microtask/macrotask wording in the test flush helper.

Co-Authored-By: Nathan Rajlich <71256+TooTallNate@users.noreply.github.com>

* Remove em dashes from docs and comments added on this branch

Per the Vercel technical writing guidelines: em dashes create ambiguity
for agents parsing sentence boundaries. Replaced with periods, commas,
parentheses, or colons across the new docs pages, the changeset, and
the code comments this branch adds. Pre-existing em dashes elsewhere
are left for the repo-wide docs audit.

Co-Authored-By: Nathan Rajlich <71256+TooTallNate@users.noreply.github.com>

* Apply Vercel technical writing rules to the lifecycle hooks docs

Beyond the em-dash pass: the guide's intro now leads with what
lifecycle hooks let you do (the first sentence is parseable as the
page's purpose) instead of opening with the failure mode; passive
constructions are active ('the runtime keeps the invocation alive
with waitUntil', 'the runtime logs and swallows a throwing handler',
'you can register multiple hook sets', 'the backend writes that
transition'); the one-word 'Semantics' heading is now the standalone
statement 'How handlers behave'; and the key lazy-hydration sentence
names the Run instance instead of leading with a pronoun so it reads
correctly when extracted alone.

Co-Authored-By: Nathan Rajlich <71256+TooTallNate@users.noreply.github.com>

* fix(core): hydrate lifecycle failures from persisted errors

Use persisted bytes and keys for lifecycle reporting, defer external reads, and expose workflow names without fetching runs. Preserve handler error diagnostics, strengthen regression coverage, and correct Next.js examples and best-effort delivery documentation.

Co-Authored-By: Nathan Rajlich <71256+TooTallNate@users.noreply.github.com>

* fix(core): sort QuickJS imports after rebase

Co-Authored-By: Nathan Rajlich <71256+TooTallNate@users.noreply.github.com>

---------

Co-authored-by: vercel[bot] <35613825+vercel[bot]@users.noreply.github.com>
Co-authored-by: Nathan Rajlich <71256+TooTallNate@users.noreply.github.com>
N
Nathan Rajlich committed
20ad2b358240819c6e590fd4752b97da1c64b390
Parent: 59a7c5b
Committed by GitHub <noreply@github.com> on 9/16/2026, 10:52:14 PM