createHook
Create a low-level hook to resume workflows with arbitrary payloads.
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.
import { createHook } from "workflow"
export async function hookWorkflow() {
"use workflow";
// `using` automatically disposes the hook when it goes out of scope
using hook = createHook();
const result = await hook; // Suspends the workflow until the hook is resumed
}API Signature
Parameters
| Name | Type | Description |
|---|---|---|
options | HookOptions | Configuration options for the hook. |
HookOptions
| Name | Type | Description |
|---|---|---|
token | string | Unique token that is used to associate with the hook.
When specifying an explicit token, the token should be constructed
with information that the dispatching side can reliably reconstruct
the token with the information it has available.
Deterministic tokens are intended for use with createHook() and
server-side resumeHook() only. For webhooks (createWebhook()), an
explicit token is not accepted — one is always generated for you.
A generated token is not trivial to guess but is not a security
contract, so authenticate webhook requests themselves rather than
relying on URL secrecy:
https://workflow-sdk.dev/docs/foundations/hooks#token-design
If provided, the token must be a non-empty string; passing an empty
string throws. If not provided (or undefined), a token is generated
for you. |
experimental_minRetention | number | StringValue | Date | **Experimental.** Keeps this Hook's token unavailable for at least the
configured time after createHook() runs.
Accepts the same values as sleep(): a duration string, a number of
milliseconds, or an absolute Date. Relative durations start when
createHook() runs, not when the workflow ends.
The Hook remains active until the workflow ends, even if the configured
time passes first. Another Hook can use the token only after both the run
has ended and the configured time has passed.
After the run ends, the Hook can still be found with getHookByToken()
until retention ends, but it cannot be resumed.
Calling dispose() (including through using) releases the token
immediately.
createHook() throws if the configured World does not support this
experimental option. |
metadata | Serializable | Additional user-defined data to include with the hook payload. |
isWebhook | boolean | Whether this hook can be resumed via the public webhook endpoint.
When true, the hook can be triggered by sending an HTTP request to the
public workflow webhook URL. This is automatically set when using
createWebhook().
When false (the default), the hook can only be resumed server-side
via resumeHook(). |
Returns
Hook<T>Hook
| Name | Type | Description |
|---|---|---|
token | string | The token used to identify this hook. |
getConflict | () => Promise<Run<unknown> | null> | Returns the Run already using this token, or null when this Hook
registers successfully.
Calling createHook() alone does not register the hook — registration
only happens when the workflow suspends. Awaiting getConflict()
suspends the workflow to commit the hook registration without waiting for
payload data.
If it returns a run, this Hook was not created. Awaiting the Hook instead
rejects with HookConflictError. |
dispose | () => void | Disposes the hook, releasing its token for reuse by other workflows.
After calling dispose(), the hook will no longer receive any events.
This is useful when you want to explicitly release a hook token before
the workflow completes, allowing another workflow to register a hook
with the same token. |
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:
import { createHook } from "workflow"
export async function approvalWorkflow() {
"use workflow";
using hook = createHook<{ approved: boolean; comment: string }>();
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.
import { createHook } from "workflow";
export async function slackBotWorkflow(channelId: string) {
"use workflow";
// Token constructed from channel ID
using hook = createHook<SlackMessage>({
token: `slack_messages:${channelId}`,
});
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:
import { createHook } from "workflow";
declare function chargeOrder(orderId: string): Promise<void>; // @setup
async function processOrder(orderId: string) {
"use workflow";
using hook = createHook({
token: `order:${orderId}`
});
const conflict = await hook.getConflict();
if (conflict) {
// 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() inside a step. See Run idempotency for these strategies in context.
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.
Waiting for Multiple Payloads
You can also wait for multiple payloads by using the for await...of syntax.
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) {
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.
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(); // 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 proposal, allowing automatic disposal with the using keyword:
import { createHook } from "workflow"
export async function scopedHookWorkflow(channelId: string) {
"use workflow";
{
using hook = createHook<{ message: string }>({
token: `channel:${channelId}`
});
const payload = await hook;
console.log("Received:", payload.message);
} // hook is automatically disposed here
// 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()- Type-safe hook helperresumeHook()- Resume a hook with a payloadcreateWebhook()- Higher-level HTTP webhook abstraction- Idempotency - Deduplicate step side effects and workflow starts