





















Pause workflows and resume them later with external data, user interactions, or HTTP requests.
Hooks provide a powerful mechanism for pausing workflow execution and resuming it later with external data. They enable workflows to wait for external events, user interactions (also known as "human in the loop"), or HTTP requests. This guide will teach you the core concepts, starting with the low-level Hook primitive and building up to the higher-level Webhook abstraction.
At their core, Hooks are a low-level primitive that allows you to pause a workflow and resume it later with arbitrary serializable data. Think of them as suspension points in your workflow where you're waiting for external input.
When you create a hook, it generates a unique token that external systems can use to send data back to your workflow. This makes hooks perfect for scenarios like:
Let's start with a simple example. Here's a workflow that creates a hook and waits for external data:
import { createHook } from "workflow";
export async function approvalWorkflow() {
"use workflow";
using hook = createHook<{ approved: boolean; comment: string }>();
console.log("Waiting for approval...");
console.log("Send approval to token:", hook.token);
// Workflow pauses here until data is sent
const result = await hook;
if (result.approved) {
console.log("Approved with comment:", result.comment);
} else {
console.log("Rejected:", result.comment);
}
}The workflow will pause at await hook until external code sends data to resume it.
See the full API reference for createHook() for all available options.
To send data to a waiting workflow, use resumeHook() from an API route, server action, or any other external context:
import { resumeHook } from "workflow/api";
// In an API route or external handler
export async function POST(request: Request) {
const { token, approved, comment } = await request.json();
try {
// Resume the workflow with the approval data
const result = await resumeHook(token, { approved, comment });
return Response.json({ success: true, runId: result.runId });
} catch (error) {
return Response.json({ error: "Invalid token" }, { status: 404 });
}
}The key points:
token to resume itSometimes you need to know that a hook token has been claimed, but you do not want to wait for external data yet. Await hook.getConflict() for that:
import { createHook } from "workflow";
declare function processOrder(orderId: string): Promise<void>; // @setup
export async function orderWorkflow(orderId: string) {
"use workflow";
using hook = createHook({
token: `order:${orderId}`
});
const conflict = await hook.getConflict();
if (conflict) {
// Another active run already owns this token.
return { dedupedTo: conflict.runId };
}
// The hook token is registered and reserved here.
await processOrder(orderId);
}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 hook registration, then resolves with null once the hook is registered and ready to receive payloads, or with { runId } identifying the run that owns the token if another active hook already claimed it (see HookConflictError). For hook_conflict events persisted by older worlds that did not record the owning run's ID, getConflict() rejects with HookConflictError instead of resolving with an incomplete handle. To act on the owner — inspect its status, wait for its result, or cancel it — pass conflict.runId to getRun() inside a step. See Idempotency for these strategies.
By default, hooks generate a random token. However, you often want to use a custom token that external systems can reconstruct. This is especially useful for long-running workflows where the same workflow instance should handle multiple events.
For example, imagine a Slack bot where each channel should have its own workflow instance:
import { createHook } from "workflow";
export async function slackChannelBot(channelId: string) {
"use workflow";
// Use channel ID in the token so Slack webhooks can find this workflow
using hook = createHook<SlackMessage>({
token: `slack_messages:${channelId}`
});
for await (const message of hook) {
console.log(`${message.user}: ${message.text}`);
if (message.text === "/stop") {
break;
}
await processMessage(message);
}
}
async function processMessage(message: SlackMessage) {
"use step";
// Process the Slack message
}Now your Slack webhook handler can deterministically resume the correct workflow:
import { resumeHook } from "workflow/api";
export async function POST(request: Request) {
const slackEvent = await request.json();
const channelId = slackEvent.channel;
try {
// Reconstruct the token using the channel ID
await resumeHook(`slack_messages:${channelId}`, slackEvent);
return new Response("OK");
} catch (error) {
return new Response("Hook not found", { status: 404 });
}
}Hooks are reusable - they implement AsyncIterable, which means you can use for await...of to receive multiple events over time:
import { createHook } from "workflow";
export async function dataCollectionWorkflow() {
"use workflow";
using hook = createHook<{ value: number; done?: boolean }>();
const values: number[] = [];
// Keep receiving data until we get a "done" signal
for await (const payload of hook) {
values.push(payload.value);
if (payload.done) {
break;
}
}
console.log("Collected values:", values);
return values;
}Each time you call resumeHook() with the same token, the loop receives another value.
When a workflow ends, hooks are automatically disposed. However, you may want to release a hook token early so another workflow can use it while your workflow continues running. Use a block scope with using to control when disposal happens:
import { createHook } from "workflow";
export async function handoffWorkflow(channelId: string) {
"use workflow";
{
using hook = createHook<{ message: string; handoff?: boolean }>({
token: `channel:${channelId}`
});
for await (const payload of hook) {
console.log("Received:", payload.message);
if (payload.handoff) {
break;
}
}
} // Hook token released here
// Token is now available for another workflow
console.log("Continuing with other work...");
}You can also manually dispose using the dispose() method:
const hook = createHook<{ message: string }>({ token: `channel:${channelId}` });
const payload = await hook;
hook.dispose(); // Manually release the tokenAfter disposal, the hook will no longer receive events and the async iterator will stop yielding values.
While hooks are powerful, they require you to manually handle HTTP requests and route them to workflows. Webhooks solve this by providing a higher-level abstraction built on top of hooks that:
Request objecturl property pointing to the generated webhook endpointResponse objects back to the callerWhen using Workflow SDK, webhooks are automatically wired up at /.well-known/workflow/v1/webhook/:token without any additional setup.
createWebhook() exposes a public route at /.well-known/workflow/v1/webhook/:token, and the token in that URL is the only authorization performed for incoming requests. This is convenient for prototypes and a simple developer experience because you can share the webhook URL (endpoint) without creating another route, but if you need stronger security, prefer createHook() behind your own route and authorize the request before calling resumeHook() to avoid unauthenticated workflow resumptions.
Here's a simple webhook that receives HTTP requests. Like hooks, webhooks support the using keyword for automatic cleanup:
import { createWebhook } from "workflow";
export async function webhookWorkflow() {
"use workflow";
using webhook = createWebhook();
// The webhook is automatically available at this URL
console.log("Send HTTP requests to:", webhook.url);
// Example: https://your-app.com/.well-known/workflow/v1/webhook/lJHkuMdQ2FxSFTbUMU84k
// Workflow pauses until an HTTP request is received
const request = await webhook;
console.log("Received request:", request.method, request.url);
const data = await request.json();
console.log("Data:", data);
}The webhook will automatically respond with a 202 Accepted status by default. External systems can simply make an HTTP request to the webhook.url to resume your workflow.
Webhooks provide two ways to send custom HTTP responses: static responses and dynamic responses.
Use the respondWith option to provide a static response that will be sent automatically for every request:
import { createWebhook } from "workflow";
export async function webhookWithStaticResponse() {
"use workflow";
using webhook = createWebhook({
respondWith: Response.json({
success: true, message: "Webhook received"
}),
});
const request = await webhook;
// The response was already sent automatically
const data = await request.json();
await processData(data);
}
async function processData(data: any) {
"use step";
// Long-running processing here
}For dynamic responses based on the request content, set respondWith: "manual" and call the respondWith() method on the request:
import { createWebhook, type RequestWithResponse } from "workflow";
async function sendCustomResponse(request: RequestWithResponse, message: string) {
"use step";
// Call respondWith() to send the response
await request.respondWith(
new Response(
JSON.stringify({ message }),
{
status: 200,
headers: { "Content-Type": "application/json" }
}
)
);
}
export async function webhookWithDynamicResponse() {
"use workflow";
// Set respondWith to "manual" to handle responses yourself
using webhook = createWebhook({ respondWith: "manual" });
const request = await webhook;
const data = await request.json();
// Decide what response to send based on the data
if (data.type === "urgent") {
await sendCustomResponse(request, "Processing urgently");
} else {
await sendCustomResponse(request, "Processing normally");
}
// Continue workflow...
}When using respondWith: "manual", the respondWith() method must be called from within a step function due to serialization requirements. This requirement may be removed in the future.
Like hooks, webhooks support iteration:
import { createWebhook, type RequestWithResponse } from "workflow";
async function sendAck(request: RequestWithResponse, message: string) {
"use step";
await request.respondWith(
Response.json({ received: true, message })
);
}
async function processEvent(data: any) {
"use step";
console.log("Processing event:", data);
}
export async function eventCollectorWorkflow() {
"use workflow";
using webhook = createWebhook({ respondWith: "manual" });
console.log("Send events to:", webhook.url);
for await (const request of webhook) {
const data = await request.json();
if (data.type === "done") {
await sendAck(request, "Workflow complete");
break;
}
await sendAck(request, "Event received");
await processEvent(data);
}
}| Feature | Hooks | Webhooks |
|---|---|---|
| Data Format | Arbitrary serializable data | HTTP Request objects |
| URL | No automatic URL | Automatic webhook.url property |
| Response Handling | N/A | Can send HTTP Response (static or dynamic) |
| Use Case | Custom integrations, type-safe payloads | HTTP webhooks, standard REST APIs |
| Resuming | resumeHook() | Automatic via HTTP, or resumeWebhook() |
Use Hooks when:
defineHook()Use Webhooks when:
defineHook()The defineHook() helper provides type safety and runtime validation between creating and resuming hooks using Standard Schema v1. Use any compliant validator like Zod or Valibot:
import { defineHook } from "workflow";
import { z } from "zod";
// Define the hook with schema for type safety and runtime validation
const approvalHook = defineHook({
schema: z.object({
requestId: z.string(),
approved: z.boolean(),
approvedBy: z.string(),
comment: z.string().transform((value) => value.trim()),
}),
});
// In your workflow
export async function documentApprovalWorkflow(documentId: string) {
"use workflow";
using hook = approvalHook.create({
token: `approval:${documentId}`
});
// Payload is type-safe and validated
const approval = await hook;
console.log(`Document ${approval.requestId} ${approval.approved ? "approved" : "rejected"}`);
console.log(`By: ${approval.approvedBy}, Comment: ${approval.comment}`);
}
// In your API route - both type-safe and runtime-validated!
export async function POST(request: Request) {
const { documentId, ...approvalData } = await request.json();
try {
// The schema validates the payload before resuming the workflow
await approvalHook.resume(`approval:${documentId}`, approvalData);
return new Response("OK");
} catch (error) {
return Response.json({ error: "Invalid token or validation failed" }, { status: 400 });
}
}This pattern is especially valuable in larger applications where the workflow and API code are in separate files, providing both compile-time type safety and runtime validation.
Custom tokens are available for createHook() with server-side resumeHook() only. Webhooks (createWebhook()) always use randomly generated tokens to prevent unauthorized access to public webhook endpoints.
When using custom tokens with createHook():
slack:${channelId}, github:${repoId})respondWith: Response) for simple acknowledgmentsrespondWith: "manual") when responses depend on request processingrespondWith() must be called from within a step functionBoth hooks and webhooks support iteration, making them perfect for long-running event loops:
using hook = createHook<Event>();
for await (const event of hook) {
await processEvent(event);
if (shouldStop(event)) {
break;
}
}This pattern allows a single workflow instance to handle multiple events over time, maintaining state between events.
createHook() API ReferencecreateWebhook() API ReferencedefineHook() API ReferenceresumeHook() API ReferenceresumeWebhook() API Reference此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。