惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

A
Arctic Wolf
博客园 - 聂微东
F
Fortinet All Blogs
云风的 BLOG
云风的 BLOG
小众软件
小众软件
V
Visual Studio Blog
博客园 - 三生石上(FineUI控件)
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Apple Machine Learning Research
Apple Machine Learning Research
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
The Cloudflare Blog
H
Hackread – Cybersecurity News, Data Breaches, AI and More
The GitHub Blog
The GitHub Blog
Y
Y Combinator Blog
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
博客园_首页
L
LangChain Blog
A
About on SuperTechFans
阮一峰的网络日志
阮一峰的网络日志
I
Intezer
T
The Blog of Author Tim Ferriss
Security Latest
Security Latest
C
CXSECURITY Database RSS Feed - CXSecurity.com
Know Your Adversary
Know Your Adversary
Simon Willison's Weblog
Simon Willison's Weblog
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
P
Palo Alto Networks Blog
Scott Helme
Scott Helme
S
Secure Thoughts
Spread Privacy
Spread Privacy
T
Threat Research - Cisco Blogs
Attack and Defense Labs
Attack and Defense Labs
P
Privacy & Cybersecurity Law Blog
O
OpenAI News
H
Heimdal Security Blog
www.infosecurity-magazine.com
www.infosecurity-magazine.com
Help Net Security
Help Net Security
C
Cyber Attacks, Cyber Crime and Cyber Security
Blog — PlanetScale
Blog — PlanetScale
GbyAI
GbyAI
G
Google Developers Blog
博客园 - Franky
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
K
Kaspersky official blog
Recent Commits to openclaw:main
Recent Commits to openclaw:main
T
Tor Project blog
D
Darknet – Hacking Tools, Hacker News & Cyber Security
T
Tenable Blog
Google Online Security Blog
Google Online Security Blog
PCI Perspectives
PCI Perspectives

Workflow SDK Documentation

Patterns for Defining Tools Human-in-the-Loop Building Durable AI Agents Queueing User Messages Resumable Streams Sleep, Suspense, and Scheduling Streaming Updates from Tools API Reference Workflow Globals Changelog Resilient run start Cookbook Building a World Deploying Astro Express Fastify Hono Getting Started NestJS Next.js Nitro Nuxt Python SvelteKit Vite corrupted-event-log fetch-in-workflow hook-conflict Errors node-js-module-in-workflow serialization-failed start-invalid-workflow-function step-not-registered timeout-in-workflow webhook-invalid-respond-with-value webhook-response-not-sent workflow-not-registered Errors & Retrying Idempotency Foundations Serialization Starting Workflows Streaming Versioning Workflows and Steps How the Directives Work Encryption Event Sourcing Framework Integrations Understanding Directives Migration Guides Migrating from AWS Step Functions Migrating from Inngest Migrating from Temporal Observability Testing Server-Based Testing createHook createWebhook defineHook FatalError fetch getStepMetadata getWorkflowMetadata getWritable workflow RetryableError sleep @workflow/vitest DurableAgent @workflow/ai WorkflowChatTransport getHookByToken getRun getWorld workflow/api resumeHook resumeWebhook Chat Session Modeling runtime-decryption-failed Upgrading Workflows abort-signal-timeout-in-workflow Cancellation How Cancellation Works Internal Serializable AbortController and AbortSignal Eager Processing of Steps & Incremental Event Replay TanStack Start Agent Cancellation Sequential & Parallel Execution Workflow Composition Local World | Workflow SDK Postgres World | Workflow SDK Vercel World | Workflow SDK Migrating from trigger.dev Secure Credential Handling Local World | Workflow SDK Postgres World | Workflow SDK Vercel World | Workflow SDK
Hooks & Webhooks
2026-05-31 · via Workflow SDK Documentation

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:

  • Waiting for approval from a user or admin
  • Receiving data from an external system or service
  • Implementing event-driven workflows that react to multiple events over time

Creating Your First Hook

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.

Resuming a Hook

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:

  • Hooks allow you to pass any serializable data as the payload
  • You need the hook's token to resume it
  • The workflow will resume execution right where it left off

Checking for Token Conflicts

Sometimes 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.

Custom Tokens for Deterministic Hooks

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 });
  }
}

Receiving Multiple Events

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.

Disposing Hooks Early

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 token

After 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:

  1. Automatically serializes the entire HTTP Request object
  2. Provides an automatically addressable url property pointing to the generated webhook endpoint
  3. Handles sending HTTP Response objects back to the caller

When 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.

Creating Your First Webhook

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.

Sending Custom Responses

Webhooks provide two ways to send custom HTTP responses: static responses and dynamic responses.

Static 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
}

Dynamic Responses (Manual Mode)

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.

Handling Multiple Webhook Requests

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);
  }
}
FeatureHooksWebhooks
Data FormatArbitrary serializable dataHTTP Request objects
URLNo automatic URLAutomatic webhook.url property
Response HandlingN/ACan send HTTP Response (static or dynamic)
Use CaseCustom integrations, type-safe payloadsHTTP webhooks, standard REST APIs
ResumingresumeHook()Automatic via HTTP, or resumeWebhook()

Use Hooks when:

  • You need full control over the payload structure
  • You're integrating with custom event sources
  • You want strong TypeScript typing with defineHook()

Use Webhooks when:

  • You're receiving HTTP requests from external services
  • You need to send HTTP responses back to the caller
  • You want automatic URL routing without writing API handlers

Type-Safe Hooks with 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.

Token Design

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():

  • Make them deterministic: Base them on data the external system can reconstruct (like channel IDs, user IDs, etc.)
  • Use namespacing: Prefix tokens to avoid conflicts (e.g., slack:${channelId}, github:${repoId})
  • Include routing information: Ensure the token contains enough information to identify the correct workflow instance

Response Handling in Webhooks

  • Use static responses (respondWith: Response) for simple acknowledgments
  • Use manual mode (respondWith: "manual") when responses depend on request processing
  • Remember that respondWith() must be called from within a step function

Iterating Over Events

Both 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.