workflow 4.2.0-beta.71 → 4.2.0-beta.73
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.
- package/dist/api.d.ts +1 -1
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +1 -1
- package/dist/internal/errors.d.ts +1 -1
- package/dist/internal/errors.d.ts.map +1 -1
- package/dist/internal/errors.js +2 -2
- package/dist/observability.d.ts +20 -0
- package/dist/observability.d.ts.map +1 -0
- package/dist/observability.js +20 -0
- package/docs/ai/defining-tools.mdx +6 -0
- package/docs/ai/index.mdx +3 -0
- package/docs/ai/message-queueing.mdx +2 -0
- package/docs/ai/resumable-streams.mdx +37 -4
- package/docs/ai/sleep-and-delays.mdx +2 -0
- package/docs/api-reference/meta.json +1 -1
- package/docs/api-reference/workflow/define-hook.mdx +2 -0
- package/docs/api-reference/workflow/get-writable.mdx +1 -0
- package/docs/api-reference/workflow-ai/durable-agent.mdx +2 -0
- package/docs/api-reference/workflow-ai/workflow-chat-transport.mdx +2 -0
- package/docs/api-reference/workflow-api/get-run.mdx +14 -0
- package/docs/api-reference/workflow-api/get-world.mdx +105 -0
- package/docs/api-reference/workflow-errors/entity-conflict-error.mdx +60 -0
- package/docs/api-reference/workflow-errors/hook-not-found-error.mdx +90 -0
- package/docs/api-reference/workflow-errors/meta.json +16 -0
- package/docs/api-reference/workflow-errors/run-expired-error.mdx +58 -0
- package/docs/api-reference/workflow-errors/step-not-registered-error.mdx +56 -0
- package/docs/api-reference/workflow-errors/throttle-error.mdx +62 -0
- package/docs/api-reference/workflow-errors/too-early-error.mdx +62 -0
- package/docs/api-reference/workflow-errors/workflow-not-registered-error.mdx +57 -0
- package/docs/api-reference/workflow-errors/workflow-run-cancelled-error.mdx +56 -0
- package/docs/api-reference/workflow-errors/workflow-run-failed-error.mdx +62 -0
- package/docs/api-reference/workflow-errors/workflow-run-not-found-error.mdx +56 -0
- package/docs/api-reference/workflow-errors/workflow-world-error.mdx +79 -0
- package/docs/changelog/index.mdx +15 -0
- package/docs/changelog/meta.json +5 -0
- package/docs/deploying/building-a-world.mdx +20 -0
- package/docs/deploying/world/postgres-world.mdx +7 -0
- package/docs/errors/hook-conflict.mdx +9 -3
- package/docs/errors/index.mdx +6 -0
- package/docs/errors/step-not-registered.mdx +66 -0
- package/docs/errors/webhook-invalid-respond-with-value.mdx +10 -0
- package/docs/errors/webhook-response-not-sent.mdx +8 -0
- package/docs/errors/workflow-not-registered.mdx +64 -0
- package/docs/foundations/errors-and-retries.mdx +29 -0
- package/docs/foundations/streaming.mdx +22 -0
- package/docs/getting-started/index.mdx +3 -3
- package/docs/getting-started/meta.json +15 -0
- package/docs/getting-started/nestjs.mdx +4 -8
- package/docs/testing/index.mdx +1 -1
- 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,
|
|
2
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYXBpLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vc3JjL2FwaS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEVBRUwsY0FBYyxFQUNkLE1BQU0sRUFDTixHQUFHLEVBQ0gsVUFBVSxFQUNWLGFBQWEsRUFDYixPQUFPLEVBRVAsS0FBSyxHQU1OLE1BQU0sd0JBQXdCLENBQUMifQ==
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export {
|
|
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,
|
|
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"}
|
package/dist/internal/errors.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export {
|
|
2
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
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
|
-
{
|
|
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
|
|
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({
|
|
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
|
|
|
@@ -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
|
+
```
|