registerLifecycleHooks

Registers global workflow lifecycle handlers, invoked by the runtime on the compute that records a run's terminal transition. Use it for best-effort centralized reporting, such as forwarding failed runs to Sentry, without wrapping each workflow body.

Register in the host process that executes workflows, before it handles requests. In Next.js 16.3.0 and later, use instrumentation.ts as below. Earlier Next.js versions can process a cold Vercel invocation before async registration finishes. See the framework support table for registration locations and a full Sentry example in the guide.

App startup registration does not reach the standalone workflow functions emitted on Vercel by Nuxt 4 / Nitro v2, Astro, Nest, or the CLI vercel-build-output-api target. Those targets do not yet support this registration pattern. In Nitro v3, use a static import and synchronous server-plugin registration; Nitro does not await async plugins. Do not register from a workflow file or a step function.

instrumentation.ts
export async function register() {
  if (process.env.NEXT_RUNTIME === "nodejs") {
    const { registerLifecycleHooks } = await import("workflow/api");

    registerLifecycleHooks({
      async onRunCompleted({ run, workflowName }) {
        console.log(`Run ${run.runId} (${workflowName}) completed`);
      },
      async onRunFailed({ run, workflowName, error }) {
        console.error(
          `Run ${run.runId} (${workflowName}) failed (${error.errorCode})`,
          error.cause
        );
      },
    });
  }
}

Keep the dynamic import inside the NEXT_RUNTIME === "nodejs" guard. Next.js also compiles instrumentation.ts for the Edge runtime. A top-level static import of workflow/api pulls Node.js-only dependencies into that compilation and breaks webpack Edge builds, even if the registration call is guarded.

Restart next dev after editing instrumentation handlers: Next.js memoizes registration per process, so edits may not take effect until restart. Other hot-reloading hosts can re-register in the same process; keep the unregister function in process-wide state across reloads, or restart the server.

API Signature

Parameters

NameTypeDescription
hooksWorkflowLifecycleHooks

Returns

Returns a function that unregisters these hooks. Registrations are not deduplicated. Register each hook set once per process, and unregister the previous hooks before registering again during hot reload or module re-evaluation.

Handlers

Both handlers receive a workflowName string and a lazily hydrated Run instance. Use the workflowName parameter to filter without a backend read; run.runId also requires no read. Accessors such as run.workflowName, run.status, and run.returnValue still fetch from the backend when used. In particular, workflowName is a string, while run.workflowName is a Promise<string>. Lazy access defers those reads rather than eliminating them.

Parameters come from the runtime's module copy, which may differ from the handler's copy in a bundled app. Avoid instanceof checks against Run, SDK errors, or your own bundled error classes. Use WorkflowRunFailedError.is(error) (workflow/errors), FatalError.is(error.cause) (workflow), error.errorCode, and the cause's name or your own serialized discriminator. See identifying errors across module copies for an example.

onRunCompleted

Invoked when a workflow run completes successfully.

ParameterTypeDescription
params.runRunThe completed run.
params.workflowNamestringThe machine-readable workflow identifier, such as workflow//./src/workflows/order//processOrder. Available without a backend read.

onRunFailed

Invoked when a workflow run fails terminally (after any retries).

ParameterTypeDescription
params.runRunThe failed run.
params.workflowNamestringThe machine-readable workflow identifier, such as workflow//./src/workflows/order//processOrder. Available without a backend read.
params.errorWorkflowRunFailedErrorThe persisted failure hydrated for reporting: error.errorCode carries the classification (e.g. USER_ERROR) and error.cause is the hydrated thrown value.

Unlike run.returnValue, error.cause defers readable stream I/O until consumption and revives abort signals as persisted snapshots without live subscriptions. Writable streams retain their normal forwarding setup, and writes still reach the failed run's stream or the parent run's stream for a forwarded writable. Custom serialization revives registered classes using the runtime's module copy; plain user-defined Error subclasses without custom serialization revive as generic errors with their name preserved. If hydration fails, the SDK logs the reason and the cause is a generic Error, matching run.returnValue's fallback. In onRunFailed, run.returnValue rejects because the run failed. Use error.cause to inspect or report the thrown value instead.

On Vercel, the invocation's waitUntil scope drains background stream operations from error.cause, even after a handler returns or throws. It does not automatically cover pipes from await run.returnValue in onRunCompleted, or consumption scheduled after the handler returns. Await consumption and other reporting work in your handler. Other hosts may stop detached work when the process freezes or terminates.

A stream lock left open can keep the drain and lifecycle span pending until the function's maximum duration. Close or release reader and writer locks when finished; cancel a partially consumed reader in finally. See the stream cleanup example.

Behavior

  • Register at host startup. Handlers run on the host (full Node.js), never inside the workflow VM. Calling registerLifecycleHooks from workflow or step code throws.
  • Handlers are fire-and-forget. They cannot delay or change the run's outcome, and the runtime logs and swallows a throwing handler. On Vercel, waitUntil keeps the invocation alive while handlers finish, subject to the invocation's duration limit. On other hosts, handlers run as detached work and may not complete if the host freezes or terminates the process after the response.
  • Delivery is best effort. Each registered handler is invoked at most once by the invocation that writes the terminal event. Callbacks are not retried if they throw or the process dies, so they may never run or may stop before completing. The event log is the system of record.
  • Handlers fire only on the invocation that wrote the terminal event. Transitions recorded outside your app's compute (e.g. a run cancelled from the CLI or dashboard) do not fire handlers.
  • You can register multiple hook sets, and handlers run in registration order. Registrations are not deduplicated; unregister a previous hook set before registering it again in the same process.