---
title: registerLifecycleHooks
description: Register global handlers that observe workflow runs completing or failing.
type: reference
summary: Use registerLifecycleHooks to observe run completions and failures from one central place.
prerequisites:
  - /docs/foundations/workflows-and-steps
related:
  - /docs/observability/lifecycle-hooks
  - /docs/api-reference/workflow-api/get-run
---

# 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 early in the process lifecycle (in Next.js, `instrumentation.ts`) so handlers exist before the first run finishes. See the [lifecycle hooks guide](/docs/observability/lifecycle-hooks) for semantics and a full Sentry example.

```typescript title="instrumentation.ts" lineNumbers
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.

## API Signature

### Parameters

<TSDoc
  definition={`
import { registerLifecycleHooks } from "workflow/api";
export default registerLifecycleHooks;`}
  showSections={["parameters"]}
/>

### 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`](/docs/api-reference/workflow-api/get-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.

### `onRunCompleted`

Invoked when a workflow run completes successfully.

| Parameter             | Type     | Description                                                                                                                          |
| --------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| `params.run`          | `Run`    | The completed run.                                                                                                                   |
| `params.workflowName` | `string` | The 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).

| Parameter             | Type                     | Description                                                                                                                                                    |
| --------------------- | ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `params.run`          | `Run`                    | The failed run.                                                                                                                                                |
| `params.workflowName` | `string`                 | The machine-readable workflow identifier, such as `workflow//./src/workflows/order//processOrder`. Available without a backend read.                           |
| `params.error`        | `WorkflowRunFailedError` | The 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 pipe and lock-polling setup during hydration. If hydration fails, 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.

The invocation's `waitUntil` scope includes background stream operations from the hydrated cause, even after a handler returns or throws. Close or release stream reader and writer locks when finished so that work can settle. Await other asynchronous reporting work in your handler to keep it in the same lifetime scope.

## Behavior

* Handlers run on the host (full Node.js), never inside the workflow VM. Calling `registerLifecycleHooks` from workflow 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](/docs/how-it-works/event-sourcing) 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.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)