workflow
Version:
Workflow SDK - Build durable, resilient, and observable workflows
228 lines (172 loc) • 8.23 kB
text/mdx
---
title: resumeHook
description: Resume a paused workflow by sending a payload to a hook token.
type: reference
summary: Use resumeHook to send a payload to a hook token and resume a paused workflow.
prerequisites:
- /docs/foundations/hooks
related:
- /docs/api-reference/workflow-api/resume-webhook
- /docs/foundations/idempotency
---
Resumes a workflow run by sending a payload to a hook identified by its token.
It durably writes the `hook_received` event and only then publishes a workflow wake. The call resolves only after both operations succeed, in that order.
`resumeHook()` throws `HookNotFoundError` when no hook holds the token or when its `hook_received` write is refused because the hook was disposed or the run ended. See [durable hook resume](/docs/changelog/lazy-hook-resume).
If `resumeHook()` throws any other error, the outcome is ambiguous only in dispatch, never in durability: the event may already be durable even though the workflow wake failed, and any later wake of the run delivers it. Calling `resumeHook()` again creates a new `resumeId` and can append a second `hook_received`. Callers that need at-most-once behavior across separate invocations must retain and deduplicate their own request key.
<Callout type="warn">
`resumeHook` is a runtime function that must be called from outside a workflow function.
</Callout>
<Callout type="warn">
`resumeHook()` does not check who is calling it. Authenticate the caller and confirm they may resume this hook before calling it; knowing the token is not enough. The examples below omit that check for brevity. See [Hook and webhook security](/docs/foundations/hooks#security).
</Callout>
```typescript lineNumbers
import { resumeHook } from "workflow/api";
export async function POST(request: Request) {
const { token, data } = await request.json();
try {
const result = await resumeHook(token, data); // [!code highlight]
return Response.json({
runId: result.runId
});
} catch (error) {
return new Response("Hook not found", { status: 404 });
}
}
```
## API signature
### Parameters
<TSDoc
definition={`
import { resumeHook } from "workflow/api";
export default resumeHook;`}
showSections={["parameters"]}
/>
### Returns
Returns a `Promise<ResumedHook>`, a `Hook` (from `workflow/api`) extended with an optional `resilientResume` flag. Resolving means the payload is durably recorded as `hook_received` and the workflow wake was accepted. `resilientResume` is retained for source compatibility and is no longer set by any path. Resuming never reads the hook's metadata, so the resolved hook's `metadata` is a Promise that hydrates on first access, exactly as with [`getHookByToken()`](/docs/api-reference/workflow-api/get-hook-by-token): `await hook.metadata` to read it. The resolved hook:
<TSDoc
definition={`
import type { Hook } from "workflow/api";
export default Hook;`}
showSections={["returns"]}
/>
## Examples
### Basic API route
Using `resumeHook` in a basic API route to resume a hook:
```typescript lineNumbers
import { resumeHook } from "workflow/api";
export async function POST(request: Request) {
const { token, data } = await request.json();
try {
const result = await resumeHook(token, data); // [!code highlight]
return Response.json({
success: true,
runId: result.runId
});
} catch (error) {
return new Response("Hook not found", { status: 404 });
}
}
```
### With type safety
Defining a payload type and using `resumeHook` to resume a hook with type safety:
```typescript lineNumbers
import { resumeHook } from "workflow/api";
type ApprovalPayload = {
approved: boolean;
comment: string;
};
export async function POST(request: Request) {
const { token, approved, comment } = await request.json();
try {
const result = await resumeHook<ApprovalPayload>(token, { // [!code highlight]
approved, // [!code highlight]
comment, // [!code highlight]
}); // [!code highlight]
return Response.json({ runId: result.runId });
} catch (error) {
return Response.json({ error: "Invalid token" }, { status: 404 });
}
}
```
### Server action (Next.js)
Using `resumeHook` in Next.js server actions to resume a hook:
```typescript lineNumbers
"use server";
import { resumeHook } from "workflow/api";
export async function approveRequest(token: string, approved: boolean) {
try {
const result = await resumeHook(token, { approved });
return result.runId;
} catch (error) {
throw new Error("Invalid approval token");
}
}
```
### Webhook handler
Using `resumeHook` in a generic webhook handler to resume a hook:
```typescript lineNumbers
import { resumeHook } from "workflow/api";
// Generic webhook handler that forwards data to a hook
export async function POST(request: Request) {
const url = new URL(request.url);
const token = url.searchParams.get("token");
if (!token) {
return Response.json({ error: "Missing token" }, { status: 400 });
}
try {
const body = await request.json();
const result = await resumeHook(token, body);
return Response.json({ success: true, runId: result.runId });
} catch (error) {
return Response.json({ error: "Hook not found" }, { status: 404 });
}
}
```
### Resume or start
A common endpoint shape is "resume or start": one route that resumes the active workflow run for a business key if one exists, or starts a new run otherwise. This comes up when the workflow uses a deterministic hook token as its idempotency key, for example, one active run per order or conversation.
`resumeHook()` is the resume half of that flow. Try it first; if it throws `HookNotFoundError`, no active run owns the token yet, so start the workflow. One subtlety: `start()` returns before the new run executes and registers its hook, so you cannot resume immediately after starting. Retry the resume until the hook is registered: if you drop the payload and only start the workflow, the data from this request is lost.
```typescript lineNumbers
import { resumeHook, start } from "workflow/api";
import { HookNotFoundError } from "workflow/errors";
import { processOrder } from "./workflows/process-order";
type OrderRequest = { confirmed: boolean };
async function resumeWithRetry(token: string, payload: OrderRequest) {
for (let attempt = 0; attempt < 5; attempt++) {
try {
return await resumeHook(token, payload); // [!code highlight]
} catch (error) {
if (!HookNotFoundError.is(error)) throw error;
await new Promise((resolve) => setTimeout(resolve, 100));
}
}
throw new Error("Workflow did not register its hook in time");
}
export async function POST(request: Request) {
const { orderId, confirmed } = await request.json();
const token = `order:${orderId}`;
const payload = { confirmed };
try {
// An active run already owns this token: resume it.
const hook = await resumeHook(token, payload); // [!code highlight]
return Response.json({ runId: hook.runId, reused: true });
} catch (error) {
if (!HookNotFoundError.is(error)) throw error;
}
// No hook yet: start a new run, then retry the resume so this
// request's payload still reaches the workflow.
const run = await start(processOrder, [orderId]); // [!code highlight]
const resumed = await resumeWithRetry(token, payload);
// A concurrent request can win the race between `start()` and hook
// registration; the resume always reaches the actual active owner.
return Response.json({
runId: resumed.runId,
reused: resumed.runId !== run.runId,
});
}
```
See [Run idempotency](/docs/foundations/idempotency#run-idempotency) for the full pattern, including how the workflow claims the token with `hook.getConflict()` and how concurrent starts converge on one active owner.
## Related functions
- [`getHookByToken()`](/docs/api-reference/workflow-api/get-hook-by-token): Get hook details before resuming.
- [`createHook()`](/docs/api-reference/workflow/create-hook): Create a hook in a workflow.
- [`defineHook()`](/docs/api-reference/workflow/define-hook): Type-safe hook helper.
- [Idempotency](/docs/foundations/idempotency): Deduplicate step side effects and workflow starts.