diff --git a/docs/content/docs/api-reference/workflow-ai/index.mdx b/docs/content/docs/api-reference/workflow-ai/index.mdx index 606fa508db..38d0f150ea 100644 --- a/docs/content/docs/api-reference/workflow-ai/index.mdx +++ b/docs/content/docs/api-reference/workflow-ai/index.mdx @@ -14,13 +14,35 @@ The `@workflow/ai` package is currently in active development and should be cons 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..17a180b40b 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 @@ -7,7 +7,7 @@ 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 the associated workflow run information 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 f528b3b739..bb5c93fd21 100644 --- a/docs/content/docs/api-reference/workflow-api/index.mdx +++ b/docs/content/docs/api-reference/workflow-api/index.mdx @@ -1,33 +1,33 @@ --- 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. - Get direct access to workflow storage, queuing, and streaming backends. + Get direct access to workflow storage, queuing, and streaming backends from `workflow/runtime`. 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..7ab1dd3ebd 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. @@ -24,12 +26,41 @@ 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 (error instanceof Error && error.name === "HookNotFoundError") { + 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 } + ); } } ``` @@ -56,6 +87,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 @@ -69,14 +106,17 @@ 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 (error instanceof Error && error.name === "HookNotFoundError") { + return Response.json({ error: "Hook not found" }, { status: 404 }); + } + return Response.json({ error: "Failed to resume hook" }, { status: 500 }); } } ``` @@ -97,14 +137,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 (error instanceof Error && error.name === "HookNotFoundError") { + return Response.json({ error: "Hook not found" }, { status: 404 }); + } + return Response.json({ error: "Failed to resume hook" }, { status: 500 }); } } ``` @@ -120,37 +163,37 @@ import { resumeHook } from "workflow/api"; 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"; -// 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 (error instanceof Error && error.name === "HookNotFoundError") { + 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-next/with-workflow.mdx b/docs/content/docs/api-reference/workflow-next/with-workflow.mdx index d42adc333a..b6a611c0ec 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,46 @@ 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`). | +| `workflows.local.dataDir` | `string` | Currently typed but ignored by `withWorkflow()`. In local mode, when `WORKFLOW_TARGET_WORLD` is unset, the implementation hardcodes `WORKFLOW_LOCAL_DATA_DIR` to `'.next/workflow-data'`. | + +### 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 +61,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 a5355e018e..40ac28147a 100644 --- a/docs/content/docs/api-reference/workflow/create-webhook.mdx +++ b/docs/content/docs/api-reference/workflow/create-webhook.mdx @@ -13,6 +13,15 @@ Creates a webhook that can be used to suspend and resume a workflow run upon rec Webhooks provide a way for external systems to send HTTP requests directly to your workflow. Unlike hooks which accept arbitrary payloads, webhooks work with standard HTTP `Request` objects and can return HTTP `Response` objects. + +`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" @@ -50,10 +59,10 @@ showSections={['returns']} The returned `Webhook` object has: - `url`: The HTTP endpoint URL that external systems can call -- `token`: The unique token identifying this webhook -- Implements `AsyncIterable` for handling multiple requests +- `token`: The unique, randomly generated token identifying this webhook +- Implements `AsyncIterable` for handling multiple requests, where `T` is `Request` (default) or `RequestWithResponse` (manual mode) -The `RequestWithResponse` type extends the standard `Request` interface with a `respondWith(response: Response)` method for sending custom responses back to the caller. +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. ## Examples @@ -80,9 +89,9 @@ export async function basicWebhookWorkflow() { } ``` -### Responding to Webhook Requests +### Responding to Webhook Requests (Manual Mode) -Use the `respondWith()` method to send custom HTTP responses. Note that `respondWith()` must be called from within a step function: +Pass `{ respondWith: 'manual' }` to get a `RequestWithResponse` object with a `respondWith()` method. Note that `respondWith()` must be called from within a step function: ```typescript lineNumbers import { createWebhook, type RequestWithResponse } from "workflow" @@ -100,7 +109,7 @@ async function sendResponse(request: RequestWithResponse) { // [!code highlight] export async function respondingWebhookWorkflow() { "use workflow"; - using webhook = createWebhook(); + 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..ca6b78e891 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,21 @@ showSections={['parameters']} { +interface TypedHook { /** -* Creates a new hook with the defined payload type. +* Creates a new hook with the defined output type. */ - create: (options?: HookOptions) => Hook; + create: (options?: HookOptions) => Hook; /** -* Resumes a hook by sending a payload with the defined type. +* Resumes a hook by sending a payload with the defined input type. +* Throws an error if the hook is not found or if schema validation fails. */ - resume: (token: string, payload: T) => Promise; + resume: (token: string, payload: TInput) => Promise; } -export default DefineHook;`} +export default TypedHook;`} /> ## Examples @@ -93,24 +100,24 @@ 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 // 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] + 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] - if (!result) { - return Response.json({ error: "Hook not found" }, { status: 404 }); + return Response.json({ success: true, runId: result.runId }); + } catch (error) { + return Response.json({ error: "Hook not found or invalid payload" }, { status: 400 }); } - - 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..f8167a3b90 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,28 @@ async function fallibleStep() { ## API Signature -### Parameters +### Constructor - +| Parameter | Type | Description | +| --------- | -------- | ----------------- | +| `message` | `string` | The error message | + +### Instance Properties + +| Property | Type | Description | +| -------- | ------ | ------------------------------------------------ | +| `fatal` | `boolean` | Always initialized to `true`. Marks the error as non-retryable. | + +### Static Methods + +#### `FatalError.is(value)` + +```typescript +FatalError.is(value: unknown): value is FatalError +``` + +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 @@ -42,6 +43,15 @@ 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. | + - - 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 DevKit 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..a79bcb383f 100644 --- a/docs/content/docs/api-reference/workflow/retryable-error.mdx +++ b/docs/content/docs/api-reference/workflow/retryable-error.mdx @@ -33,26 +33,38 @@ The difference between `Error` and `RetryableError` may not be entirely obvious, ## API Signature -### Parameters - - +| Property | Type | Description | +| ------------ | ------ | --------------------------------------------------------------------------- | +| `retryAfter` | `Date` | The date/time when the step should be retried. Defaults to 1 second from now when `options.retryAfter` is omitted. | -#### RetryableErrorOptions +### `RetryableErrorOptions` + +| Property | Type | Description | +| ------------- | --------------------------------- | ------------------------------------------------------------------------------------------------------- | +| `retryAfter?` | `number \| StringValue \| Date` | Delay before retrying. A `number` is milliseconds, a `StringValue` is a duration string (e.g. `"5s"`, `"2m"`), or a `Date` for an absolute time. Defaults to 1 second (1000 ms) when omitted. | + +### Static Methods + +#### `RetryableError.is(value)` + +```typescript +RetryableError.is(value: unknown): value is RetryableError +``` - +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/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..4776caf3ee --- /dev/null +++ b/packages/docs-typecheck/src/__tests__/hook-runtime-docs.test.ts @@ -0,0 +1,53 @@ +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 source = read('packages/core/src/runtime/resume-hook.ts'); + 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(source).toContain('hook.metadata = await hydrateStepArguments'); + expect(source).toContain('tokenOrHook: string | Hook'); + + 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 apiEntry = read('packages/workflow/src/api.ts'); + const getWorldDoc = read( + 'docs/content/docs/api-reference/workflow-api/get-world.mdx' + ); + + expect(apiEntry).not.toContain('getWorld'); + 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.' + ); + }); +});