---
title: createHook
description: Create a low-level hook to resume workflows with arbitrary payloads.
type: reference
summary: Use createHook to pause a workflow and resume it with an arbitrary payload from an external system.
prerequisites:
  - /docs/foundations/hooks
related:
  - /docs/api-reference/workflow/define-hook
  - /docs/api-reference/workflow/create-webhook
  - /docs/foundations/idempotency
---

Creates a low-level hook primitive that can be used to resume a workflow run with arbitrary payloads.

Hooks allow external systems to send data to a paused workflow without the HTTP-specific constraints of webhooks. They're identified by a token and can receive any serializable payload.

```ts lineNumbers
import { createHook } from "workflow"

export async function hookWorkflow() {
  "use workflow";
  // `using` automatically disposes the hook when it goes out of scope
  using hook = createHook();  // [!code highlight]
  const result = await hook; // Suspends the workflow until the hook is resumed
}
```

## API Signature

### Parameters

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

#### HookOptions

<TSDoc
definition={`
import type { HookOptions } from "workflow";
export default HookOptions;`
}
/>

### Returns

<TSDoc
definition={`
import { createHook } from "workflow";
export default createHook;`}
showSections={['returns']}
/>

#### Hook

<TSDoc
definition={`
import type { Hook } from "workflow";
export default Hook;`}
/>

The returned `Hook` object also implements `AsyncIterable<T>`, which allows you to iterate over incoming payloads using `for await...of` syntax.

Use `hook.getConflict()` (available starting in `workflow@4.5.0`) to check whether the hook token is already claimed by another active hook, without waiting for hook payload data. Calling `createHook()` on its own does not register the hook — registration is only committed when the workflow suspends. Awaiting `hook.getConflict()` suspends the workflow to commit the registration, then resolves with `null` once `hook_created` is recorded, or with `{ runId }` identifying the conflicting run if another active hook already owns the same token.

## Examples

### Basic Usage

When creating a hook, you can specify a payload type for automatic type safety:

```typescript lineNumbers
import { createHook } from "workflow"

export async function approvalWorkflow() {
  "use workflow";

  using hook = createHook<{ approved: boolean; comment: string }>(); // [!code highlight]
  console.log("Send approval to token:", hook.token);

  const result = await hook;

  if (result.approved) {
    console.log("Approved with comment:", result.comment);
  }
}
```

### Customizing Tokens

Tokens are used to identify a specific hook. You can customize the token to be more specific to a use case.

```typescript lineNumbers
import { createHook } from "workflow";

export async function slackBotWorkflow(channelId: string) {
  "use workflow";

  // Token constructed from channel ID
  using hook = createHook<SlackMessage>({ // [!code highlight]
    token: `slack_messages:${channelId}`, // [!code highlight]
  }); // [!code highlight]

  for await (const message of hook) {
    if (message.text === "/stop") {
      break;
    }
    await processMessage(message);
  }
}
```

### Detecting Token Conflicts

Use `hook.getConflict()` (available starting in `workflow@4.5.0`) when the workflow needs to claim a hook token before doing other work, but does not need a payload yet:

```typescript lineNumbers
import { createHook } from "workflow";

declare function chargeOrder(orderId: string): Promise<void>; // @setup

async function processOrder(orderId: string) {
  "use workflow";

  using hook = createHook({ // [!code highlight]
    token: `order:${orderId}` // [!code highlight]
  }); // [!code highlight]

  const conflict = await hook.getConflict(); // [!code highlight]
  if (conflict) { // [!code highlight]
    // Another active workflow run already owns this token.
    return { dedupedTo: conflict.runId };
  }

  await chargeOrder(orderId);
}
```

Because `createHook()` alone does not suspend the workflow, awaiting `hook.getConflict()` is what actually suspends the run and commits the hook registration. It only waits for registration — to receive payload data from a future `resumeHook()` call, await the hook itself or iterate it with `for await...of`.

On a conflict, the resolved value is `{ runId }` identifying the run that currently owns the token. To act on the owner — inspect its status, wait for its result, or cancel it — pass `conflict.runId` to [`getRun()`](/docs/api-reference/workflow-api/get-run) inside a step. See [Run idempotency](/docs/foundations/idempotency#run-idempotency) for these strategies in context.

<Callout type="info">
Custom hook tokens are the recommended way to coordinate active workflow runs. Use a deterministic token from your domain, such as an order ID or conversation ID, create the hook near the beginning of the workflow, and check `await hook.getConflict()` before work that depends on owning the token. See [Run idempotency](/docs/foundations/idempotency#run-idempotency).
</Callout>

### Waiting for Multiple Payloads

You can also wait for multiple payloads by using the `for await...of` syntax.

```typescript lineNumbers
import { createHook } from "workflow"

export async function collectHookWorkflow() {
  "use workflow";

  using hook = createHook<{ message: string; done?: boolean }>();

  const payloads = [];
  for await (const payload of hook) { // [!code highlight]
    payloads.push(payload);

    if (payload.done) break;
  }

  return payloads;
}
```

### Disposing Hooks Early

You can dispose a hook early to release its token for reuse by another workflow. This is useful for handoff patterns where one workflow needs to transfer a hook token to another workflow while still running.

```typescript lineNumbers
import { createHook } from "workflow"

export async function handoffWorkflow(channelId: string) {
  "use workflow";

  const hook = createHook<{ message: string; handoff?: boolean }>({
    token: `channel:${channelId}`
  });

  for await (const payload of hook) {
    console.log("Received:", payload.message);

    if (payload.handoff) {
      hook.dispose(); // [!code highlight] Release the token for another workflow
      break;
    }
  }

  // Continue with other work while another workflow uses the token
}
```

After calling `dispose()`, the hook will no longer receive events and its token becomes available for other workflows to use.

### Automatic Disposal with `using`

Hooks implement the [TC39 Explicit Resource Management](https://github.com/tc39/proposal-explicit-resource-management) proposal, allowing automatic disposal with the `using` keyword:

```typescript lineNumbers
import { createHook } from "workflow"

export async function scopedHookWorkflow(channelId: string) {
  "use workflow";

  {
    using hook = createHook<{ message: string }>({ // [!code highlight]
      token: `channel:${channelId}`
    });

    const payload = await hook;
    console.log("Received:", payload.message);
  } // hook is automatically disposed here // [!code highlight]

  // Token is now available for other workflows to use
  console.log("Hook disposed, continuing with other work...");
}
```

This is equivalent to manually calling `dispose()` but ensures the hook is always cleaned up, even if an error occurs.

## Related Functions

- [`defineHook()`](/docs/api-reference/workflow/define-hook) - Type-safe hook helper
- [`resumeHook()`](/docs/api-reference/workflow-api/resume-hook) - Resume a hook with a payload
- [`createWebhook()`](/docs/api-reference/workflow/create-webhook) - Higher-level HTTP webhook abstraction
- [Idempotency](/docs/foundations/idempotency) - Deduplicate step side effects and workflow starts
