UNPKG

workflow

Version:

Workflow SDK - Build durable, resilient, and observable workflows

507 lines (379 loc) • 19.6 kB
--- title: Storage description: Query workflow runs, steps, hooks, and the underlying event log via the World storage interface. type: reference summary: "Interfaces: world.events, world.runs, world.steps, world.hooks. Events are the source of truth; runs, steps, and hooks are materialized views." prerequisites: - /docs/api-reference/workflow-runtime/get-world related: - /docs/api-reference/workflow-api/get-run - /docs/how-it-works/event-sourcing - /docs/api-reference/workflow-observability keywords: - world.events - world.runs - world.steps - world.hooks - event log - event sourcing - materialized views - WorkflowRun - Step - Hook - Event - cursor pagination - resolveData - skip-step-inputs - run_cancelled - correlation ID - parseStepName - parseWorkflowName --- The World storage interface exposes four sub-interfaces for querying workflow data: - **`world.events`**: The append-only event log. This is the source of truth for all workflow state. See [Event Sourcing](/docs/how-it-works/event-sourcing) for background. - **`world.runs`**, **`world.steps`**, **`world.hooks`**: Materialized views derived from the event log, provided as convenience accessors for the most common query patterns. ```typescript lineNumbers import { getWorld } from "workflow/runtime"; const world = await getWorld(); // [!code highlight] ``` --- ## world.events The event log drives all workflow state. Use it for audit trails, debugging, and programmatic run cancellation. ### events.create() Create a new event for a workflow run. Most commonly used to cancel a run. ```typescript lineNumbers await world.events.create(runId, { // [!code highlight] eventType: "run_cancelled", // [!code highlight] }); // [!code highlight] ``` | Parameter | Type | Description | |-----------|------|-------------| | `runId` | `string \| null` | The workflow run ID (`null` only for `run_created` events, where the server generates an ID) | | `data` | `CreateEventRequest` | Event data including `eventType` | | `params` | `object` | Optional parameters | **Returns:** `EventResult`, the created event and the affected entity (run/step/hook) ### events.get() Retrieve a single event by run ID and event ID. ```typescript lineNumbers const event = await world.events.get(runId, eventId); // [!code highlight] ``` | Parameter | Type | Description | |-----------|------|-------------| | `runId` | `string` | The workflow run ID | | `eventId` | `string` | The event ID | **Returns:** `Event` ### events.list() List events for a run. Omit `pagination.limit` to return every remaining event, or set it to return one bounded page. ```typescript lineNumbers const result = await world.events.list({ runId, pagination: { cursor } }); // [!code highlight] ``` | Parameter | Type | Description | |-----------|------|-------------| | `params.runId` | `string` | Filter events by run ID | | `params.pagination.cursor` | `string` | Cursor for the next page | | `params.pagination.limit` | `number` | Maximum events to return. When omitted, returns every remaining event up to the World's event ceiling. | | `params.pagination.sortOrder` | `"asc" \| "desc"` | Event order | | `params.resolveData` | `"all" \| "none" \| "skip-step-inputs"` | Include or omit event payload data. `"skip-step-inputs"` is `"all"` without the `input` of `step_created` and `step_started` events, which replay does not read. | **Returns:** `{ data: Event[], cursor: string | null, hasMore: boolean }` ### events.listByCorrelationId() List one run's events that share a correlation ID, useful for tracing a single step, hook or wait through its lifecycle. A correlation ID is unique within its run, not across runs: two runs can each hold a `step_…`, `hook_…` or `wait_…` ID that reads the same. `runId` is therefore required, and it is also what makes the pagination cursor unambiguous. ```typescript lineNumbers const result = await world.events.listByCorrelationId({ // [!code highlight] runId, correlationId: "order-123", }); // [!code highlight] ``` | Parameter | Type | Description | |-----------|------|-------------| | `params.runId` | `string` | The run the correlation ID belongs to | | `params.correlationId` | `string` | The correlation ID to filter by | | `params.pagination.cursor` | `string` | Cursor for the next page | **Returns:** `{ data: Event[], cursor?: string }` ### Event types | Category | Types | |----------|-------| | Run | `run_created`, `run_started`, `run_completed`, `run_failed`, `run_cancelled` | | Attribute | `attr_set` | | Step | `step_created`, `step_started`, `step_completed`, `step_failed`, `step_retrying` | | Hook | `hook_created`, `hook_received`, `hook_disposed`, `hook_conflict` | | Wait | `wait_created`, `wait_completed` | --- ## world.runs Materialized from run events. Use it for canonical operational reads, including reads that require workflow input or output data. For observability dashboards, inspection tools, and historical listings, use [`world.analytics.runs.list()`](/docs/api-reference/workflow-runtime/world/analytics#runslist). ### runs.get() ```typescript lineNumbers const run = await world.runs.get(runId); // [!code highlight] ``` | Parameter | Type | Description | |-----------|------|-------------| | `runId` | `string` | The workflow run ID | | `params.resolveData` | `'all' \| 'none'` | Whether to include input/output data. Default: `'all'` | **Returns:** `WorkflowRun` (or `WorkflowRunWithoutData` when `resolveData: 'none'`) ### runs.waitForTerminalStatus() Optional. Long poll for a run to reach a terminal status (`completed`, `failed`, or `cancelled`) instead of re-reading it on an interval. This is what `await run.returnValue` uses, so a run's result reaches the awaiting side as soon as it finishes rather than at the next poll tick. ```typescript lineNumbers const run = await world.runs.waitForTerminalStatus?.(runId, { // [!code highlight] timeoutMs: 25_000, // [!code highlight] }); // [!code highlight] ``` | Parameter | Type | Description | |-----------|------|-------------| | `runId` | `string` | The workflow run ID | | `params.timeoutMs` | `number` | Upper bound on the wait. The call returns earlier, the moment the run is terminal | | `params.signal` | `AbortSignal` | Abandons the wait | | `params.resolveData` | `'all' \| 'none'` | Whether to include input/output data. Default: `'all'` | **Returns:** the same `WorkflowRun` as `runs.get()`: terminal if the run finished within the budget, otherwise the latest snapshot. An expired budget is a normal return, not an error, and a missing run throws `WorkflowRunNotFoundError` exactly as `runs.get()` does. <Callout> Not every backend can hold a read open, so this method is optional and may also return a non-terminal snapshot before `timeoutMs` is up. Callers pace their own retries (`await run.returnValue` keeps consecutive non-terminal observations at least one `WORKFLOW_RETURN_VALUE_POLL_INTERVAL_MS` apart), and worlds that omit the method are polled on that interval instead. Set `WORKFLOW_RETURN_VALUE_LONG_POLL=0` to force interval polling everywhere. </Callout> ### runs.list() ```typescript lineNumbers const result = await world.runs.list({ // [!code highlight] status: "running", // [!code highlight] pagination: { cursor }, }); // [!code highlight] ``` | Parameter | Type | Description | |-----------|------|-------------| | `params.workflowName` | `string` | Only return runs of this workflow | | `params.status` | `WorkflowRunStatus \| WorkflowRunStatus[]` | Only return runs in this status. Pass an array to match any of the listed statuses; an empty array matches no runs. `@workflow/world-vercel` accepts a single status only and throws `WorkflowWorldError` (`INVALID_ARGUMENT`) for an array | | `params.pagination.cursor` | `string` | Cursor for the next page | | `params.resolveData` | `'all' \| 'none'` | Whether to include input/output data | **Returns:** `{ data: WorkflowRun[], cursor?: string }` The array form of `status` lets you express set filters without restating the status vocabulary. `@workflow/world` exports `TERMINAL_WORKFLOW_RUN_STATUSES` (`['completed', 'failed', 'cancelled']`) for that purpose: ```typescript lineNumbers import { TERMINAL_WORKFLOW_RUN_STATUSES } from "@workflow/world"; // Every run that has not reached a terminal status const inFlight = await world.runs.list({ status: ["pending", "running"], }); // The complement, without hardcoding the list const finished = await world.runs.list({ status: [...TERMINAL_WORKFLOW_RUN_STATUSES], }); ``` <Callout type="warn"> `status: []` matches **no** runs, mirroring SQL `IN ()`. To leave the filter unset, omit the field entirely. Support for the array form is per World. The Local and Postgres Worlds accept it; the Vercel World's `/v2/runs` endpoint takes a single status today and throws a `WorkflowWorldError` (`INVALID_ARGUMENT`) when given an array, rather than silently filtering on something else. </Callout> <Callout type="warn"> Observability and inspection usage of `world.runs.list()` is deprecated. Use [`world.analytics.runs.list()`](/docs/api-reference/workflow-runtime/world/analytics#runslist) for metadata-only, plan-aware queries backed by the observability pipeline. `world.runs.list()` remains supported for operational and payload-bearing reads. </Callout> ### Cancelling runs To cancel a run, create a `run_cancelled` event through `world.events.create()` (see [world.events](#worldevents) above), or use the Workflow CLI or web interface helpers. To cancel a batch in one call, a world may implement the optional `runs.cancelMany({ runIds })`. It returns a summary plus a per-run outcome (`cancelled`, `already_cancelled`, `not_cancellable`, `not_found`, or `failed`). Backends that omit it fall back to per-run cancellation automatically. ### WorkflowRun type | Field | Type | Description | |-------|------|-------------| | `runId` | `string` | Unique run identifier | | `status` | `string` | `'pending'`, `'running'`, `'completed'`, `'failed'`, `'cancelled'` | | `workflowName` | `string` | Machine-readable workflow identifier | | `input` | `any` | Workflow input data (when `resolveData: 'all'`) | | `output` | `any` | Workflow output data (when `resolveData: 'all'`) | | `error` | `any` | Error data if the run failed | | `startedAt` | `string` | ISO timestamp when the run started | | `completedAt` | `string \| null` | ISO timestamp when the run completed | <Callout type="warn"> `workflowName` is a machine-readable identifier like `workflow//./src/workflows/order//processOrder`. Use `parseWorkflowName()` from `workflow/observability` to extract a display-friendly name. </Callout> --- ## world.steps Materialized from step events. Use it to list steps, inspect their input/output, and build progress dashboards. ### steps.get() ```typescript lineNumbers const step = await world.steps.get(runId, stepId); // [!code highlight] ``` | Parameter | Type | Description | |-----------|------|-------------| | `runId` | `string` | The workflow run ID that owns the step | | `stepId` | `string` | The step ID | | `params.resolveData` | `'all' \| 'none'` | Whether to include input/output data. Default: `'all'` | **Returns:** `Step` (or `StepWithoutData` when `resolveData: 'none'`) ### steps.list() ```typescript lineNumbers const result = await world.steps.list({ // [!code highlight] runId, pagination: { cursor }, }); // [!code highlight] ``` | Parameter | Type | Description | |-----------|------|-------------| | `params.runId` | `string` | Filter steps by run ID | | `params.pagination.cursor` | `string` | Cursor for the next page | | `params.resolveData` | `'all' \| 'none'` | Whether to include input/output data | **Returns:** `{ data: Step[], cursor?: string }` ### Step type | Field | Type | Description | |-------|------|-------------| | `runId` | `string` | Parent workflow run ID | | `stepId` | `string` | Unique step identifier | | `stepName` | `string` | Machine-readable step identifier | | `status` | `string` | `'running'`, `'completed'`, `'failed'` | | `input` | `any` | Step input data (when `resolveData: 'all'`) | | `output` | `any` | Step output data (when `resolveData: 'all'`) | | `error` | `any` | Error data if the step failed | | `attempt` | `number` | Current retry attempt number | | `startedAt` | `string` | ISO timestamp when the step started | | `completedAt` | `string \| null` | ISO timestamp when the step completed | | `retryAfter` | `string \| null` | ISO timestamp for next retry attempt | <Callout type="info"> Step input/output (I/O) is serialized using the [devalue](https://github.com/Rich-Harris/devalue) format. Use `hydrateResourceIO()` from `workflow/observability` to deserialize it for display. See [`hydrateResourceIO()`](/docs/api-reference/workflow-observability/hydrate-resource-io). </Callout> <Callout type="warn"> `stepName` is a machine-readable identifier like `step//./src/workflows/order//processPayment`. Use `parseStepName()` from `workflow/observability` to extract the `shortName` for display in a user interface. </Callout> --- ## world.hooks Materialized from hook events. Hooks are pause points in workflows that wait for external input. Use this interface to look up hooks by ID or token, inspect metadata, and build UIs for pending approvals. ### hooks.get() ```typescript lineNumbers const hook = await world.hooks.get(hookId); // [!code highlight] ``` | Parameter | Type | Description | |-----------|------|-------------| | `hookId` | `string` | The hook ID | **Returns:** `Hook` ### hooks.getByToken() Look up a hook by its token. Useful in webhook resume flows where you receive a token in the callback URL. <Callout type="info"> For runtime application code, prefer [`getHookByToken()`](/docs/api-reference/workflow-api/get-hook-by-token). Use `world.hooks.getByToken()` when you are working directly with the World storage interface for custom tooling, admin views, or low-level integrations. Hook-token lookup is the low-level form of the recommended idempotency flow: if a hook is already registered for your business key, reuse the hook's `runId` or resume that hook instead of starting another run. If no hook exists yet, start a workflow that creates the deterministic hook near the beginning and checks `await hook.getConflict()` to detect whether another run claimed the token first. On a conflict it resolves with the run that owns the token. See [Run idempotency](/docs/foundations/idempotency#run-idempotency). </Callout> ```typescript lineNumbers const hook = await world.hooks.getByToken(token); // [!code highlight] ``` | Parameter | Type | Description | |-----------|------|-------------| | `token` | `string` | The hook token | **Returns:** `Hook` ### hooks.list() ```typescript lineNumbers const result = await world.hooks.list({ // [!code highlight] pagination: { cursor }, }); // [!code highlight] ``` | Parameter | Type | Description | |-----------|------|-------------| | `params.pagination.cursor` | `string` | Cursor for the next page | **Returns:** `{ data: Hook[], cursor?: string }` ### Hook type | Field | Type | Description | |-------|------|-------------| | `runId` | `string` | Parent workflow run ID | | `hookId` | `string` | Unique hook identifier | | `token` | `string` | Hook token for resuming | | `ownerId` | `string` | Owner (team/user) ID | | `projectId` | `string` | Project ID | | `environment` | `string` | Deployment environment | | `metadata` | `object` | Custom metadata attached to the hook | | `isWebhook` | `boolean` | Whether this is a webhook-style hook | | `claimedFrom` | `{ runId: string; hookId: string } \| undefined` | Set when this hook took its token from another run with [`experimental_force`](/docs/api-reference/workflow/create-hook#take-over-a-token-another-run-holds). Names that run and its hook | --- ## Examples ### List runs with pagination ```typescript lineNumbers import { getWorld } from "workflow/runtime"; const world = await getWorld(); let cursor: string | undefined; const runs = await world.runs.list({ // [!code highlight] pagination: { cursor }, }); // [!code highlight] cursor = runs.cursor; // pass to next call for pagination ``` ### Get a run: full data vs. metadata only ```typescript lineNumbers import { getWorld } from "workflow/runtime"; const world = await getWorld(); // Full data (default): includes serialized input/output const run = await world.runs.get(runId); // [!code highlight] // Metadata only: lighter, no I/O loaded const lightweight = await world.runs.get(runId, { // [!code highlight] resolveData: "none", // [!code highlight] }); // [!code highlight] ``` ### List steps for a progress dashboard ```typescript lineNumbers import { getWorld } from "workflow/runtime"; import { parseStepName } from "workflow/observability"; // [!code highlight] const world = await getWorld(); const steps = await world.steps.list({ // [!code highlight] runId, resolveData: "none", }); // [!code highlight] const progress = steps.data.map((step) => { const parsed = parseStepName(step.stepName); // [!code highlight] return { stepId: step.stepId, displayName: parsed?.shortName ?? step.stepName, // [!code highlight] status: step.status, }; }); ``` ### Hydrate step I/O ```typescript lineNumbers import { getWorld } from "workflow/runtime"; import { hydrateResourceIO, observabilityRevivers } from "workflow/observability"; // [!code highlight] const world = await getWorld(); const step = await world.steps.get(runId, stepId); // [!code highlight] const hydrated = hydrateResourceIO(step, observabilityRevivers); // [!code highlight] console.log(hydrated.input, hydrated.output); ``` ### Cancel a run ```typescript lineNumbers import { getWorld } from "workflow/runtime"; const world = await getWorld(); await world.events.create(runId, { // [!code highlight] eventType: "run_cancelled", // [!code highlight] }); // [!code highlight] ``` ### Look up hook by token ```typescript lineNumbers import { getWorld } from "workflow/runtime"; const world = await getWorld(); const hook = await world.hooks.getByToken(token); // [!code highlight] console.log(hook.runId); // [!code highlight] ``` The World-level `Hook` carries `metadata` as the raw serialized (and, on encrypting Worlds, encrypted) value, not the object the workflow passed to `createHook()`. To read the decoded value, use [`getHookByToken()`](/docs/api-reference/workflow-api/get-hook-by-token), whose `hook.metadata` is a Promise that hydrates it on first access. ### List events for audit trail ```typescript lineNumbers import { getWorld } from "workflow/runtime"; const world = await getWorld(); const events = await world.events.list({ runId }); // [!code highlight] for (const event of events.data) { console.log(event.eventType, event.createdAt); } ``` ## Related - [Event sourcing](/docs/how-it-works/event-sourcing): How the event log powers workflow replay and state - [`getRun()`](/docs/api-reference/workflow-api/get-run): Higher-level API for working with individual runs - [`workflow/observability`](/docs/api-reference/workflow-observability): Hydrate step I/O and parse display names - [`resumeHook()`](/docs/api-reference/workflow-api/resume-hook): Resume a workflow by sending a payload to a hook - [Hooks](/docs/foundations/hooks): Core concepts for hooks and pause points - [Workflows and steps](/docs/foundations/workflows-and-steps): Core concepts for steps