diff --git a/docs/content/docs/api-reference/workflow-ai/index.mdx b/docs/content/docs/api-reference/workflow-ai/index.mdx
index b532a1455f..8422dbf8db 100644
--- a/docs/content/docs/api-reference/workflow-ai/index.mdx
+++ b/docs/content/docs/api-reference/workflow-ai/index.mdx
@@ -9,13 +9,35 @@ related:
Helpers for integrating AI SDK for building AI-powered workflows.
-## Classes
+## Root Exports (`@workflow/ai`)
+
+The root `@workflow/ai` entrypoint exports:
-
- A class for building durable AI agents that maintain state across workflow steps and handle tool execution with automatic retries.
-
A drop-in transport for the AI SDK for automatic reconnection in interrupted streams.
+
+It also re-exports the `ModelMessage` type from the AI SDK for convenience.
+
+## Agent (`@workflow/ai/agent`)
+
+The `@workflow/ai/agent` subpath exports the `DurableAgent` class and related types:
+
+
+
+ A class for building durable AI agents that maintain state across workflow steps and handle tool execution with automatic retries.
+
+
+
+## Provider Subpaths
+
+Model provider wrappers are available as separate subpath imports:
+
+- `@workflow/ai/anthropic` — Anthropic provider
+- `@workflow/ai/openai` — OpenAI provider
+- `@workflow/ai/google` — Google provider
+- `@workflow/ai/gateway` — Gateway provider
+- `@workflow/ai/xai` — xAI provider
+- `@workflow/ai/test` — Mock provider for testing
diff --git a/docs/content/docs/api-reference/workflow-api/get-hook-by-token.mdx b/docs/content/docs/api-reference/workflow-api/get-hook-by-token.mdx
index 028c9b4047..2bc170ee07 100644
--- a/docs/content/docs/api-reference/workflow-api/get-hook-by-token.mdx
+++ b/docs/content/docs/api-reference/workflow-api/get-hook-by-token.mdx
@@ -1,13 +1,13 @@
---
title: getHookByToken
-description: Retrieve hook details and workflow run information by token.
+description: Retrieve hook details, metadata, and associated run ID by token.
type: reference
-summary: Use getHookByToken to look up a hook's metadata and associated workflow run before resuming it.
+summary: Use getHookByToken to look up a hook's metadata and associated run ID before resuming it.
prerequisites:
- /docs/foundations/hooks
---
-Retrieves a hook by its unique token, returning the associated workflow run information and any metadata that was set when the hook was created. This function is useful for inspecting hook details before deciding whether to resume a workflow.
+Retrieves a hook by its unique token, returning hook details, the associated run ID, and any metadata that was set when the hook was created. Metadata is automatically hydrated (deserialized) before being returned, so you receive the original values rather than raw serialized data. This function is useful for inspecting hook details before deciding whether to resume a workflow.
`getHookByToken` is a runtime function that must be called from outside a workflow function.
diff --git a/docs/content/docs/api-reference/workflow-api/index.mdx b/docs/content/docs/api-reference/workflow-api/index.mdx
index 5941e9068d..d1420233a5 100644
--- a/docs/content/docs/api-reference/workflow-api/index.mdx
+++ b/docs/content/docs/api-reference/workflow-api/index.mdx
@@ -1,34 +1,34 @@
---
title: "workflow/api"
-description: Runtime functions to inspect runs, start workflows, and access world data.
+description: Runtime functions for starting workflows, inspecting runs, resuming hooks, and accessing world data.
type: overview
-summary: Explore runtime functions for starting workflows, inspecting runs, and managing hooks.
+summary: Explore runtime functions across the workflow/api and workflow/runtime entrypoints.
---
-API reference for runtime functions from the `workflow/api` package.
+API reference for runtime functions from the `workflow/api` and `workflow/runtime` entrypoints.
## Functions
-The API package is for access and introspection of workflow data to inspect runs, start new runs, or access anything else directly accessible by the world.
+Most functions in this section are imported from `workflow/api`. `getWorld()` is imported from `workflow/runtime`.
Start/enqueue a new workflow run.
- Resume a workflow by sending a payload to a hook.
+ Resume a hook created with `createHook()` by sending an arbitrary payload.
- Resume a workflow by sending a `Request` to a webhook.
+ Resume a webhook created with `createWebhook()` by forwarding an HTTP `Request`.
- Get hook details and metadata by its token.
+ Retrieve hook details, metadata, and run information by token.
Get workflow run status and metadata without waiting for completion.
- Async: resolve the World instance for storage, queuing, and streaming backends.
+ Get direct access to workflow storage, queuing, and streaming backends from `workflow/runtime`.
Low-level API for inspecting runs, steps, events, hooks, streams, and queues.
diff --git a/docs/content/docs/api-reference/workflow-api/resume-hook.mdx b/docs/content/docs/api-reference/workflow-api/resume-hook.mdx
index e98b8c092f..8530c9ac71 100644
--- a/docs/content/docs/api-reference/workflow-api/resume-hook.mdx
+++ b/docs/content/docs/api-reference/workflow-api/resume-hook.mdx
@@ -1,17 +1,19 @@
---
title: resumeHook
-description: Resume a paused workflow by sending a payload to a hook token.
+description: Resume a paused workflow by sending a payload to a hook token or hook object.
type: reference
-summary: Use resumeHook to send a payload to a hook token and resume a paused workflow.
+summary: Use resumeHook to send a payload to a hook token or hook object and resume a paused workflow.
prerequisites:
- /docs/foundations/hooks
related:
- /docs/api-reference/workflow-api/resume-webhook
---
-Resumes a workflow run by sending a payload to a hook identified by its token.
+Resumes a workflow run by sending a payload to a hook identified by its token or an existing hook object.
-It creates a `hook_received` event and re-triggers the workflow to continue execution.
+`resumeHook()` returns the resumed hook entity, not the workflow's eventual return value. This is useful when you need the associated `runId`, `hookId`, or hook metadata immediately after resumption.
+
+It creates a `hook_received` event and re-queues the workflow so execution can continue. Use `resumeHook()` for hooks created with [`createHook()`](/docs/api-reference/workflow/create-hook), whether the token was explicitly set or auto-generated. For hooks created with [`createWebhook()`](/docs/api-reference/workflow/create-webhook), use [`resumeWebhook()`](/docs/api-reference/workflow-api/resume-webhook) instead.
`resumeHook` is a runtime function that must be called from outside a workflow function.
@@ -19,17 +21,47 @@ It creates a `hook_received` event and re-triggers the workflow to continue exec
```typescript lineNumbers
import { resumeHook } from "workflow/api";
+import { HookNotFoundError } from "workflow/errors";
export async function POST(request: Request) {
const { token, data } = await request.json();
try {
- const result = await resumeHook(token, data); // [!code highlight]
+ const hook = await resumeHook(token, data); // [!code highlight]
+
+ console.info(JSON.stringify({
+ event: "workflow.hook.resumed",
+ token: hook.token,
+ hookId: hook.hookId,
+ runId: hook.runId,
+ }));
+
return Response.json({
- runId: result.runId
+ runId: hook.runId,
});
} catch (error) {
- return new Response("Hook not found", { status: 404 });
+ if (HookNotFoundError.is(error)) {
+ console.warn(JSON.stringify({
+ event: "workflow.hook.not_found",
+ token,
+ }));
+
+ return Response.json(
+ { error: "Hook not found", token },
+ { status: 404 }
+ );
+ }
+
+ console.error(JSON.stringify({
+ event: "workflow.hook.resume_failed",
+ token,
+ error: error instanceof Error ? error.message : String(error),
+ }));
+
+ return Response.json(
+ { error: "Failed to resume hook" },
+ { status: 500 }
+ );
}
}
```
@@ -41,6 +73,7 @@ export async function POST(request: Request) {
@@ -56,6 +89,12 @@ export default Hook;`}
showSections={["returns"]}
/>
+## Error Behavior
+
+A missing hook token is only one failure mode. `resumeHook()` can also fail while dehydrating the payload, creating the `hook_received` event, or re-queueing the workflow. In HTTP handlers, map missing hooks to `404` and unexpected failures to `500` so operational failures stay visible.
+
+`resumeHook()` resolves to the hook record that was resumed. Use `hook.runId` with [`getRun()`](/docs/api-reference/workflow-api/get-run) if you want to inspect the workflow after resumption; it does not wait for the workflow to finish.
+
## Examples
### Basic API Route
@@ -64,19 +103,23 @@ Using `resumeHook` in a basic API route to resume a hook:
```typescript lineNumbers
import { resumeHook } from "workflow/api";
+import { HookNotFoundError } from "workflow/errors";
export async function POST(request: Request) {
const { token, data } = await request.json();
try {
- const result = await resumeHook(token, data); // [!code highlight]
+ const hook = await resumeHook(token, data); // [!code highlight]
return Response.json({
success: true,
- runId: result.runId
+ runId: hook.runId
});
} catch (error) {
- return new Response("Hook not found", { status: 404 });
+ if (HookNotFoundError.is(error)) {
+ return Response.json({ error: "Hook not found" }, { status: 404 });
+ }
+ return Response.json({ error: "Failed to resume hook" }, { status: 500 });
}
}
```
@@ -87,6 +130,7 @@ Defining a payload type and using `resumeHook` to resume a hook with type safety
```typescript lineNumbers
import { resumeHook } from "workflow/api";
+import { HookNotFoundError } from "workflow/errors";
type ApprovalPayload = {
approved: boolean;
@@ -97,14 +141,17 @@ export async function POST(request: Request) {
const { token, approved, comment } = await request.json();
try {
- const result = await resumeHook(token, { // [!code highlight]
+ const hook = await resumeHook(token, { // [!code highlight]
approved, // [!code highlight]
comment, // [!code highlight]
}); // [!code highlight]
- return Response.json({ runId: result.runId });
+ return Response.json({ runId: hook.runId });
} catch (error) {
- return Response.json({ error: "Invalid token" }, { status: 404 });
+ if (HookNotFoundError.is(error)) {
+ return Response.json({ error: "Hook not found" }, { status: 404 });
+ }
+ return Response.json({ error: "Failed to resume hook" }, { status: 500 });
}
}
```
@@ -117,40 +164,42 @@ Using `resumeHook` in Next.js server actions to resume a hook:
"use server";
import { resumeHook } from "workflow/api";
+import { HookNotFoundError } from "workflow/errors";
export async function approveRequest(token: string, approved: boolean) {
try {
- const result = await resumeHook(token, { approved });
- return result.runId;
+ const hook = await resumeHook(token, { approved });
+ return hook.runId;
} catch (error) {
- throw new Error("Invalid approval token");
+ throw new Error("Failed to resume hook");
}
}
```
-### Webhook Handler
+### Passing a Hook Object
-Using `resumeHook` in a generic webhook handler to resume a hook:
+Instead of a token string, you can pass a `Hook` object directly (e.g. one returned by [`getHookByToken()`](/docs/api-reference/workflow-api/get-hook-by-token)):
```typescript lineNumbers
-import { resumeHook } from "workflow/api";
+import { getHookByToken, resumeHook } from "workflow/api";
+import { HookNotFoundError } from "workflow/errors";
-// 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 });
- }
+ const { token, data } = await request.json();
try {
- const body = await request.json();
- const result = await resumeHook(token, body);
+ const hook = await getHookByToken(token);
+
+ // Validate metadata before resuming
+ console.log("Hook metadata:", hook.metadata);
- return Response.json({ success: true, runId: result.runId });
+ const resumedHook = await resumeHook(hook, data); // [!code highlight]
+ return Response.json({ success: true, runId: resumedHook.runId });
} catch (error) {
- return Response.json({ error: "Hook not found" }, { status: 404 });
+ if (HookNotFoundError.is(error)) {
+ return Response.json({ error: "Hook not found" }, { status: 404 });
+ }
+ return Response.json({ error: "Failed to resume hook" }, { status: 500 });
}
}
```
diff --git a/docs/content/docs/api-reference/workflow-api/resume-webhook.mdx b/docs/content/docs/api-reference/workflow-api/resume-webhook.mdx
index b4ee9f2008..3a3cac61b3 100644
--- a/docs/content/docs/api-reference/workflow-api/resume-webhook.mdx
+++ b/docs/content/docs/api-reference/workflow-api/resume-webhook.mdx
@@ -50,9 +50,11 @@ showSections={['parameters']}
### Returns
-Returns a `Promise` that resolves to:
+Returns a `Promise` that resolves to one of three outcomes:
-- `Response`: The HTTP response from the workflow's `respondWith()` call
+- A `202 Accepted` response when the webhook was created with the default mode (no `respondWith` option)
+- The exact `Response` object configured with `createWebhook({ respondWith: new Response(...) })`
+- The workflow's manual `Response` when the webhook was created with `createWebhook({ respondWith: 'manual' })` and a step calls `request.respondWith(response)`
Throws an error if the webhook token is not found or invalid.
@@ -81,7 +83,7 @@ export async function POST(request: Request) {
try {
const response = await resumeWebhook(token, request); // [!code highlight]
- return response; // Returns the workflow's custom response
+ return response; // May be 202 Accepted, a configured static Response, or a manual workflow response
} catch (error) {
return new Response("Webhook not found", { status: 404 });
}
diff --git a/docs/content/docs/api-reference/workflow-api/start.mdx b/docs/content/docs/api-reference/workflow-api/start.mdx
index a72dde3681..a88bb10aa8 100644
--- a/docs/content/docs/api-reference/workflow-api/start.mdx
+++ b/docs/content/docs/api-reference/workflow-api/start.mdx
@@ -57,7 +57,7 @@ Learn more about [`WorkflowReadableStreamOptions`](/docs/api-reference/workflow-
* When `deploymentId` is provided, the argument types and return type become `unknown` since there is no guarantee the workflow function's types will be consistent across different deployments.
-If `start()` throws `'start' received an invalid workflow function. Ensure the Workflow Development Kit is configured correctly and the function includes a 'use workflow' directive.`, the passed function was not transformed as a workflow. The two most common causes are a missing `"use workflow"` directive or missing framework integration. See [start-invalid-workflow-function](/docs/errors/start-invalid-workflow-function).
+If `start()` throws `'start' received an invalid workflow function. Ensure the Workflow SDK is configured correctly and the function includes a 'use workflow' directive.`, the passed function was not transformed as a workflow. The two most common causes are a missing `"use workflow"` directive or missing framework integration. See [start-invalid-workflow-function](/docs/errors/start-invalid-workflow-function).
## Examples
diff --git a/docs/content/docs/api-reference/workflow-next/with-workflow.mdx b/docs/content/docs/api-reference/workflow-next/with-workflow.mdx
index 85090227d0..cfa94fe04c 100644
--- a/docs/content/docs/api-reference/workflow-next/with-workflow.mdx
+++ b/docs/content/docs/api-reference/workflow-next/with-workflow.mdx
@@ -9,6 +9,45 @@ prerequisites:
Configures webpack/turbopack loaders to transform workflow code (`"use step"`/`"use workflow"` directives)
+## API Signature
+
+### Parameters
+
+
+
+#### Options
+
+The second parameter accepts an optional configuration object:
+
+| Property | Type | Description |
+|---|---|---|
+| `workflows.lazyDiscovery` | `boolean` | Enable lazy discovery mode. Sets the `WORKFLOW_NEXT_LAZY_DISCOVERY` flag. Deferred discovery only activates on Next.js `>= 16.2.0-canary.48`; on older versions, Workflow logs a warning and falls back to eager scanning. |
+| `workflows.local.port` | `number` | Override the local development server port. Sets the `PORT` environment variable when running locally (no `VERCEL_DEPLOYMENT_ID`). |
+
+### Returns
+
+Returns an async function `(phase: string, ctx: { defaultConfig: NextConfig }) => Promise` compatible with the `next.config.ts` default export.
+
+### Environment Behavior
+
+When running locally (no `VERCEL_DEPLOYMENT_ID`) and `WORKFLOW_TARGET_WORLD` is not already set:
+- Sets `WORKFLOW_TARGET_WORLD` to `'local'`
+- Sets `WORKFLOW_LOCAL_DATA_DIR` to `'.next/workflow-data'`
+
+When running locally (no `VERCEL_DEPLOYMENT_ID`):
+- If `workflows.local.port` is provided, sets `PORT` to that value
+
+When running on Vercel (`VERCEL_DEPLOYMENT_ID` is present) and `WORKFLOW_TARGET_WORLD` is not already set:
+- Sets `WORKFLOW_TARGET_WORLD` to `'vercel'`
+
+During the development server phase (`phase-development-server`):
+- Sets `WORKFLOW_PUBLIC_MANIFEST` to `'1'` if not already set
+
## Usage
To enable `"use step"` and `"use workflow"` directives while developing locally or deploying to production, wrap your `nextConfig` with `withWorkflow`.
@@ -21,8 +60,14 @@ const nextConfig: NextConfig = {
// … rest of your Next.js config
};
-// not required but allows configuring workflow options
-const workflowConfig = {}
+// optional, but this shows the actual supported shape
+const workflowConfig = {
+ workflows: {
+ local: {
+ port: 3001,
+ },
+ },
+};
export default withWorkflow(nextConfig, workflowConfig); // [!code highlight]
```
diff --git a/docs/content/docs/api-reference/workflow/create-hook.mdx b/docs/content/docs/api-reference/workflow/create-hook.mdx
index 00e5afccd1..df1bbfd5d5 100644
--- a/docs/content/docs/api-reference/workflow/create-hook.mdx
+++ b/docs/content/docs/api-reference/workflow/create-hook.mdx
@@ -10,9 +10,9 @@ related:
- /docs/api-reference/workflow/create-webhook
---
-Creates a low-level hook primitive that can be used to resume a workflow run with arbitrary payloads.
+Creates a hook primitive that can be used to suspend a workflow and later resume it with an arbitrary serializable payload.
-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.
+Unlike [`createWebhook()`](/docs/api-reference/workflow/create-webhook), which always generates a random token for its public HTTP endpoint, `createHook()` accepts an optional custom `token`. If you omit `token`, Workflow generates a unique token automatically. Use a custom token only when the sender can deterministically reconstruct it. Hooks are resumed server-side via [`resumeHook()`](/docs/api-reference/workflow-api/resume-hook) and can receive any serializable payload.
```ts lineNumbers
import { createHook } from "workflow"
@@ -65,6 +65,10 @@ export default Hook;`}
The returned `Hook` object also implements `AsyncIterable`, which allows you to iterate over incoming payloads using `for await...of` syntax.
+
+The `isWebhook` option in `HookOptions` controls whether the hook can be resumed via the public webhook endpoint (`/.well-known/workflow/v1/webhook/{token}`). It defaults to `false`, meaning hooks created with `createHook()` can only be resumed server-side via `resumeHook()`. The `createWebhook()` function sets this to `true` automatically.
+
+
## Examples
### Basic Usage
@@ -88,9 +92,36 @@ export async function approvalWorkflow() {
}
```
+### Machine-readable logging
+
+Emit a structured log line when a hook is created so external systems can discover its token programmatically:
+
+```typescript lineNumbers
+import { createHook } from "workflow"
+
+export async function approvalWorkflow() {
+ "use workflow";
+
+ using hook = createHook<{ approved: boolean }>();
+
+ console.info(JSON.stringify({ // [!code highlight]
+ event: "workflow.hook.created", // [!code highlight]
+ token: hook.token, // [!code highlight]
+ })); // [!code highlight]
+
+ return await hook;
+}
+```
+
+Example log line:
+
+```json
+{"event":"workflow.hook.created","token":"nk_abc123"}
+```
+
### Customizing Tokens
-Tokens are used to identify a specific hook. You can customize the token to be more specific to a use case.
+By default, Workflow generates a unique token for each hook. You can provide a custom `token` when the resuming side needs to reconstruct the token without prior communication.
```typescript lineNumbers
import { createHook } from "workflow";
diff --git a/docs/content/docs/api-reference/workflow/create-webhook.mdx b/docs/content/docs/api-reference/workflow/create-webhook.mdx
index ac63878d4d..105a56a20c 100644
--- a/docs/content/docs/api-reference/workflow/create-webhook.mdx
+++ b/docs/content/docs/api-reference/workflow/create-webhook.mdx
@@ -17,6 +17,15 @@ Webhooks provide a way for external systems to send HTTP requests directly to yo
`createWebhook()` creates a public endpoint at `/.well-known/workflow/v1/webhook/:token`, and the token in that URL is the only authorization performed for incoming requests resuming that webhook. This is convenient for prototypes and simple resume links because it avoids creating another route, but if you need stronger security, prefer [`createHook()`](/docs/api-reference/workflow/create-hook) behind your own route and authorize the request before calling [`resumeHook()`](/docs/api-reference/workflow-api/resume-hook) to avoid unauthenticated workflow resumptions.
+
+`createWebhook()` does **not** accept a `token` option. Webhook tokens are always randomly generated. Use [`createHook()`](/docs/api-reference/workflow/create-hook) with [`resumeHook()`](/docs/api-reference/workflow-api/resume-hook) for deterministic token patterns.
+
+
+`createWebhook` has two overloads:
+
+- **Default mode** — `createWebhook(options?)` returns `Webhook`. The caller automatically receives a `202 Accepted` response (or the `Response` object you pass as `respondWith`).
+- **Manual-response mode** — `createWebhook({ respondWith: 'manual' })` returns `Webhook`. Each request exposes a `respondWith()` method so the workflow can send a custom HTTP response from within a step function.
+
```ts lineNumbers
import { createWebhook } from "workflow"
@@ -54,7 +63,7 @@ showSections={['returns']}
The returned `Webhook` object has:
- `url`: The HTTP endpoint URL that external systems can call
-- `token`: The unique token identifying this webhook
+- `token`: The unique, randomly generated token identifying this webhook
- Implements `AsyncIterable` for handling multiple requests, where `T` is `Request` (default) or `RequestWithResponse` (manual mode)
When using `createWebhook({ respondWith: 'manual' })`, the resolved request type is `RequestWithResponse`, which extends the standard `Request` interface with a `respondWith(response: Response): Promise` method for sending custom responses back to the caller.
@@ -122,7 +131,7 @@ async function sendResponse(request: RequestWithResponse): Promise {
export async function respondingWebhookWorkflow() {
"use workflow";
- using webhook = createWebhook({ respondWith: "manual" });
+ using webhook = createWebhook({ respondWith: "manual" }); // [!code highlight]
console.log("Webhook URL:", webhook.url);
const request = await webhook;
diff --git a/docs/content/docs/api-reference/workflow/define-hook.mdx b/docs/content/docs/api-reference/workflow/define-hook.mdx
index ad0d697f30..4bf6d7fb71 100644
--- a/docs/content/docs/api-reference/workflow/define-hook.mdx
+++ b/docs/content/docs/api-reference/workflow/define-hook.mdx
@@ -13,6 +13,12 @@ Creates a type-safe hook helper that ensures the payload type is consistent betw
This is a lightweight wrapper around [`createHook()`](/docs/api-reference/workflow/create-hook) and [`resumeHook()`](/docs/api-reference/workflow-api/resume-hook) to avoid type mismatches. It also supports optional runtime validation and transformation of payloads using any [Standard Schema v1](https://standardschema.dev) compliant validator like Zod or Valibot.
+When a `schema` is provided, the hook is typed as `TypedHook` — the `resume()` method accepts `TInput` (the raw payload), while the workflow receives `TOutput` (the validated and potentially transformed result). Without a schema, `TOutput` defaults to `TInput`.
+
+
+`defineHook()` spans both execution contexts: call `.create()` inside `"use workflow"` code to create the hook, and call `.resume()` from runtime code (such as API routes or server actions) to resume it. Calling `.create()` outside a workflow or `.resume()` inside one will throw an error.
+
+
We recommend using `defineHook()` over `createHook()` in production codebases for better type safety and optional runtime validation.
@@ -48,20 +54,8 @@ showSections={['parameters']}
{
- /**
-
-* Creates a new hook with the defined payload type.
- */
- create: (options?: HookOptions) => Hook;
-
- /**
-
-* Resumes a hook by sending a payload with the defined type.
- */
- resume: (token: string, payload: T) => Promise;
-}
-export default DefineHook;`}
+import type { TypedHook } from "workflow";
+export default TypedHook;`}
/>
## Examples
@@ -93,24 +87,29 @@ export async function workflowWithApproval() {
### Resuming with Type Safety
-Hooks can be resumed using the same defined hook and a token. By using the same hook, you can ensure that the payload matches the defined type when resuming a hook.
+Hooks can be resumed using the same defined hook and a token. By using the same hook, you can ensure that the payload matches the defined type when resuming a hook. The `resume()` method throws an error if the hook is not found or if schema validation fails.
```typescript lineNumbers
+import { HookNotFoundError } from "workflow/errors";
+
// Use the same defined hook to resume
export async function POST(request: Request) {
const { token, approved, comment } = await request.json();
- // Type-safe resumption - TypeScript ensures the payload matches
- const result = await approvalHook.resume(token, { // [!code highlight]
- approved, // [!code highlight]
- comment, // [!code highlight]
- }); // [!code highlight]
-
- if (!result) {
- return Response.json({ error: "Hook not found" }, { status: 404 });
+ try {
+ // Type-safe resumption - TypeScript ensures the payload matches
+ const result = await approvalHook.resume(token, { // [!code highlight]
+ approved, // [!code highlight]
+ comment, // [!code highlight]
+ }); // [!code highlight]
+
+ return Response.json({ success: true, runId: result.runId });
+ } catch (error) {
+ if (HookNotFoundError.is(error)) { // [!code highlight]
+ return Response.json({ error: "Hook not found" }, { status: 404 }); // [!code highlight]
+ } // [!code highlight]
+ return Response.json({ error: "Resume failed" }, { status: 500 });
}
-
- return Response.json({ success: true, runId: result.runId });
}
```
diff --git a/docs/content/docs/api-reference/workflow/fatal-error.mdx b/docs/content/docs/api-reference/workflow/fatal-error.mdx
index 8c31e1e611..57b5f53ecc 100644
--- a/docs/content/docs/api-reference/workflow/fatal-error.mdx
+++ b/docs/content/docs/api-reference/workflow/fatal-error.mdx
@@ -9,9 +9,9 @@ related:
- /docs/api-reference/workflow/retryable-error
---
-When a `FatalError` is thrown in a step, it indicates that the workflow should not retry a step, marking it as failure.
+When a `FatalError` is thrown in a step, the step fails without retrying and the error bubbles back to the workflow logic.
-You should use this when you don't want a specific step to retry.
+Use `FatalError` when a failure is intentional or unrecoverable and retrying would be wasteful or harmful.
```typescript lineNumbers
import { FatalError } from "workflow"
@@ -29,16 +29,20 @@ async function fallibleStep() {
## API Signature
-### Parameters
+### Constructor
-
+### Instance Properties
+
+| Property | Type | Description |
+| -------- | ------ | ------------------------------------------------ |
+| `fatal` | `boolean` | Always initialized to `true`. Marks the error as non-retryable. |
+
+### Static Methods
+
+#### `FatalError.is(value)`
+
+Returns `true` if `value` is a `FatalError` instance. Useful for checking caught errors without `instanceof`.
diff --git a/docs/content/docs/api-reference/workflow/fetch.mdx b/docs/content/docs/api-reference/workflow/fetch.mdx
index e1d18f44ac..51e71a3b48 100644
--- a/docs/content/docs/api-reference/workflow/fetch.mdx
+++ b/docs/content/docs/api-reference/workflow/fetch.mdx
@@ -1,20 +1,20 @@
---
title: fetch
-description: Make HTTP requests from workflows with automatic serialization and retry semantics.
+description: Make HTTP requests from workflows using a step-wrapped fetch.
type: reference
-summary: Use the workflow-aware fetch to make HTTP requests with automatic serialization and retry semantics.
+summary: Use the workflow-aware fetch to make HTTP requests from workflow code.
prerequisites:
- /docs/foundations/workflows-and-steps
related:
- /docs/errors/fetch-in-workflow
---
-Makes HTTP requests from within a workflow. This is a special step function that wraps the standard `fetch` API, automatically handling serialization and providing retry semantics.
+Makes HTTP requests from within a workflow. This is a hoisted `"use step"` wrapper over `globalThis.fetch`, allowing you to call `fetch` directly inside a `"use workflow"` function. Because it runs as a step, thrown request failures follow the normal step retry policy, while ordinary HTTP responses (including `4xx` and `5xx`) are returned as normal `Response` objects.
This is useful when you need to call external APIs or services from within your workflow.
-`fetch` is a *special* type of step function provided and should be called directly inside workflow functions.
+`fetch` is a `"use step"` wrapper and should be called directly inside workflow functions. It does not add HTTP-status-aware retry behavior on top of the standard `fetch` API. If you want retries to depend on `response.status`, headers, or body content, wrap `globalThis.fetch` in your own `"use step"` function and throw [`FatalError`](/docs/api-reference/workflow/fatal-error) or [`RetryableError`](/docs/api-reference/workflow/retryable-error) yourself.
```typescript lineNumbers
@@ -82,9 +82,9 @@ async function apiWorkflow() {
}
```
-We call `fetch()` with a URL and optional request options, just like the standard fetch API. The workflow runtime automatically handles the response serialization.
+We call `fetch()` with a URL and optional request options, just like the standard `fetch` API. Because `fetch` runs as a step, the workflow runtime handles serialization and replay. If the request throws, normal step retry behavior applies; if it returns a `Response`, your code decides whether that response should be treated as success, fatal failure, or retryable failure.
-This API is provided as a convenience to easily use `fetch` in workflow, but often, you might want to extend and implement your own fetch for more powerful error handing and retry logic.
+This API is provided as a convenience to easily use `fetch` in a workflow, but you may want to write your own `"use step"` wrapper when retries should depend on HTTP response details such as `status`, headers, or body content.
### Customizing Fetch Behavior
@@ -99,7 +99,7 @@ export async function customFetch(
) {
"use step"
- const response = await fetch(url, init)
+ const response = await globalThis.fetch(url, init)
// Handle client errors (4xx) - don't retry
if (response.status >= 400 && response.status < 500) {
@@ -140,7 +140,6 @@ export async function customFetch(
This example demonstrates:
-- Setting custom `maxRetries` to 5 retries (6 total attempts including the initial attempt).
- Throwing [`FatalError`](/docs/api-reference/workflow/fatal-error) for client errors (400-499) to prevent retries.
- Handling 429 rate limiting by reading the `Retry-After` header and using [`RetryableError`](/docs/api-reference/workflow/retryable-error).
-- Allowing automatic retries for server errors (5xx).
+- Allowing automatic retries for server errors (5xx) by throwing a plain `Error`.
diff --git a/docs/content/docs/api-reference/workflow/get-step-metadata.mdx b/docs/content/docs/api-reference/workflow/get-step-metadata.mdx
index 27cdfaa754..a42affd85e 100644
--- a/docs/content/docs/api-reference/workflow/get-step-metadata.mdx
+++ b/docs/content/docs/api-reference/workflow/get-step-metadata.mdx
@@ -74,6 +74,15 @@ export default getStepMetadata;`}
### Returns
+Returns a `StepMetadata` object with the following fields:
+
+| Field | Type | Description |
+| --- | --- | --- |
+| `stepName` | `string` | The name of the current step. |
+| `stepId` | `string` | Unique identifier for the current step execution. Useful as part of an idempotency key. |
+| `stepStartedAt` | `Date` | Timestamp when the current step started. |
+| `attempt` | `number` | The number of times the current step has been executed. Increases with each retry. |
+
-If you need to access step context, take a look at [`getStepMetadata`](/docs/api-reference/workflow/get-step-metadata).
+If you need to access step-specific context, take a look at [`getStepMetadata`](/docs/api-reference/workflow/get-step-metadata).
```typescript lineNumbers
@@ -103,6 +104,16 @@ showSections={['parameters']}
### Returns
+Returns a `WorkflowMetadata` object with the following fields:
+
+| Field | Type | Description |
+| --- | --- | --- |
+| `workflowName` | `string` | The name of the workflow. |
+| `workflowRunId` | `string` | Unique identifier for the workflow run. |
+| `workflowStartedAt` | `Date` | Timestamp when the workflow run started. |
+| `url` | `string` | The URL where the workflow can be triggered. |
+| `features.encryption` | `boolean` | Whether serialized workflow data is encrypted at rest for the current run. |
+
-
- A function that returns context about the current workflow execution.
-
-
- A function that returns context about the current step execution.
-
- Sleeping workflows for a specified duration. Deterministic and replay-safe.
+ Suspend a workflow for a specified duration or until a date. Deterministic and replay-safe.
- Make HTTP requests from within a workflow with automatic retry semantics.
+ Make HTTP requests from within a workflow. Runs as a step under the hood.
- Create a low-level hook to receive arbitrary payloads from external systems.
-
-
- Type-safe hook helper for consistent payload types.
+ Create a hook with an optional custom token, resumed server-side via `resumeHook()`.
- Create a webhook that suspends the workflow until an HTTP request is received.
+ Create a webhook with a randomly generated token and public HTTP endpoint.
+
+
+
+## Workflow and Step APIs
+
+These functions can be called from either `"use workflow"` or `"use step"` functions:
+
+
+
+ Return context about the current workflow execution.
- Access the current workflow run's default stream.
+ Access the current workflow run's stream.
+
+
+
+## Cross-context Helpers
+
+These helpers span workflow and runtime contexts:
+
+
+
+ Define a type-safe hook helper whose `.create()` method is workflow-only and whose `.resume()` method is runtime-side.
+
+
+
+## Step-only APIs
+
+These functions must be called inside `"use step"` functions:
+
+
+
+ Return context about the current step execution.
## Error Classes
-Workflow SDK includes error classes that can be thrown in a workflow or step to change the error exit strategy of a workflow.
+Error classes that can be thrown inside a step to control retry behavior.
diff --git a/docs/content/docs/api-reference/workflow/retryable-error.mdx b/docs/content/docs/api-reference/workflow/retryable-error.mdx
index 7783e4c981..a383b84a21 100644
--- a/docs/content/docs/api-reference/workflow/retryable-error.mdx
+++ b/docs/content/docs/api-reference/workflow/retryable-error.mdx
@@ -33,26 +33,30 @@ The difference between `Error` and `RetryableError` may not be entirely obvious,
## API Signature
-### Parameters
-
-
+### Static Methods
-#### RetryableErrorOptions
+#### `RetryableError.is(value)`
-
+Returns `true` if `value` is a `RetryableError` instance. Useful for checking caught errors without `instanceof`.
## Examples
diff --git a/docs/content/docs/api-reference/workflow/sleep.mdx b/docs/content/docs/api-reference/workflow/sleep.mdx
index 86d7b14f9e..92cade0753 100644
--- a/docs/content/docs/api-reference/workflow/sleep.mdx
+++ b/docs/content/docs/api-reference/workflow/sleep.mdx
@@ -14,7 +14,7 @@ Suspends a workflow for a specified duration or until an end date without consum
This is useful when you want to resume a workflow after some duration or date.
-`sleep` is a *special* type of step function and should be called directly inside workflow functions.
+`sleep` is a built-in workflow runtime function and should be called directly inside workflow functions.
```typescript lineNumbers
@@ -37,11 +37,19 @@ export default sleep;`}
showSections={['parameters']}
/>
+## Overloads
+
+`sleep` accepts three types of arguments:
+
+- **`StringValue`** — a human-readable duration string such as `"10s"`, `"1m"`, `"1h"`, or `"1d"`.
+- **`Date`** — a future `Date` object to sleep until.
+- **`number`** — a duration in milliseconds.
+
## Examples
-### Sleeping With a Duration
+### Sleeping With a Duration String
-You can specify a duration for `sleep` to suspend the workflow for a fixed amount of time.
+You can specify a duration string for `sleep` to suspend the workflow for a fixed amount of time.
```typescript lineNumbers
import { sleep } from "workflow"
@@ -52,15 +60,28 @@ async function testWorkflow() {
}
```
-### Sleeping Until an End Date
+### Sleeping With Milliseconds
-You can specify a future `Date` object for `sleep` to suspend the workflow until a specific date.
+You can specify a number of milliseconds for `sleep` to suspend the workflow.
```typescript lineNumbers
import { sleep } from "workflow"
async function testWorkflow() {
"use workflow"
- await sleep(new Date(Date.now() + 10_000)) // [!code highlight]
+ await sleep(10_000) // [!code highlight]
+}
+```
+
+### Sleeping Until an End Date
+
+Pass a `Date` into the workflow and sleep until that exact timestamp.
+
+```typescript lineNumbers
+import { sleep } from "workflow"
+
+async function testWorkflow(wakeAt: Date) {
+ "use workflow"
+ await sleep(wakeAt) // [!code highlight]
}
```
diff --git a/docs/content/docs/getting-started/astro.mdx b/docs/content/docs/getting-started/astro.mdx
index 37012bc810..9acdaec62c 100644
--- a/docs/content/docs/getting-started/astro.mdx
+++ b/docs/content/docs/getting-started/astro.mdx
@@ -242,7 +242,7 @@ Additionally, check the [Deploying](/docs/deploying) section to learn how your w
If you see this error:
```
-'start' received an invalid workflow function. Ensure the Workflow Development Kit is configured correctly and the function includes a 'use workflow' directive.
+'start' received an invalid workflow function. Ensure the Workflow SDK is configured correctly and the function includes a 'use workflow' directive.
```
Check both of these first:
diff --git a/docs/content/docs/getting-started/express.mdx b/docs/content/docs/getting-started/express.mdx
index cc50a2aedd..b643a60293 100644
--- a/docs/content/docs/getting-started/express.mdx
+++ b/docs/content/docs/getting-started/express.mdx
@@ -269,7 +269,7 @@ Check the [Deploying](/docs/deploying) section to learn how your workflows can b
If you see this error:
```
-'start' received an invalid workflow function. Ensure the Workflow Development Kit is configured correctly and the function includes a 'use workflow' directive.
+'start' received an invalid workflow function. Ensure the Workflow SDK is configured correctly and the function includes a 'use workflow' directive.
```
Check both of these first:
diff --git a/docs/content/docs/getting-started/fastify.mdx b/docs/content/docs/getting-started/fastify.mdx
index 15ccd343ae..bc7bacb38d 100644
--- a/docs/content/docs/getting-started/fastify.mdx
+++ b/docs/content/docs/getting-started/fastify.mdx
@@ -256,7 +256,7 @@ Check the [Deploying](/docs/deploying) section to learn how your workflows can b
If you see this error:
```
-'start' received an invalid workflow function. Ensure the Workflow Development Kit is configured correctly and the function includes a 'use workflow' directive.
+'start' received an invalid workflow function. Ensure the Workflow SDK is configured correctly and the function includes a 'use workflow' directive.
```
Check both of these first:
diff --git a/docs/content/docs/getting-started/hono.mdx b/docs/content/docs/getting-started/hono.mdx
index 55a56b7d9b..3ea2f4d2b4 100644
--- a/docs/content/docs/getting-started/hono.mdx
+++ b/docs/content/docs/getting-started/hono.mdx
@@ -251,7 +251,7 @@ Check the [Deploying](/docs/deploying) section to learn how your workflows can b
If you see this error:
```
-'start' received an invalid workflow function. Ensure the Workflow Development Kit is configured correctly and the function includes a 'use workflow' directive.
+'start' received an invalid workflow function. Ensure the Workflow SDK is configured correctly and the function includes a 'use workflow' directive.
```
Check both of these first:
diff --git a/docs/content/docs/getting-started/nestjs.mdx b/docs/content/docs/getting-started/nestjs.mdx
index afe193c0e9..6e1d0fecae 100644
--- a/docs/content/docs/getting-started/nestjs.mdx
+++ b/docs/content/docs/getting-started/nestjs.mdx
@@ -396,7 +396,7 @@ WorkflowModule.forRoot({
If you see this error:
```
-'start' received an invalid workflow function. Ensure the Workflow Development Kit is configured correctly and the function includes a 'use workflow' directive.
+'start' received an invalid workflow function. Ensure the Workflow SDK is configured correctly and the function includes a 'use workflow' directive.
```
Check both of these first:
diff --git a/docs/content/docs/getting-started/next.mdx b/docs/content/docs/getting-started/next.mdx
index c9785d4aec..32c4de3d4c 100644
--- a/docs/content/docs/getting-started/next.mdx
+++ b/docs/content/docs/getting-started/next.mdx
@@ -310,7 +310,7 @@ Without this configuration, you may experience intermittent issues where workflo
If you see this error:
```
-'start' received an invalid workflow function. Ensure the Workflow Development Kit is configured correctly and the function includes a 'use workflow' directive.
+'start' received an invalid workflow function. Ensure the Workflow SDK is configured correctly and the function includes a 'use workflow' directive.
```
Check both of these first:
diff --git a/docs/content/docs/getting-started/nitro.mdx b/docs/content/docs/getting-started/nitro.mdx
index 688b965ebe..fdcf2d20ad 100644
--- a/docs/content/docs/getting-started/nitro.mdx
+++ b/docs/content/docs/getting-started/nitro.mdx
@@ -235,7 +235,7 @@ Check the [Deploying](/docs/deploying) section to learn how your workflows can b
If you see this error:
```
-'start' received an invalid workflow function. Ensure the Workflow Development Kit is configured correctly and the function includes a 'use workflow' directive.
+'start' received an invalid workflow function. Ensure the Workflow SDK is configured correctly and the function includes a 'use workflow' directive.
```
Check both of these first:
diff --git a/docs/content/docs/getting-started/nuxt.mdx b/docs/content/docs/getting-started/nuxt.mdx
index fa50472c40..0d2bb3bb36 100644
--- a/docs/content/docs/getting-started/nuxt.mdx
+++ b/docs/content/docs/getting-started/nuxt.mdx
@@ -236,7 +236,7 @@ Check the [Deploying](/docs/deploying) section to learn how your workflows can b
If you see this error:
```
-'start' received an invalid workflow function. Ensure the Workflow Development Kit is configured correctly and the function includes a 'use workflow' directive.
+'start' received an invalid workflow function. Ensure the Workflow SDK is configured correctly and the function includes a 'use workflow' directive.
```
Check both of these first:
diff --git a/docs/content/docs/getting-started/sveltekit.mdx b/docs/content/docs/getting-started/sveltekit.mdx
index 9dd77e158b..636c9120eb 100644
--- a/docs/content/docs/getting-started/sveltekit.mdx
+++ b/docs/content/docs/getting-started/sveltekit.mdx
@@ -235,7 +235,7 @@ Check the [Deploying](/docs/deploying) section to learn how your workflows can b
If you see this error:
```
-'start' received an invalid workflow function. Ensure the Workflow Development Kit is configured correctly and the function includes a 'use workflow' directive.
+'start' received an invalid workflow function. Ensure the Workflow SDK is configured correctly and the function includes a 'use workflow' directive.
```
Check both of these first:
diff --git a/docs/content/docs/getting-started/vite.mdx b/docs/content/docs/getting-started/vite.mdx
index 983825513d..f7ff67dc13 100644
--- a/docs/content/docs/getting-started/vite.mdx
+++ b/docs/content/docs/getting-started/vite.mdx
@@ -241,7 +241,7 @@ Check the [Deploying](/docs/deploying) section to learn how your workflows can b
If you see this error:
```
-'start' received an invalid workflow function. Ensure the Workflow Development Kit is configured correctly and the function includes a 'use workflow' directive.
+'start' received an invalid workflow function. Ensure the Workflow SDK is configured correctly and the function includes a 'use workflow' directive.
```
Check both of these first:
diff --git a/packages/docs-typecheck/src/__tests__/api-reference-public-contract.test.ts b/packages/docs-typecheck/src/__tests__/api-reference-public-contract.test.ts
new file mode 100644
index 0000000000..7001ab3307
--- /dev/null
+++ b/packages/docs-typecheck/src/__tests__/api-reference-public-contract.test.ts
@@ -0,0 +1,403 @@
+import fs from 'node:fs';
+import path from 'node:path';
+import { fileURLToPath } from 'node:url';
+import { globSync } from 'glob';
+import ts from 'typescript';
+import { describe, expect, it } from 'vitest';
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
+const repoRoot = path.resolve(__dirname, '../../../..');
+
+type PackageJson = {
+ name?: string;
+ exports?: Record | string;
+};
+
+type DocumentedContract = {
+ file: string;
+ module: string;
+ values?: string[];
+ types?: string[];
+};
+
+const documentedContracts: DocumentedContract[] = [
+ {
+ file: 'docs/content/docs/api-reference/workflow/create-hook.mdx',
+ module: 'workflow',
+ values: ['createHook'],
+ types: ['HookOptions', 'Hook'],
+ },
+ {
+ file: 'docs/content/docs/api-reference/workflow/create-webhook.mdx',
+ module: 'workflow',
+ values: ['createWebhook'],
+ types: ['RequestWithResponse', 'WebhookOptions'],
+ },
+ {
+ file: 'docs/content/docs/api-reference/workflow/sleep.mdx',
+ module: 'workflow',
+ values: ['sleep'],
+ },
+ {
+ file: 'docs/content/docs/api-reference/workflow/fetch.mdx',
+ module: 'workflow',
+ values: ['fetch'],
+ },
+ {
+ file: 'docs/content/docs/api-reference/workflow/fatal-error.mdx',
+ module: 'workflow',
+ values: ['FatalError'],
+ },
+ {
+ file: 'docs/content/docs/api-reference/workflow/retryable-error.mdx',
+ module: 'workflow',
+ values: ['RetryableError'],
+ types: ['RetryableErrorOptions'],
+ },
+ {
+ file: 'docs/content/docs/api-reference/workflow/get-workflow-metadata.mdx',
+ module: 'workflow',
+ values: ['getWorkflowMetadata'],
+ types: ['WorkflowMetadata'],
+ },
+ {
+ file: 'docs/content/docs/api-reference/workflow/get-step-metadata.mdx',
+ module: 'workflow',
+ values: ['getStepMetadata'],
+ types: ['StepMetadata'],
+ },
+ {
+ file: 'docs/content/docs/api-reference/workflow/define-hook.mdx',
+ module: 'workflow',
+ values: ['defineHook'],
+ types: ['TypedHook'],
+ },
+ {
+ file: 'docs/content/docs/api-reference/workflow-api/resume-hook.mdx',
+ module: 'workflow/api',
+ values: ['resumeHook'],
+ },
+ {
+ file: 'docs/content/docs/api-reference/workflow-api/resume-webhook.mdx',
+ module: 'workflow/api',
+ values: ['resumeWebhook'],
+ },
+ {
+ file: 'docs/content/docs/api-reference/workflow-api/get-hook-by-token.mdx',
+ module: 'workflow/api',
+ values: ['getHookByToken'],
+ },
+ {
+ file: 'docs/content/docs/api-reference/workflow-api/start.mdx',
+ module: 'workflow/api',
+ values: ['Run', 'start'],
+ types: ['StartOptions'],
+ },
+ {
+ file: 'docs/content/docs/api-reference/workflow-api/get-run.mdx',
+ module: 'workflow/api',
+ values: ['Run', 'getRun'],
+ types: [
+ 'StopSleepOptions',
+ 'StopSleepResult',
+ 'WorkflowReadableStream',
+ 'WorkflowReadableStreamOptions',
+ ],
+ },
+ {
+ file: 'docs/content/docs/api-reference/workflow-api/get-world.mdx',
+ module: 'workflow/runtime',
+ values: ['getWorld'],
+ },
+ {
+ file: 'docs/content/docs/api-reference/workflow-api/get-world.mdx',
+ module: '@workflow/world',
+ types: ['World'],
+ },
+ {
+ file: 'docs/content/docs/api-reference/workflow-next/with-workflow.mdx',
+ module: 'workflow/next',
+ values: ['withWorkflow'],
+ },
+ {
+ file: 'docs/content/docs/api-reference/workflow-ai/index.mdx',
+ module: '@workflow/ai',
+ values: ['WorkflowChatTransport'],
+ types: ['ModelMessage'],
+ },
+ {
+ file: 'docs/content/docs/api-reference/workflow-api/resume-hook.mdx',
+ module: 'workflow/errors',
+ values: ['HookNotFoundError'],
+ },
+];
+
+const extraPublicModules = [
+ '@workflow/ai',
+ '@workflow/ai/agent',
+ '@workflow/ai/anthropic',
+ '@workflow/ai/gateway',
+ '@workflow/ai/google',
+ '@workflow/ai/openai',
+ '@workflow/ai/xai',
+ '@workflow/ai/test',
+ '@workflow/next',
+ '@workflow/utils/parse-name',
+];
+
+const publicModules = Array.from(
+ new Set([
+ ...documentedContracts.map((contract) => contract.module),
+ ...extraPublicModules,
+ ])
+);
+
+function readJson(relativePath: string): PackageJson {
+ return JSON.parse(
+ fs.readFileSync(path.join(repoRoot, relativePath), 'utf-8')
+ );
+}
+
+function getPackageRoots(): Map {
+ return new Map(
+ globSync(path.join(repoRoot, 'packages/*/package.json'))
+ .map((packageJsonPath) => {
+ const packageJson = JSON.parse(
+ fs.readFileSync(packageJsonPath, 'utf-8')
+ ) as PackageJson;
+ if (!packageJson.name) return undefined;
+ return [
+ packageJson.name,
+ path.relative(repoRoot, path.dirname(packageJsonPath)),
+ ] as const;
+ })
+ .filter((entry): entry is readonly [string, string] => Boolean(entry))
+ );
+}
+
+const packageRoots = getPackageRoots();
+
+function splitPackageSpecifier(specifier: string): {
+ packageName: string;
+ subpath: string;
+} {
+ if (specifier.startsWith('@')) {
+ const [scope, name, ...rest] = specifier.split('/');
+ return {
+ packageName: `${scope}/${name}`,
+ subpath: rest.length === 0 ? '.' : `./${rest.join('/')}`,
+ };
+ }
+
+ const [name, ...rest] = specifier.split('/');
+ return {
+ packageName: name,
+ subpath: rest.length === 0 ? '.' : `./${rest.join('/')}`,
+ };
+}
+
+function getExportEntry(packageJson: PackageJson, subpath: string): unknown {
+ if (typeof packageJson.exports === 'string') {
+ return subpath === '.' ? packageJson.exports : undefined;
+ }
+ return packageJson.exports?.[subpath];
+}
+
+function getExportTarget(entry: unknown): string | undefined {
+ if (typeof entry === 'string') return entry;
+ if (!entry || typeof entry !== 'object') return undefined;
+
+ const record = entry as Record;
+ for (const key of ['types', 'default', 'workflow', 'require']) {
+ const value = record[key];
+ if (typeof value === 'string') return value;
+ }
+}
+
+function declarationCandidates(target: string): string[] {
+ if (target.endsWith('.d.ts') || target.endsWith('.d.cts')) return [target];
+ if (target.endsWith('.cjs')) return [target.replace(/\.cjs$/, '.d.cts')];
+ if (target.endsWith('.js')) return [target.replace(/\.js$/, '.d.ts')];
+ return [`${target}.d.ts`, path.join(target, 'index.d.ts')];
+}
+
+function modulePathMapping(specifier: string): string[] {
+ const { packageName, subpath } = splitPackageSpecifier(specifier);
+ const packageRoot = packageRoots.get(packageName);
+ if (!packageRoot)
+ throw new Error(`No package root configured for ${specifier}`);
+
+ const packageJson = readJson(path.join(packageRoot, 'package.json'));
+ const entry = getExportEntry(packageJson, subpath);
+ const target = getExportTarget(entry);
+ if (!target) throw new Error(`No export target for ${specifier}`);
+
+ return declarationCandidates(target).map((candidate) =>
+ path.join(repoRoot, packageRoot, candidate)
+ );
+}
+
+function formatDiagnostics(diagnostics: readonly ts.Diagnostic[]): string {
+ return diagnostics
+ .map((diagnostic) => {
+ const message = ts.flattenDiagnosticMessageText(
+ diagnostic.messageText,
+ '\n'
+ );
+ if (!diagnostic.file || diagnostic.start === undefined) {
+ return `TS${diagnostic.code}: ${message}`;
+ }
+
+ const { line, character } = diagnostic.file.getLineAndCharacterOfPosition(
+ diagnostic.start
+ );
+ return `${path.basename(diagnostic.file.fileName)}:${line + 1}:${
+ character + 1
+ } TS${diagnostic.code}: ${message}`;
+ })
+ .join('\n');
+}
+
+function compileVirtualContract(source: string): readonly ts.Diagnostic[] {
+ const contractPath = path.join(repoRoot, '__api_reference_contract__.ts');
+ const virtualFiles = new Map([[contractPath, source]]);
+
+ const compilerOptions: ts.CompilerOptions = {
+ target: ts.ScriptTarget.ES2022,
+ module: ts.ModuleKind.ESNext,
+ moduleResolution: ts.ModuleResolutionKind.Bundler,
+ moduleDetection: ts.ModuleDetectionKind.Force,
+ lib: [
+ 'lib.es2022.d.ts',
+ 'lib.dom.d.ts',
+ 'lib.dom.iterable.d.ts',
+ 'lib.dom.asynciterable.d.ts',
+ 'lib.esnext.disposable.d.ts',
+ ],
+ strict: true,
+ skipLibCheck: true,
+ noEmit: true,
+ esModuleInterop: true,
+ allowSyntheticDefaultImports: true,
+ baseUrl: repoRoot,
+ paths: Object.fromEntries(
+ publicModules.map((specifier) => [
+ specifier,
+ modulePathMapping(specifier),
+ ])
+ ),
+ types: ['node'],
+ typeRoots: [
+ path.join(__dirname, '../../node_modules/@types'),
+ path.join(repoRoot, 'node_modules/@types'),
+ ],
+ };
+
+ const defaultHost = ts.createCompilerHost(compilerOptions);
+ const host: ts.CompilerHost = {
+ ...defaultHost,
+ getSourceFile: (fileName, languageVersion) => {
+ const content = virtualFiles.get(fileName);
+ if (content !== undefined) {
+ return ts.createSourceFile(fileName, content, languageVersion, true);
+ }
+ return defaultHost.getSourceFile(fileName, languageVersion);
+ },
+ fileExists: (fileName) =>
+ virtualFiles.has(fileName) || defaultHost.fileExists(fileName),
+ readFile: (fileName) =>
+ virtualFiles.get(fileName) ?? defaultHost.readFile(fileName),
+ getCurrentDirectory: () => repoRoot,
+ };
+
+ const program = ts.createProgram([contractPath], compilerOptions, host);
+ return ts.getPreEmitDiagnostics(program);
+}
+
+describe('API reference public contract', () => {
+ it('covers API reference files that exist', () => {
+ for (const contract of documentedContracts) {
+ expect(
+ fs.existsSync(path.join(repoRoot, contract.file)),
+ `${contract.file} should exist`
+ ).toBe(true);
+ }
+ });
+
+ it('documents import paths that exist in package exports and declaration output', () => {
+ for (const specifier of publicModules) {
+ const candidates = modulePathMapping(specifier);
+ expect(
+ candidates.some((candidate) => fs.existsSync(candidate)),
+ `${specifier} should resolve to one of:\n${candidates.join('\n')}`
+ ).toBe(true);
+ }
+ });
+
+ it('documents symbols that are exported from their public modules', () => {
+ const imports = documentedContracts
+ .flatMap((contract, index) => {
+ const values = (contract.values ?? []).map((exportName) => {
+ const alias = `contract_${index}_${exportName}`;
+ return `import { ${exportName} as ${alias} } from '${contract.module}';`;
+ });
+ const types = (contract.types ?? []).map((exportName) => {
+ const alias = `contract_${index}_${exportName}`;
+ return `import type { ${exportName} as ${alias} } from '${contract.module}';`;
+ });
+ return [...values, ...types];
+ })
+ .join('\n');
+
+ const diagnostics = compileVirtualContract(imports);
+ expect(formatDiagnostics(diagnostics)).toBe('');
+ });
+
+ it('preserves the high-risk API signatures described by the reference docs', () => {
+ const source = `
+ import { getHookByToken, resumeHook, resumeWebhook } from 'workflow/api';
+ import { getWorld } from 'workflow/runtime';
+ import { createWebhook, fetch, RetryableError, sleep } from 'workflow';
+ import type { Hook, World } from '@workflow/world';
+
+ declare const hook: Hook;
+
+ const resumedFromToken: Promise = resumeHook('token', { ok: true });
+ const resumedFromHook: Promise = resumeHook(hook, { ok: true });
+ const lookedUpHook: Promise = getHookByToken('token');
+ const webhookResponse: Promise = resumeWebhook(
+ 'token',
+ new Request('https://example.test')
+ );
+
+ async function manualWebhookCheck() {
+ const request = await createWebhook({ respondWith: 'manual' });
+ await request.respondWith(new Response('ok'));
+ }
+
+ async function sleepCheck() {
+ await sleep('10s');
+ await sleep(1000);
+ await sleep(new Date(Date.now() + 1000));
+ }
+
+ const retryable = new RetryableError('retry', { retryAfter: '5s' });
+ const retryAfter: Date = retryable.retryAfter;
+ const worldPromise: Promise = getWorld();
+ const responsePromise: Promise = fetch('https://example.test');
+
+ void manualWebhookCheck;
+ void sleepCheck;
+ void retryAfter;
+ void worldPromise;
+ void responsePromise;
+ void resumedFromToken;
+ void resumedFromHook;
+ void lookedUpHook;
+ void webhookResponse;
+ `;
+
+ const diagnostics = compileVirtualContract(source);
+ expect(formatDiagnostics(diagnostics)).toBe('');
+ });
+});
diff --git a/packages/docs-typecheck/src/__tests__/hook-runtime-docs.test.ts b/packages/docs-typecheck/src/__tests__/hook-runtime-docs.test.ts
new file mode 100644
index 0000000000..4a4de3166d
--- /dev/null
+++ b/packages/docs-typecheck/src/__tests__/hook-runtime-docs.test.ts
@@ -0,0 +1,47 @@
+import fs from 'node:fs';
+import path from 'node:path';
+import { fileURLToPath } from 'node:url';
+import { describe, expect, it } from 'vitest';
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
+const repoRoot = path.resolve(__dirname, '../../../..');
+
+const read = (relativePath: string) =>
+ fs.readFileSync(path.join(repoRoot, relativePath), 'utf-8');
+
+describe('hook runtime API docs stay aligned with runtime behavior', () => {
+ it('documents hydrated hook metadata and the hook-object resumeHook overload', () => {
+ const getHookDoc = read(
+ 'docs/content/docs/api-reference/workflow-api/get-hook-by-token.mdx'
+ );
+ const resumeHookDoc = read(
+ 'docs/content/docs/api-reference/workflow-api/resume-hook.mdx'
+ );
+
+ expect(getHookDoc).toContain(
+ 'Metadata is automatically hydrated (deserialized)'
+ );
+ expect(resumeHookDoc).toContain('token or hook object');
+ expect(resumeHookDoc).toContain('await resumeHook(hook, data)');
+ expect(resumeHookDoc).toContain('hook token or hook object');
+ });
+
+ it('does not imply getWorld() is imported from workflow/api', () => {
+ const apiIndexDoc = read(
+ 'docs/content/docs/api-reference/workflow-api/index.mdx'
+ );
+ const getWorldDoc = read(
+ 'docs/content/docs/api-reference/workflow-api/get-world.mdx'
+ );
+
+ expect(getWorldDoc).toContain(
+ 'import { getWorld } from "workflow/runtime";'
+ );
+
+ expect(apiIndexDoc).toContain('workflow/api and workflow/runtime');
+ expect(apiIndexDoc).toContain('from `workflow/runtime`');
+ expect(apiIndexDoc).not.toContain(
+ 'API reference for runtime functions from the `workflow/api` package.'
+ );
+ });
+});
diff --git a/packages/docs-typecheck/src/type-checker.ts b/packages/docs-typecheck/src/type-checker.ts
index c0e23dce11..0d26084886 100644
--- a/packages/docs-typecheck/src/type-checker.ts
+++ b/packages/docs-typecheck/src/type-checker.ts
@@ -70,17 +70,22 @@ const compilerOptions: ts.CompilerOptions = {
// have "require" conditions that TS picks up incorrectly with Bundler resolution.
workflow: [path.join(repoRoot, 'packages/workflow/dist/index')],
'workflow/api': [path.join(repoRoot, 'packages/workflow/dist/api')],
+ 'workflow/runtime': [path.join(repoRoot, 'packages/workflow/dist/runtime')],
'workflow/errors': [
path.join(repoRoot, 'packages/workflow/dist/internal/errors'),
],
'workflow/observability': [
path.join(repoRoot, 'packages/workflow/dist/observability'),
],
+ 'workflow/next': [path.join(repoRoot, 'packages/workflow/dist/next.d.cts')],
'@workflow/core': [path.join(repoRoot, 'packages/core/dist/index')],
'@workflow/core/serialization-format': [
path.join(repoRoot, 'packages/core/dist/serialization-format'),
],
'@workflow/utils': [path.join(repoRoot, 'packages/utils/dist/index')],
+ '@workflow/utils/parse-name': [
+ path.join(repoRoot, 'packages/utils/dist/parse-name'),
+ ],
'@workflow/ai': [path.join(repoRoot, 'packages/ai/dist/index')],
'@workflow/ai/agent': [
path.join(repoRoot, 'packages/ai/dist/agent/durable-agent'),