workflow 4.2.0-beta.71 → 4.2.0-beta.72

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/dist/api.d.ts +1 -1
  2. package/dist/api.d.ts.map +1 -1
  3. package/dist/api.js +1 -1
  4. package/dist/internal/errors.d.ts +1 -1
  5. package/dist/internal/errors.d.ts.map +1 -1
  6. package/dist/internal/errors.js +2 -2
  7. package/dist/observability.d.ts +20 -0
  8. package/dist/observability.d.ts.map +1 -0
  9. package/dist/observability.js +20 -0
  10. package/docs/ai/defining-tools.mdx +6 -0
  11. package/docs/ai/index.mdx +3 -0
  12. package/docs/ai/message-queueing.mdx +2 -0
  13. package/docs/ai/resumable-streams.mdx +37 -4
  14. package/docs/ai/sleep-and-delays.mdx +2 -0
  15. package/docs/api-reference/meta.json +1 -1
  16. package/docs/api-reference/workflow/define-hook.mdx +2 -0
  17. package/docs/api-reference/workflow/get-writable.mdx +1 -0
  18. package/docs/api-reference/workflow-ai/durable-agent.mdx +2 -0
  19. package/docs/api-reference/workflow-ai/workflow-chat-transport.mdx +2 -0
  20. package/docs/api-reference/workflow-api/get-run.mdx +14 -0
  21. package/docs/api-reference/workflow-api/get-world.mdx +105 -0
  22. package/docs/api-reference/workflow-errors/entity-conflict-error.mdx +60 -0
  23. package/docs/api-reference/workflow-errors/hook-not-found-error.mdx +90 -0
  24. package/docs/api-reference/workflow-errors/meta.json +16 -0
  25. package/docs/api-reference/workflow-errors/run-expired-error.mdx +58 -0
  26. package/docs/api-reference/workflow-errors/step-not-registered-error.mdx +56 -0
  27. package/docs/api-reference/workflow-errors/throttle-error.mdx +62 -0
  28. package/docs/api-reference/workflow-errors/too-early-error.mdx +62 -0
  29. package/docs/api-reference/workflow-errors/workflow-not-registered-error.mdx +57 -0
  30. package/docs/api-reference/workflow-errors/workflow-run-cancelled-error.mdx +56 -0
  31. package/docs/api-reference/workflow-errors/workflow-run-failed-error.mdx +62 -0
  32. package/docs/api-reference/workflow-errors/workflow-run-not-found-error.mdx +56 -0
  33. package/docs/api-reference/workflow-errors/workflow-world-error.mdx +79 -0
  34. package/docs/changelog/index.mdx +15 -0
  35. package/docs/changelog/meta.json +5 -0
  36. package/docs/deploying/building-a-world.mdx +20 -0
  37. package/docs/errors/hook-conflict.mdx +9 -3
  38. package/docs/errors/index.mdx +6 -0
  39. package/docs/errors/step-not-registered.mdx +66 -0
  40. package/docs/errors/webhook-invalid-respond-with-value.mdx +10 -0
  41. package/docs/errors/webhook-response-not-sent.mdx +8 -0
  42. package/docs/errors/workflow-not-registered.mdx +64 -0
  43. package/docs/foundations/errors-and-retries.mdx +29 -0
  44. package/docs/foundations/streaming.mdx +22 -0
  45. package/docs/getting-started/index.mdx +3 -3
  46. package/docs/getting-started/meta.json +15 -0
  47. package/docs/getting-started/nestjs.mdx +4 -8
  48. package/package.json +18 -12
package/dist/api.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export { type Event, getHookByToken, getRun, Run, resumeHook, resumeWebhook, runStep, type StartOptions, start, type StopSleepOptions, type StopSleepResult, type WorkflowReadableStreamOptions, type WorkflowRun, } from '@workflow/core/runtime';
1
+ export { type Event, getHookByToken, getRun, Run, resumeHook, resumeWebhook, runStep, type StartOptions, start, type StopSleepOptions, type StopSleepResult, type WorkflowReadableStream, type WorkflowReadableStreamOptions, type WorkflowRun, } from '@workflow/core/runtime';
2
2
  //# sourceMappingURL=api.d.ts.map
package/dist/api.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,KAAK,EACV,cAAc,EACd,MAAM,EACN,GAAG,EACH,UAAU,EACV,aAAa,EACb,OAAO,EACP,KAAK,YAAY,EACjB,KAAK,EACL,KAAK,gBAAgB,EACrB,KAAK,eAAe,EACpB,KAAK,6BAA6B,EAClC,KAAK,WAAW,GACjB,MAAM,wBAAwB,CAAC"}
1
+ {"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,KAAK,EACV,cAAc,EACd,MAAM,EACN,GAAG,EACH,UAAU,EACV,aAAa,EACb,OAAO,EACP,KAAK,YAAY,EACjB,KAAK,EACL,KAAK,gBAAgB,EACrB,KAAK,eAAe,EACpB,KAAK,sBAAsB,EAC3B,KAAK,6BAA6B,EAClC,KAAK,WAAW,GACjB,MAAM,wBAAwB,CAAC"}
package/dist/api.js CHANGED
@@ -1,2 +1,2 @@
1
1
  export { getHookByToken, getRun, Run, resumeHook, resumeWebhook, runStep, start, } from '@workflow/core/runtime';
2
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYXBpLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vc3JjL2FwaS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEVBRUwsY0FBYyxFQUNkLE1BQU0sRUFDTixHQUFHLEVBQ0gsVUFBVSxFQUNWLGFBQWEsRUFDYixPQUFPLEVBRVAsS0FBSyxHQUtOLE1BQU0sd0JBQXdCLENBQUMifQ==
2
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYXBpLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vc3JjL2FwaS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEVBRUwsY0FBYyxFQUNkLE1BQU0sRUFDTixHQUFHLEVBQ0gsVUFBVSxFQUNWLGFBQWEsRUFDYixPQUFPLEVBRVAsS0FBSyxHQU1OLE1BQU0sd0JBQXdCLENBQUMifQ==
@@ -1,2 +1,2 @@
1
- export { WorkflowAPIError, WorkflowError, WorkflowRunCancelledError, WorkflowRunFailedError, WorkflowRunNotCompletedError, WorkflowRunNotFoundError, WorkflowRuntimeError, } from '@workflow/errors';
1
+ export { EntityConflictError, HookConflictError, HookNotFoundError, RunExpiredError, RunNotSupportedError, StepNotRegisteredError, ThrottleError, TooEarlyError, WorkflowError, WorkflowNotRegisteredError, WorkflowRunCancelledError, WorkflowRunFailedError, WorkflowRunNotCompletedError, WorkflowRunNotFoundError, WorkflowRuntimeError, WorkflowWorldError, } from '@workflow/errors';
2
2
  //# sourceMappingURL=errors.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/internal/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,gBAAgB,EAChB,aAAa,EACb,yBAAyB,EACzB,sBAAsB,EACtB,4BAA4B,EAC5B,wBAAwB,EACxB,oBAAoB,GACrB,MAAM,kBAAkB,CAAC"}
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/internal/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,mBAAmB,EACnB,iBAAiB,EACjB,iBAAiB,EACjB,eAAe,EACf,oBAAoB,EACpB,sBAAsB,EACtB,aAAa,EACb,aAAa,EACb,aAAa,EACb,0BAA0B,EAC1B,yBAAyB,EACzB,sBAAsB,EACtB,4BAA4B,EAC5B,wBAAwB,EACxB,oBAAoB,EACpB,kBAAkB,GACnB,MAAM,kBAAkB,CAAC"}
@@ -1,2 +1,2 @@
1
- export { WorkflowAPIError, WorkflowError, WorkflowRunCancelledError, WorkflowRunFailedError, WorkflowRunNotCompletedError, WorkflowRunNotFoundError, WorkflowRuntimeError, } from '@workflow/errors';
2
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZXJyb3JzLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL2ludGVybmFsL2Vycm9ycy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEVBQ0wsZ0JBQWdCLEVBQ2hCLGFBQWEsRUFDYix5QkFBeUIsRUFDekIsc0JBQXNCLEVBQ3RCLDRCQUE0QixFQUM1Qix3QkFBd0IsRUFDeEIsb0JBQW9CLEdBQ3JCLE1BQU0sa0JBQWtCLENBQUMifQ==
1
+ export { EntityConflictError, HookConflictError, HookNotFoundError, RunExpiredError, RunNotSupportedError, StepNotRegisteredError, ThrottleError, TooEarlyError, WorkflowError, WorkflowNotRegisteredError, WorkflowRunCancelledError, WorkflowRunFailedError, WorkflowRunNotCompletedError, WorkflowRunNotFoundError, WorkflowRuntimeError, WorkflowWorldError, } from '@workflow/errors';
2
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZXJyb3JzLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL2ludGVybmFsL2Vycm9ycy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEVBQ0wsbUJBQW1CLEVBQ25CLGlCQUFpQixFQUNqQixpQkFBaUIsRUFDakIsZUFBZSxFQUNmLG9CQUFvQixFQUNwQixzQkFBc0IsRUFDdEIsYUFBYSxFQUNiLGFBQWEsRUFDYixhQUFhLEVBQ2IsMEJBQTBCLEVBQzFCLHlCQUF5QixFQUN6QixzQkFBc0IsRUFDdEIsNEJBQTRCLEVBQzVCLHdCQUF3QixFQUN4QixvQkFBb0IsRUFDcEIsa0JBQWtCLEdBQ25CLE1BQU0sa0JBQWtCLENBQUMifQ==
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Observability utilities for hydrating serialized workflow data.
3
+ *
4
+ * Use these when inspecting workflow step I/O, run inputs/outputs,
5
+ * or event data from the Workflow SDK's world APIs.
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * import { getWorld } from 'workflow/api';
10
+ * import { hydrateResourceIO, observabilityRevivers } from 'workflow/observability';
11
+ *
12
+ * const world = getWorld();
13
+ * const step = await world.steps.get(runId, stepId, { resolveData: 'all' });
14
+ * const hydrated = hydrateResourceIO(step, observabilityRevivers);
15
+ * // hydrated.input and hydrated.output are now plain JS objects
16
+ * ```
17
+ */
18
+ export { hydrateData, hydrateResourceIO, observabilityRevivers, type Revivers, } from '@workflow/core/serialization-format';
19
+ export { parseClassName, parseStepName, parseWorkflowName, } from '@workflow/utils';
20
+ //# sourceMappingURL=observability.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"observability.d.ts","sourceRoot":"","sources":["../src/observability.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,EACL,WAAW,EACX,iBAAiB,EACjB,qBAAqB,EACrB,KAAK,QAAQ,GACd,MAAM,qCAAqC,CAAC;AAE7C,OAAO,EACL,cAAc,EACd,aAAa,EACb,iBAAiB,GAClB,MAAM,iBAAiB,CAAC"}
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Observability utilities for hydrating serialized workflow data.
3
+ *
4
+ * Use these when inspecting workflow step I/O, run inputs/outputs,
5
+ * or event data from the Workflow SDK's world APIs.
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * import { getWorld } from 'workflow/api';
10
+ * import { hydrateResourceIO, observabilityRevivers } from 'workflow/observability';
11
+ *
12
+ * const world = getWorld();
13
+ * const step = await world.steps.get(runId, stepId, { resolveData: 'all' });
14
+ * const hydrated = hydrateResourceIO(step, observabilityRevivers);
15
+ * // hydrated.input and hydrated.output are now plain JS objects
16
+ * ```
17
+ */
18
+ export { hydrateData, hydrateResourceIO, observabilityRevivers, } from '@workflow/core/serialization-format';
19
+ export { parseClassName, parseStepName, parseWorkflowName, } from '@workflow/utils';
20
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoib2JzZXJ2YWJpbGl0eS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3NyYy9vYnNlcnZhYmlsaXR5LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBOzs7Ozs7Ozs7Ozs7Ozs7O0dBZ0JHO0FBQ0gsT0FBTyxFQUNMLFdBQVcsRUFDWCxpQkFBaUIsRUFDakIscUJBQXFCLEdBRXRCLE1BQU0scUNBQXFDLENBQUM7QUFFN0MsT0FBTyxFQUNMLGNBQWMsRUFDZCxhQUFhLEVBQ2IsaUJBQWlCLEdBQ2xCLE1BQU0saUJBQWlCLENBQUMifQ==
@@ -23,6 +23,9 @@ Just like in regular AI SDK tool definitions, tool in DurableAgent are called wi
23
23
  When you tool needs access to the full message history, you can access it via the `messages` property of the tool call context:
24
24
 
25
25
  ```typescript title="tools.ts" lineNumbers
26
+ import { Experimental_Agent as Agent } from "ai";
27
+ import type { ModelMessage } from "ai";
28
+
26
29
  async function getWeather(
27
30
  { city }: { city: string },
28
31
  { messages, toolCallId }: { messages: ModelMessage[], toolCallId: string }) { // [!code highlight]
@@ -65,6 +68,9 @@ Tools can be implemented either at the step level or the workflow level, with di
65
68
  Tools can also combine both by starting out on the workflow level, and calling into steps for I/O operations, like so:
66
69
 
67
70
  ```typescript title="tools.ts" lineNumbers
71
+ import { sleep } from "workflow";
72
+ import type { LanguageModel, ModelMessage } from "ai";
73
+
68
74
  // Step: handles I/O with retries
69
75
  async function performFetch(url: string) {
70
76
  "use step";
package/docs/ai/index.mdx CHANGED
@@ -123,6 +123,9 @@ The core code that makes all of this happen is quite simple. Here's a breakdown
123
123
  Our API route makes a simple call to [AI SDK's `Agent` class](https://ai-sdk.dev/docs/agents/overview), which is a simple wrapper around [AI SDK's `streamText` function](https://ai-sdk.dev/docs/reference/ai-sdk-core/stream-text#streamtext). This is also where we pass tools to the agent.
124
124
 
125
125
  ```typescript title="app/api/chat/route.ts" lineNumbers
126
+ import { Experimental_Agent as Agent } from "ai";
127
+ import type { LanguageModel } from "ai";
128
+
126
129
  export async function POST(req: Request) {
127
130
  const { messages }: { messages: UIMessage[] } = await req.json();
128
131
  const agent = new Agent({ // [!code highlight]
@@ -32,6 +32,8 @@ If you just need basic multi-turn conversations where messages arrive between tu
32
32
  The `prepareStep` callback runs before each step in the agent loop. It receives the current state and can modify the messages sent to the model:
33
33
 
34
34
  ```typescript lineNumbers
35
+ import type { ModelMessage, LanguageModel } from "ai";
36
+
35
37
  interface PrepareStepInfo {
36
38
  model: string | (() => Promise<LanguageModel>); // Current model
37
39
  stepNumber: number; // 0-indexed step count
@@ -30,7 +30,8 @@ Let's add stream resumption to our Flight Booking Agent that we build in the [Bu
30
30
 
31
31
  Modify your chat endpoint to include the workflow run ID in a response header. The Run ID uniquely identifies the run's stream, so it allows the client to know which stream to reconnect to.
32
32
 
33
- {/* @skip-typecheck: incomplete code sample */}
33
+ {/*@skip-typecheck: incomplete code sample*/}
34
+
34
35
  ```typescript title="app/api/chat/route.ts" lineNumbers
35
36
  // ... imports ...
36
37
 
@@ -76,14 +77,24 @@ export async function GET(
76
77
 
77
78
  // Instead of starting a new run, we fetch an existing run.
78
79
  const run = getRun(id); // [!code highlight]
79
- const stream = run.getReadable({ startIndex }); // [!code highlight]
80
+ const readable = run.getReadable({ startIndex }); // [!code highlight]
81
+
82
+ // Provide the stream's tail index so the transport can resolve
83
+ // negative startIndex values into absolute positions for retries.
84
+ const tailIndex = await readable.getTailIndex(); // [!code highlight]
80
85
 
81
- return createUIMessageStreamResponse({ stream }); // [!code highlight]
86
+ return createUIMessageStreamResponse({
87
+ stream: readable, // [!code highlight]
88
+ headers: { // [!code highlight]
89
+ "x-workflow-stream-tail-index": String(tailIndex), // [!code highlight]
90
+ }, // [!code highlight]
91
+ });
82
92
  }
83
93
  ```
84
94
 
85
- The `startIndex` parameter ensures the client can choose where to resume the stream from. For instance, if the function times out during streaming, the chat transport will use `startIndex` to resume the stream exactly from the last token it received.
95
+ The `startIndex` parameter ensures the client can choose where to resume the stream from. For instance, if the function times out during streaming, the chat transport will use `startIndex` to resume the stream exactly from the last token it received. Negative values are also supported (e.g. `-5` starts 5 chunks before the end), which is useful for custom stream consumers (such as a dashboard showing recent output) that want to show the most recent output without replaying the full stream.
86
96
 
97
+ When using a negative `startIndex`, your stream endpoint must return a `x-workflow-stream-tail-index` header in order for relative resumption to work. Missing the header will fall back to replaying the entire stream.
87
98
  </Step>
88
99
 
89
100
  <Step>
@@ -161,6 +172,28 @@ Now try the flight booking example again. Open it up in a separate tab, or spam
161
172
 
162
173
  This approach also handles page refreshes, as the client will automatically reconnect to the stream from the last known position when the UI loads with a stored run ID, following the behavior of [AI SDK's stream resumption](https://ai-sdk.dev/docs/ai-sdk-ui/chatbot-resume-streams#chatbot-resume-streams).
163
174
 
175
+ ### Resuming from the end of the stream
176
+
177
+ By default, reconnecting replays the entire stream from the beginning (`startIndex: 0`). If you only need to show recent output — for example, when resuming a long conversation after a page refresh — you can set `initialStartIndex` to a negative value to read from the end of the stream instead:
178
+
179
+ {/*@skip-typecheck: incomplete code sample*/}
180
+
181
+ ```typescript
182
+ const { messages, sendMessage } = useChat({
183
+ resume: !!activeWorkflowRunId,
184
+ transport: new WorkflowChatTransport({
185
+ initialStartIndex: -20, // Only fetch the last 20 chunks // [!code highlight]
186
+ // ... callbacks as above
187
+ }),
188
+ });
189
+ ```
190
+
191
+ This avoids replaying potentially thousands of chunks and lets the UI render faster. The negative value is resolved server-side, so `-20` on a 500-chunk stream starts at chunk 480.
192
+
193
+ <Callout>
194
+ When using a negative `initialStartIndex`, the reconnection endpoint **must** return the `x-workflow-stream-tail-index` header (as shown in [Step 2](#add-a-stream-reconnection-endpoint) above). The transport uses this header to compute absolute chunk positions so that retries after a disconnect resume from the correct position. If the header is missing, the transport falls back to `startIndex: 0` (replaying the entire stream) and logs a warning.
195
+ </Callout>
196
+
164
197
  ## Related Documentation
165
198
 
166
199
  - [`WorkflowChatTransport` API Reference](/docs/api-reference/workflow-ai/workflow-chat-transport) - Full configuration options
@@ -162,6 +162,8 @@ Aside from providing `sleep()` as a tool, there are other use cases for Agents t
162
162
  When hitting API rate limits, use `RetryableError` with a delay:
163
163
 
164
164
  ```typescript lineNumbers
165
+ import { RetryableError } from "workflow";
166
+
165
167
  async function callRateLimitedAPI(endpoint: string) {
166
168
  "use step";
167
169
 
@@ -1,4 +1,4 @@
1
1
  {
2
2
  "title": "API Reference",
3
- "pages": ["...", "workflow-serde", "workflow-ai", "vitest"]
3
+ "pages": ["...", "workflow-errors", "workflow-serde", "workflow-ai", "vitest"]
4
4
  }
@@ -193,6 +193,8 @@ export const approvalHook = defineHook({
193
193
  Tokens are used to identify a specific hook and for resuming a hook. You can customize the token to be more specific to a use case.
194
194
 
195
195
  ```typescript lineNumbers
196
+ import { defineHook } from "workflow";
197
+
196
198
  const slackHook = defineHook<{ text: string; userId: string }>();
197
199
 
198
200
  export async function slackBotWorkflow(channelId: string) {
@@ -208,6 +208,7 @@ Here's a more complex example showing how you might stream AI chat responses:
208
208
  ```typescript lineNumbers
209
209
  import { getWritable } from "workflow";
210
210
  import { generateId, streamText, type UIMessageChunk } from "ai";
211
+ import type { ModelMessage } from "ai";
211
212
 
212
213
  export async function chat(messages: ModelMessage[]) {
213
214
  "use workflow";
@@ -317,6 +317,8 @@ async function multiToolAgentWorkflow(userQuery: string) {
317
317
 
318
318
  ```typescript
319
319
  import { DurableAgent } from "@workflow/ai/agent";
320
+ import { getWritable } from "workflow";
321
+ import type { UIMessageChunk } from "ai";
320
322
  import { z } from "zod";
321
323
 
322
324
  async function searchProducts({ query }: { query: string }) {
@@ -71,6 +71,8 @@ export default WorkflowChatTransportOptions;`}
71
71
  - The `onChatSendMessage` callback receives the full response object, allowing you to extract and store the workflow run ID for session resumption
72
72
  - Stream interruptions are automatically detected when a "finish" chunk is not received in the initial response
73
73
  - The `maxConsecutiveErrors` option controls how many reconnection attempts are made before giving up (default: 3)
74
+ - `initialStartIndex` (constructor option) sets the default chunk position for the **first** reconnection attempt (e.g. after a page refresh). Subsequent retries within the same reconnection loop always resume from the last received chunk. Negative values (e.g. `-20`) read from the end of the stream, which is useful for showing only recent output without replaying the full conversation. `startIndex` (per-call option on `reconnectToStream`) overrides `initialStartIndex` for a single reconnection
75
+ - When using a negative `initialStartIndex`, the reconnection endpoint must return the `x-workflow-stream-tail-index` response header (via `readable.getTailIndex()`). The transport reads this header to compute absolute chunk positions for retries. Without it, startIndex is assumed to be 0, replaying the entire stream
74
76
 
75
77
  ## Examples
76
78
 
@@ -39,6 +39,18 @@ export default Run;`}
39
39
  showSections={["returns"]}
40
40
  />
41
41
 
42
+ #### WorkflowReadableStream
43
+
44
+ `run.getReadable()` returns a `WorkflowReadableStream` — a standard `ReadableStream` extended with a `getTailIndex()` helper:
45
+
46
+ <TSDoc
47
+ definition={`
48
+ import type { WorkflowReadableStream } from "workflow/api";
49
+ export default WorkflowReadableStream;`}
50
+ />
51
+
52
+ `getTailIndex()` returns the index of the last known chunk (0-based), or `-1` when no chunks have been written. This is useful when building [reconnection endpoints](/docs/ai/resumable-streams) that need to inform clients where the stream starts.
53
+
42
54
  #### WorkflowReadableStreamOptions
43
55
 
44
56
  <TSDoc
@@ -144,6 +156,8 @@ export async function POST(req: Request) {
144
156
  You can also target specific sleep calls by correlation ID:
145
157
 
146
158
  ```typescript lineNumbers
159
+ import { getRun } from "workflow/api";
160
+
147
161
  const run = getRun("my-run-id"); // @setup
148
162
  const { stoppedCount } = await run.wakeUp({
149
163
  correlationIds: ["wait_abc123"],
@@ -91,6 +91,111 @@ export async function POST(req: Request) {
91
91
  }
92
92
  ```
93
93
 
94
+ ### List Steps for a Run (Without Data)
95
+
96
+ List steps for a workflow run with `resolveData: 'none'` to efficiently get step metadata without fetching serialized input/output. Use `parseStepName` to extract user-friendly display names:
97
+
98
+ ```typescript lineNumbers
99
+ import { getWorld } from "workflow/runtime";
100
+ import { parseStepName } from "@workflow/utils/parse-name"; // [!code highlight]
101
+
102
+ export async function GET(req: Request) {
103
+ const url = new URL(req.url);
104
+ const runId = url.searchParams.get("runId");
105
+
106
+ if (!runId) {
107
+ return Response.json({ error: "No runId provided" }, { status: 400 });
108
+ }
109
+
110
+ try {
111
+ const world = getWorld(); // [!code highlight]
112
+ const steps = await world.steps.list({ // [!code highlight]
113
+ runId, // [!code highlight]
114
+ resolveData: "none", // Skip fetching input/output for performance // [!code highlight]
115
+ }); // [!code highlight]
116
+
117
+ // Map steps to a progress view using parseStepName for display
118
+ const progress = steps.data.map((step) => {
119
+ const parsed = parseStepName(step.stepName); // [!code highlight]
120
+ return {
121
+ stepId: step.stepId,
122
+ // Use shortName for UI display (e.g., "fetchUserData") // [!code highlight]
123
+ displayName: parsed?.shortName ?? step.stepName, // [!code highlight]
124
+ // Module info available for debugging // [!code highlight]
125
+ module: parsed?.moduleSpecifier, // [!code highlight]
126
+ status: step.status,
127
+ startedAt: step.startedAt,
128
+ completedAt: step.completedAt,
129
+ };
130
+ });
131
+
132
+ return Response.json({ progress, cursor: steps.cursor });
133
+ } catch (error) {
134
+ return Response.json(
135
+ { error: "Failed to list steps" },
136
+ { status: 500 }
137
+ );
138
+ }
139
+ }
140
+ ```
141
+
142
+ ### Get Step with Hydrated Input/Output
143
+
144
+ Retrieve a step with its serialized data and hydrate it for display. This example shows how to decrypt and deserialize step input/output:
145
+
146
+ ```typescript lineNumbers
147
+ import { getWorld } from "workflow/runtime";
148
+ import { parseStepName } from "@workflow/utils/parse-name"; // [!code highlight]
149
+ import { // [!code highlight]
150
+ hydrateResourceIO, // [!code highlight]
151
+ observabilityRevivers, // [!code highlight]
152
+ } from "@workflow/core/serialization-format"; // [!code highlight]
153
+
154
+ export async function GET(req: Request) {
155
+ const url = new URL(req.url);
156
+ const runId = url.searchParams.get("runId");
157
+ const stepId = url.searchParams.get("stepId");
158
+
159
+ if (!runId || !stepId) {
160
+ return Response.json({ error: "runId and stepId required" }, { status: 400 });
161
+ }
162
+
163
+ try {
164
+ const world = getWorld(); // [!code highlight]
165
+ // Fetch step with data (default resolveData behavior) // [!code highlight]
166
+ const step = await world.steps.get(runId, stepId); // [!code highlight]
167
+
168
+ // Hydrate serialized input/output for display // [!code highlight]
169
+ const hydrated = hydrateResourceIO(step, observabilityRevivers); // [!code highlight]
170
+
171
+ // Parse the stepName for user-friendly display
172
+ const parsed = parseStepName(step.stepName);
173
+
174
+ return Response.json({
175
+ stepId: hydrated.stepId,
176
+ displayName: parsed?.shortName ?? step.stepName, // [!code highlight]
177
+ module: parsed?.moduleSpecifier, // [!code highlight]
178
+ status: hydrated.status,
179
+ attempt: hydrated.attempt,
180
+ // Hydrated input/output ready for rendering // [!code highlight]
181
+ input: hydrated.input, // [!code highlight]
182
+ output: hydrated.output, // [!code highlight]
183
+ });
184
+ } catch (error) {
185
+ return Response.json(
186
+ { error: "Step not found" },
187
+ { status: 404 }
188
+ );
189
+ }
190
+ }
191
+ ```
192
+
193
+ <Callout type="info">
194
+ The `stepName` field contains a machine-readable identifier like `step//./src/workflows/order//processPayment`.
195
+ Use `parseStepName()` from `@workflow/utils/parse-name` to extract the `shortName` (e.g., `"processPayment"`)
196
+ and `moduleSpecifier` for display in your UI.
197
+ </Callout>
198
+
94
199
  ## Related Functions
95
200
 
96
201
  - [`getRun()`](/docs/api-reference/workflow-api/get-run) - Higher-level API for working with individual runs by ID.
@@ -0,0 +1,60 @@
1
+ ---
2
+ title: EntityConflictError
3
+ description: Thrown when a storage operation conflicts with the current entity state.
4
+ type: reference
5
+ summary: Catch EntityConflictError when a world operation conflicts with entity state (e.g. duplicate events or runs).
6
+ related:
7
+ - /docs/api-reference/workflow-errors/workflow-world-error
8
+ - /docs/api-reference/workflow-errors/run-expired-error
9
+ ---
10
+
11
+ `EntityConflictError` is thrown by world implementations when a storage operation conflicts with the current entity state. This includes cases like creating a run that already exists or writing an event that has already been persisted.
12
+
13
+ It corresponds to HTTP 409 Conflict semantics.
14
+
15
+ <Callout>
16
+ The Workflow runtime handles this error automatically during replay and event deduplication. You will only encounter it when interacting with world storage APIs directly.
17
+ </Callout>
18
+
19
+ ```typescript lineNumbers
20
+ import { EntityConflictError } from "workflow/errors"
21
+ declare const world: { events: { create(...args: any[]): Promise<any> } }; // @setup
22
+ declare const runId: string; // @setup
23
+ declare const event: any; // @setup
24
+
25
+ try {
26
+ await world.events.create(runId, event);
27
+ } catch (error) {
28
+ if (EntityConflictError.is(error)) { // [!code highlight]
29
+ // Event already exists — safe to ignore during replay
30
+ }
31
+ }
32
+ ```
33
+
34
+ ## API Signature
35
+
36
+ ### Properties
37
+
38
+ <TSDoc
39
+ definition={`
40
+ interface EntityConflictError {
41
+ /** The error message. */
42
+ message: string;
43
+ }
44
+ export default EntityConflictError;`}
45
+ />
46
+
47
+ ### Static Methods
48
+
49
+ #### `EntityConflictError.is(value)`
50
+
51
+ Type-safe check for `EntityConflictError` instances. Preferred over `instanceof` because it works across module boundaries and VM contexts.
52
+
53
+ ```typescript
54
+ import { EntityConflictError } from "workflow/errors"
55
+ declare const error: unknown; // @setup
56
+
57
+ if (EntityConflictError.is(error)) {
58
+ // error is typed as EntityConflictError
59
+ }
60
+ ```
@@ -0,0 +1,90 @@
1
+ ---
2
+ title: HookNotFoundError
3
+ description: Thrown when resuming a hook that does not exist.
4
+ type: reference
5
+ summary: Catch HookNotFoundError when calling resumeHook() or resumeWebhook() with a token that doesn't match any active hook.
6
+ related:
7
+ - /docs/api-reference/workflow/create-hook
8
+ - /docs/api-reference/workflow/define-hook
9
+ ---
10
+
11
+ `HookNotFoundError` is thrown when calling `resumeHook()` or `resumeWebhook()` with a token that does not match any active hook. This typically happens when:
12
+
13
+ - The hook has expired (past its TTL)
14
+ - The hook was already consumed and disposed
15
+ - The workflow has not started yet, so the hook does not exist
16
+
17
+ ```typescript lineNumbers
18
+ import { HookNotFoundError } from "workflow/errors"
19
+ declare function resumeHook(token: string, payload: any): Promise<any>; // @setup
20
+ declare const token: string; // @setup
21
+ declare const payload: any; // @setup
22
+
23
+ try {
24
+ await resumeHook(token, payload);
25
+ } catch (error) {
26
+ if (HookNotFoundError.is(error)) { // [!code highlight]
27
+ console.error("Hook not found:", error.token);
28
+ }
29
+ }
30
+ ```
31
+
32
+ ## API Signature
33
+
34
+ ### Properties
35
+
36
+ <TSDoc
37
+ definition={`
38
+ interface HookNotFoundError {
39
+ /** The hook token that was not found. */
40
+ token: string;
41
+ /** The error message. */
42
+ message: string;
43
+ }
44
+ export default HookNotFoundError;`}
45
+ />
46
+
47
+ ### Static Methods
48
+
49
+ #### `HookNotFoundError.is(value)`
50
+
51
+ Type-safe check for `HookNotFoundError` instances. Preferred over `instanceof` because it works across module boundaries and VM contexts.
52
+
53
+ ```typescript
54
+ import { HookNotFoundError } from "workflow/errors"
55
+ declare const error: unknown; // @setup
56
+
57
+ if (HookNotFoundError.is(error)) {
58
+ // error is typed as HookNotFoundError
59
+ }
60
+ ```
61
+
62
+ ## Examples
63
+
64
+ ### Resume hook or start workflow
65
+
66
+ A common pattern for idempotent workflows is to try resuming a hook, and if it doesn't exist yet, start a new workflow run with the input data.
67
+
68
+ <Callout>
69
+ This "resume or start" pattern is not atomic — there is a small window where a race condition is possible. A better native approach is being worked on, but this pattern works well for many use cases.
70
+ </Callout>
71
+
72
+ ```typescript lineNumbers
73
+ import { HookNotFoundError } from "workflow/errors"
74
+ declare function resumeHook(token: string, data: unknown): Promise<any>; // @setup
75
+ declare function startWorkflow(name: string, data: unknown): Promise<any>; // @setup
76
+
77
+ async function handleIncomingEvent(token: string, data: unknown) {
78
+ try {
79
+ // Try to resume an existing hook
80
+ await resumeHook(token, data);
81
+ } catch (error) {
82
+ if (HookNotFoundError.is(error)) { // [!code highlight]
83
+ // Hook doesn't exist yet — start a new workflow run
84
+ await startWorkflow("processEvent", data); // [!code highlight]
85
+ } else {
86
+ throw error;
87
+ }
88
+ }
89
+ }
90
+ ```
@@ -0,0 +1,16 @@
1
+ {
2
+ "title": "workflow/errors",
3
+ "pages": [
4
+ "hook-not-found-error",
5
+ "step-not-registered-error",
6
+ "workflow-not-registered-error",
7
+ "workflow-run-not-found-error",
8
+ "workflow-run-failed-error",
9
+ "workflow-run-cancelled-error",
10
+ "workflow-world-error",
11
+ "throttle-error",
12
+ "entity-conflict-error",
13
+ "run-expired-error",
14
+ "too-early-error"
15
+ ]
16
+ }
@@ -0,0 +1,58 @@
1
+ ---
2
+ title: RunExpiredError
3
+ description: Thrown when a workflow run has expired and can no longer be operated on.
4
+ type: reference
5
+ summary: Catch RunExpiredError when a workflow run has expired and can no longer accept operations.
6
+ related:
7
+ - /docs/api-reference/workflow-errors/workflow-world-error
8
+ - /docs/api-reference/workflow-errors/entity-conflict-error
9
+ ---
10
+
11
+ `RunExpiredError` is thrown by world implementations when a workflow run has expired and can no longer be operated on. It corresponds to HTTP 410 Gone semantics.
12
+
13
+ <Callout>
14
+ The Workflow runtime handles this error automatically. You will only encounter it when interacting with world storage APIs directly.
15
+ </Callout>
16
+
17
+ ```typescript lineNumbers
18
+ import { RunExpiredError } from "workflow/errors"
19
+ declare const world: { events: { create(...args: any[]): Promise<any> } }; // @setup
20
+ declare const runId: string; // @setup
21
+ declare const event: any; // @setup
22
+
23
+ try {
24
+ await world.events.create(runId, event);
25
+ } catch (error) {
26
+ if (RunExpiredError.is(error)) { // [!code highlight]
27
+ console.log("Run has expired and can no longer accept events");
28
+ }
29
+ }
30
+ ```
31
+
32
+ ## API Signature
33
+
34
+ ### Properties
35
+
36
+ <TSDoc
37
+ definition={`
38
+ interface RunExpiredError {
39
+ /** The error message. */
40
+ message: string;
41
+ }
42
+ export default RunExpiredError;`}
43
+ />
44
+
45
+ ### Static Methods
46
+
47
+ #### `RunExpiredError.is(value)`
48
+
49
+ Type-safe check for `RunExpiredError` instances. Preferred over `instanceof` because it works across module boundaries and VM contexts.
50
+
51
+ ```typescript
52
+ import { RunExpiredError } from "workflow/errors"
53
+ declare const error: unknown; // @setup
54
+
55
+ if (RunExpiredError.is(error)) {
56
+ // error is typed as RunExpiredError
57
+ }
58
+ ```