workflow 5.0.0-beta.2 → 5.0.0-beta.4

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 (42) hide show
  1. package/dist/api-workflow.d.ts +1 -1
  2. package/dist/api-workflow.d.ts.map +1 -1
  3. package/dist/api-workflow.js +2 -2
  4. package/docs/cookbook/{common-patterns → advanced}/child-workflows.mdx +1 -1
  5. package/docs/cookbook/advanced/distributed-abort-controller.mdx +318 -0
  6. package/docs/cookbook/advanced/meta.json +2 -3
  7. package/docs/cookbook/advanced/publishing-libraries.mdx +83 -26
  8. package/docs/cookbook/advanced/serializable-steps.mdx +15 -3
  9. package/docs/cookbook/agent-patterns/agent-cancellation.mdx +205 -0
  10. package/docs/cookbook/agent-patterns/durable-agent.mdx +50 -91
  11. package/docs/cookbook/agent-patterns/human-in-the-loop.mdx +148 -171
  12. package/docs/cookbook/agent-patterns/meta.json +1 -7
  13. package/docs/cookbook/common-patterns/batching.mdx +44 -118
  14. package/docs/cookbook/common-patterns/meta.json +4 -4
  15. package/docs/cookbook/common-patterns/saga.mdx +126 -31
  16. package/docs/cookbook/common-patterns/scheduling.mdx +70 -194
  17. package/docs/cookbook/common-patterns/sequential-and-parallel.mdx +155 -0
  18. package/docs/cookbook/common-patterns/timeouts.mdx +99 -0
  19. package/docs/cookbook/common-patterns/workflow-composition.mdx +118 -0
  20. package/docs/cookbook/index.mdx +13 -16
  21. package/docs/cookbook/integrations/ai-sdk.mdx +296 -140
  22. package/docs/cookbook/integrations/chat-sdk.mdx +251 -151
  23. package/docs/cookbook/integrations/sandbox.mdx +469 -81
  24. package/docs/cookbook/meta.json +1 -1
  25. package/docs/foundations/index.mdx +0 -3
  26. package/docs/foundations/meta.json +0 -1
  27. package/docs/foundations/serialization.mdx +1 -1
  28. package/docs/foundations/starting-workflows.mdx +1 -1
  29. package/docs/migration-guides/migrating-from-aws-step-functions.mdx +60 -8
  30. package/docs/migration-guides/migrating-from-inngest.mdx +38 -6
  31. package/docs/migration-guides/migrating-from-temporal.mdx +38 -4
  32. package/docs/migration-guides/migrating-from-trigger-dev.mdx +52 -11
  33. package/package.json +11 -11
  34. package/docs/cookbook/advanced/custom-serialization.mdx +0 -168
  35. package/docs/cookbook/advanced/durable-objects.mdx +0 -148
  36. package/docs/cookbook/advanced/isomorphic-packages.mdx +0 -145
  37. package/docs/cookbook/agent-patterns/stop-workflow.mdx +0 -216
  38. package/docs/cookbook/agent-patterns/tool-orchestration.mdx +0 -255
  39. package/docs/cookbook/agent-patterns/tool-streaming.mdx +0 -181
  40. package/docs/cookbook/common-patterns/content-router.mdx +0 -207
  41. package/docs/cookbook/common-patterns/fan-out.mdx +0 -208
  42. package/docs/foundations/common-patterns.mdx +0 -265
@@ -1,255 +0,0 @@
1
- ---
2
- title: Tool Orchestration
3
- description: Choose between step-level and workflow-level tools, or combine both for complex tool implementations.
4
- type: guide
5
- summary: Implement tools as steps for retries and I/O, at the workflow level for sleep and hooks, or combine both.
6
- ---
7
-
8
- Use this pattern to understand when to implement a tool as a step, at the workflow level, or as a combination. The choice depends on whether the tool needs Node.js I/O (step), workflow primitives like `sleep()` and hooks (workflow level), or both.
9
-
10
- ## Pattern
11
-
12
- Tools marked with `"use step"` get automatic retries and full Node.js access but cannot use `sleep()` or hooks. Tools without `"use step"` run in the workflow context and can use workflow primitives but cannot perform side effects directly. Combine both by having a workflow-level tool call into steps for I/O.
13
-
14
- ### Step-Level vs Workflow-Level
15
-
16
- | Capability | Step (`"use step"`) | Workflow Level |
17
- |------------|---------------------|----------------|
18
- | `getWritable()` | Yes | Yes |
19
- | Automatic retries | Yes | No |
20
- | Side effects (fetch, DB) | Yes | No |
21
- | `sleep()` | No | Yes |
22
- | `createHook()` / `createWebhook()` | No | Yes |
23
-
24
- ### Simplified
25
-
26
- ```typescript lineNumbers
27
- import { DurableAgent } from "@workflow/ai/agent";
28
- import { sleep, getWritable } from "workflow";
29
- import { z } from "zod";
30
- import type { UIMessageChunk } from "ai";
31
-
32
- // Step-level tool: I/O with retries
33
- async function fetchWeather({ city }: { city: string }) {
34
- "use step";
35
- const res = await fetch(`https://api.weather.com?city=${city}`);
36
- return res.json();
37
- }
38
-
39
- // Workflow-level tool: uses sleep()
40
- async function scheduleReminder({ delayMs }: { delayMs: number }) {
41
- // No "use step" — sleep() requires workflow context
42
- await sleep(delayMs); // [!code highlight]
43
- return { message: `Reminder fired after ${delayMs}ms` };
44
- }
45
-
46
- // Combined: workflow-level orchestration calling into steps
47
- async function fetchWithDelay({ url, delayMs }: { url: string; delayMs: number }) {
48
- const result = await doFetch(url); // Step handles I/O // [!code highlight]
49
- await sleep(delayMs); // Workflow handles sleep // [!code highlight]
50
- return result;
51
- }
52
-
53
- async function doFetch(url: string) {
54
- "use step";
55
- const res = await fetch(url);
56
- return res.json();
57
- }
58
-
59
- export async function assistantAgent(userMessage: string) {
60
- "use workflow";
61
-
62
- const agent = new DurableAgent({
63
- model: "anthropic/claude-haiku-4.5",
64
- tools: {
65
- fetchWeather: {
66
- description: "Get weather for a city",
67
- inputSchema: z.object({ city: z.string() }),
68
- execute: fetchWeather,
69
- },
70
- scheduleReminder: {
71
- description: "Set a reminder after a delay",
72
- inputSchema: z.object({ delayMs: z.number() }),
73
- execute: scheduleReminder,
74
- },
75
- fetchWithDelay: {
76
- description: "Fetch a URL then wait before returning",
77
- inputSchema: z.object({ url: z.string(), delayMs: z.number() }),
78
- execute: fetchWithDelay,
79
- },
80
- },
81
- });
82
-
83
- await agent.stream({ // [!code highlight]
84
- messages: [{ role: "user", content: userMessage }],
85
- writable: getWritable<UIMessageChunk>(),
86
- });
87
- }
88
- ```
89
-
90
- ### Full Implementation
91
-
92
- ```typescript lineNumbers
93
- import { DurableAgent } from "@workflow/ai/agent";
94
- import { sleep, createWebhook, getWritable } from "workflow";
95
- import { z } from "zod";
96
- import type { UIMessageChunk } from "ai";
97
-
98
- // --- Step-level tools: I/O with retries ---
99
-
100
- async function searchDatabase({ query }: { query: string }) {
101
- "use step";
102
-
103
- const response = await fetch(`https://api.example.com/search?q=${query}`);
104
- if (!response.ok) throw new Error(`Search failed: ${response.status}`);
105
- return response.json();
106
- }
107
-
108
- async function sendNotification({
109
- userId,
110
- message,
111
- }: {
112
- userId: string;
113
- message: string;
114
- }) {
115
- "use step";
116
-
117
- await fetch("https://api.example.com/notifications", {
118
- method: "POST",
119
- headers: { "Content-Type": "application/json" },
120
- body: JSON.stringify({ userId, message }),
121
- });
122
- return { sent: true };
123
- }
124
-
125
- // --- Workflow-level tool: uses sleep ---
126
-
127
- async function waitThenCheck({
128
- delayMs,
129
- endpoint,
130
- }: {
131
- delayMs: number;
132
- endpoint: string;
133
- }) {
134
- // No "use step" — workflow context needed for sleep()
135
- await sleep(delayMs); // [!code highlight]
136
- // Delegate I/O to a step
137
- return pollEndpoint(endpoint);
138
- }
139
-
140
- async function pollEndpoint(endpoint: string) {
141
- "use step";
142
- const res = await fetch(endpoint);
143
- return res.json();
144
- }
145
-
146
- // --- Workflow-level tool: uses webhook ---
147
-
148
- async function waitForCallback({ description }: { description: string }) {
149
- // No "use step" — webhooks are workflow primitives
150
- const webhook = createWebhook(); // [!code highlight]
151
- // Log the URL so external systems can call it
152
- console.log(`Waiting for callback at: ${webhook.url}`);
153
-
154
- const result = await Promise.race([ // [!code highlight]
155
- webhook.then((req) => req.json()),
156
- sleep("1h").then(() => ({ status: "timeout" })),
157
- ]);
158
-
159
- return result;
160
- }
161
-
162
- // --- Combined tool: step I/O + workflow sleep + step I/O ---
163
-
164
- async function retryWithCooldown({
165
- url,
166
- maxAttempts,
167
- }: {
168
- url: string;
169
- maxAttempts: number;
170
- }) {
171
- for (let i = 0; i < maxAttempts; i++) {
172
- const result = await attemptFetch(url);
173
- if (result.success) return result;
174
- if (i < maxAttempts - 1) {
175
- await sleep(`${(i + 1) * 5}s`); // Increasing cooldown between attempts // [!code highlight]
176
- }
177
- }
178
- return { success: false, error: "All attempts failed" };
179
- }
180
-
181
- async function attemptFetch(url: string) {
182
- "use step";
183
- try {
184
- const res = await fetch(url);
185
- if (!res.ok) return { success: false, status: res.status };
186
- return { success: true, data: await res.json() };
187
- } catch {
188
- return { success: false, error: "Network error" };
189
- }
190
- }
191
-
192
- export async function orchestrationAgent(userMessage: string) {
193
- "use workflow";
194
-
195
- const writable = getWritable<UIMessageChunk>();
196
-
197
- const agent = new DurableAgent({
198
- model: "anthropic/claude-haiku-4.5",
199
- instructions:
200
- "You are an assistant with access to search, notifications, polling, callbacks, and retry tools.",
201
- tools: {
202
- searchDatabase: {
203
- description: "Search the database",
204
- inputSchema: z.object({ query: z.string() }),
205
- execute: searchDatabase,
206
- },
207
- sendNotification: {
208
- description: "Send a notification to a user",
209
- inputSchema: z.object({
210
- userId: z.string(),
211
- message: z.string(),
212
- }),
213
- execute: sendNotification,
214
- },
215
- waitThenCheck: {
216
- description: "Wait for a duration then check an endpoint",
217
- inputSchema: z.object({
218
- delayMs: z.number().describe("Milliseconds to wait"),
219
- endpoint: z.string().describe("URL to check after waiting"),
220
- }),
221
- execute: waitThenCheck,
222
- },
223
- waitForCallback: {
224
- description: "Create a webhook and wait for an external system to call it",
225
- inputSchema: z.object({
226
- description: z.string().describe("What the callback is for"),
227
- }),
228
- execute: waitForCallback,
229
- },
230
- retryWithCooldown: {
231
- description: "Fetch a URL with retries and increasing cooldown between attempts",
232
- inputSchema: z.object({
233
- url: z.string(),
234
- maxAttempts: z.number().default(3),
235
- }),
236
- execute: retryWithCooldown,
237
- },
238
- },
239
- });
240
-
241
- await agent.stream({ // [!code highlight]
242
- messages: [{ role: "user", content: userMessage }],
243
- writable,
244
- });
245
- }
246
- ```
247
-
248
- ## Key APIs
249
-
250
- - [`"use workflow"`](/docs/api-reference/workflow/use-workflow) — declares the orchestrator function
251
- - [`"use step"`](/docs/api-reference/workflow/use-step) — declares step functions with retries and Node.js access
252
- - [`sleep()`](/docs/api-reference/workflow/sleep) — durable pause (only in workflow context)
253
- - [`createWebhook()`](/docs/api-reference/workflow/create-webhook) — wait for external HTTP callbacks (only in workflow context)
254
- - [`getWritable()`](/docs/api-reference/workflow/get-writable) — stream data from steps
255
- - [`DurableAgent`](/docs/api-reference/workflow-ai/durable-agent) — agent with mixed step/workflow-level tools
@@ -1,181 +0,0 @@
1
- ---
2
- title: Tool Streaming
3
- description: Stream real-time progress updates from tools to the UI while they execute.
4
- type: guide
5
- summary: Emit custom data parts from step functions to show incremental results during long-running tool calls.
6
- ---
7
-
8
- Use this pattern when tools take a long time to execute and you want to show progress updates, intermediate results, or status messages in the UI while the tool is still running.
9
-
10
- ## Pattern
11
-
12
- Inside a step function, call `getWritable<UIMessageChunk>()` to write custom data parts to the same stream the agent uses. These appear as typed data parts in the client's message parts array.
13
-
14
- ### Simplified
15
-
16
- ```typescript lineNumbers
17
- import { DurableAgent } from "@workflow/ai/agent";
18
- import { getWritable } from "workflow";
19
- import { z } from "zod";
20
- import type { UIMessageChunk } from "ai";
21
-
22
- declare function performSearch(query: string): Promise<{ id: string; title: string }[]>; // @setup
23
- declare function searchWithProgress(args: { query: string }): Promise<any>; // @setup
24
-
25
- export async function searchAgent(userMessage: string) {
26
- "use workflow";
27
-
28
- const agent = new DurableAgent({
29
- model: "anthropic/claude-haiku-4.5",
30
- tools: {
31
- search: {
32
- description: "Search for items",
33
- inputSchema: z.object({ query: z.string() }),
34
- execute: searchWithProgress,
35
- },
36
- },
37
- });
38
-
39
- await agent.stream({ // [!code highlight]
40
- messages: [{ role: "user", content: userMessage }],
41
- writable: getWritable<UIMessageChunk>(),
42
- });
43
- }
44
- ```
45
-
46
- ### Full Implementation
47
-
48
- ```typescript lineNumbers
49
- import { DurableAgent } from "@workflow/ai/agent";
50
- import { getWritable } from "workflow";
51
- import { z } from "zod";
52
- import type { UIMessageChunk } from "ai";
53
-
54
- // Custom data part type for the client to render
55
- interface FoundItemDataPart {
56
- type: "data-found-item";
57
- id: string;
58
- data: {
59
- title: string;
60
- score: number;
61
- };
62
- }
63
-
64
- // Step: Search with streaming progress updates
65
- async function searchWithProgress(
66
- { query }: { query: string },
67
- { toolCallId }: { toolCallId: string }
68
- ) {
69
- "use step";
70
-
71
- const writable = getWritable<UIMessageChunk>(); // [!code highlight]
72
- const writer = writable.getWriter();
73
-
74
- try {
75
- // Simulate finding items one at a time
76
- const items = [
77
- { title: "Result A", score: 95 },
78
- { title: "Result B", score: 87 },
79
- { title: "Result C", score: 72 },
80
- ];
81
-
82
- for (const item of items) {
83
- // Simulate search latency
84
- await new Promise((resolve) => setTimeout(resolve, 800));
85
-
86
- // Stream each result to the UI as it's found
87
- await writer.write({ // [!code highlight]
88
- type: "data-found-item",
89
- id: `${toolCallId}-${item.title}`,
90
- data: item,
91
- } as UIMessageChunk);
92
- }
93
-
94
- return {
95
- message: `Found ${items.length} results for "${query}"`,
96
- items,
97
- };
98
- } finally {
99
- writer.releaseLock();
100
- }
101
- }
102
-
103
- // Step: Fetch details for a specific item
104
- async function getItemDetails({ itemId }: { itemId: string }) {
105
- "use step";
106
-
107
- const writable = getWritable<UIMessageChunk>();
108
- const writer = writable.getWriter();
109
-
110
- try {
111
- // Emit a transient progress message
112
- await writer.write({ // [!code highlight]
113
- type: "data-progress",
114
- data: { message: `Loading details for ${itemId}...` },
115
- transient: true,
116
- } as UIMessageChunk);
117
-
118
- await new Promise((resolve) => setTimeout(resolve, 1000));
119
-
120
- return { itemId, description: "Detailed information", available: true };
121
- } finally {
122
- writer.releaseLock();
123
- }
124
- }
125
-
126
- export async function searchAgent(userMessage: string) {
127
- "use workflow";
128
-
129
- const writable = getWritable<UIMessageChunk>();
130
-
131
- const agent = new DurableAgent({
132
- model: "anthropic/claude-haiku-4.5",
133
- instructions: "You help users search for items. Use the search tool first, then get details if asked.",
134
- tools: {
135
- search: {
136
- description: "Search for items matching a query",
137
- inputSchema: z.object({
138
- query: z.string().describe("Search query"),
139
- }),
140
- execute: searchWithProgress,
141
- },
142
- getDetails: {
143
- description: "Get detailed information about a specific item",
144
- inputSchema: z.object({
145
- itemId: z.string().describe("Item ID from search results"),
146
- }),
147
- execute: getItemDetails,
148
- },
149
- },
150
- });
151
-
152
- await agent.stream({ // [!code highlight]
153
- messages: [{ role: "user", content: userMessage }],
154
- writable,
155
- });
156
- }
157
- ```
158
-
159
- ### Client Rendering
160
-
161
- ```tsx lineNumbers
162
- // In your chat component's message rendering:
163
- {message.parts.map((part, i) => {
164
- if (part.type === "data-found-item") {
165
- const item = part.data as { title: string; score: number };
166
- return (
167
- <div key={part.id} className="p-3 bg-muted rounded-md">
168
- <div className="font-medium">{item.title}</div>
169
- <div className="text-muted-foreground">Score: {item.score}</div>
170
- </div>
171
- );
172
- }
173
- // ... other part types
174
- })}
175
- ```
176
-
177
- ## Key APIs
178
-
179
- - [`"use step"`](/docs/api-reference/workflow/use-step) — step functions can write to the stream
180
- - [`getWritable()`](/docs/api-reference/workflow/get-writable) — access the run's output stream from inside a step
181
- - [`DurableAgent`](/docs/api-reference/workflow-ai/durable-agent) — agent streams LLM output to the same writable
@@ -1,207 +0,0 @@
1
- ---
2
- title: Conditional Routing
3
- description: Inspect a payload and route it to different step handlers based on its content.
4
- type: guide
5
- summary: Classify incoming messages and branch to specialized handlers using standard if/else logic in the workflow function.
6
- ---
7
-
8
- Use conditional routing when incoming messages need different processing paths depending on their content. A support ticket about billing goes to the billing handler; a bug report goes to engineering. The workflow inspects the payload and branches with standard JavaScript control flow.
9
-
10
- ## When to use this
11
-
12
- - Support ticket routing by category
13
- - Order processing with different flows per product type
14
- - Event handling where different event types need different logic
15
- - Any message-driven system where the handler depends on the content
16
-
17
- ## Pattern: Content-based router
18
-
19
- The workflow classifies the input, then branches with `if`/`else` to call the appropriate step:
20
-
21
- ```typescript
22
- declare function classifyTicket(ticketId: string, subject: string): Promise<{ ticketType: string }>; // @setup
23
- declare function handleBilling(ticketId: string): Promise<void>; // @setup
24
- declare function handleTechnical(ticketId: string): Promise<void>; // @setup
25
- declare function handleAccount(ticketId: string): Promise<void>; // @setup
26
- declare function handleFeedback(ticketId: string): Promise<void>; // @setup
27
-
28
- export async function routeTicket(ticketId: string, subject: string) {
29
- "use workflow";
30
-
31
- const { ticketType } = await classifyTicket(ticketId, subject); // [!code highlight]
32
-
33
- if (ticketType === "billing") { // [!code highlight]
34
- await handleBilling(ticketId);
35
- } else if (ticketType === "technical") {
36
- await handleTechnical(ticketId);
37
- } else if (ticketType === "account") {
38
- await handleAccount(ticketId);
39
- } else {
40
- await handleFeedback(ticketId);
41
- }
42
-
43
- return { ticketId, routedTo: ticketType };
44
- }
45
- ```
46
-
47
- ### Step functions
48
-
49
- Each handler is a separate `"use step"` function. The classification step can use an LLM, keyword matching, or any logic you need:
50
-
51
- ```typescript
52
- async function classifyTicket(
53
- ticketId: string,
54
- subject: string
55
- ): Promise<{ ticketType: string }> {
56
- "use step";
57
-
58
- // Example: simple keyword classification
59
- // In production, this could call an LLM or ML model
60
- const lower = subject.toLowerCase();
61
- if (lower.includes("invoice") || lower.includes("charge") || lower.includes("refund")) {
62
- return { ticketType: "billing" };
63
- }
64
- if (lower.includes("error") || lower.includes("bug") || lower.includes("crash")) {
65
- return { ticketType: "technical" };
66
- }
67
- if (lower.includes("password") || lower.includes("login") || lower.includes("access")) {
68
- return { ticketType: "account" };
69
- }
70
- return { ticketType: "feedback" };
71
- }
72
-
73
- async function handleBilling(ticketId: string): Promise<void> {
74
- "use step";
75
- // Look up billing records, process refund, etc.
76
- }
77
-
78
- async function handleTechnical(ticketId: string): Promise<void> {
79
- "use step";
80
- // Create bug report, notify engineering, etc.
81
- }
82
-
83
- async function handleAccount(ticketId: string): Promise<void> {
84
- "use step";
85
- // Reset password, update permissions, etc.
86
- }
87
-
88
- async function handleFeedback(ticketId: string): Promise<void> {
89
- "use step";
90
- // Log feedback, notify product team, etc.
91
- }
92
- ```
93
-
94
- ## Pattern: Enrichment before routing
95
-
96
- When downstream handlers need more context than the raw input provides, enrich the message in parallel before routing:
97
-
98
- ```typescript
99
- export async function enrichAndRoute(email: string) {
100
- "use workflow";
101
-
102
- // Step 1: Look up base data
103
- const contact = await lookupContact(email);
104
-
105
- // Step 2: Enrich from multiple sources in parallel
106
- const [crm, social] = await Promise.allSettled([ // [!code highlight]
107
- fetchCrmData(contact),
108
- fetchSocialData(contact),
109
- ]);
110
-
111
- const enriched = {
112
- ...contact,
113
- crm: crm.status === "fulfilled" ? crm.value : null,
114
- social: social.status === "fulfilled" ? social.value : null,
115
- };
116
-
117
- // Step 3: Route based on enriched data
118
- if (enriched.crm?.segment === "enterprise") { // [!code highlight]
119
- await routeToEnterpriseSales(enriched);
120
- } else {
121
- await routeToSelfServe(enriched);
122
- }
123
-
124
- return { email, segment: enriched.crm?.segment ?? "self-serve" };
125
- }
126
-
127
- async function lookupContact(email: string): Promise<{ email: string; domain: string }> {
128
- "use step";
129
- return { email, domain: email.split("@")[1] ?? "unknown" };
130
- }
131
-
132
- async function fetchCrmData(contact: { email: string }): Promise<{ segment: string }> {
133
- "use step";
134
- const res = await fetch(`https://crm.example.com/lookup?email=${contact.email}`);
135
- return res.json();
136
- }
137
-
138
- async function fetchSocialData(contact: { email: string }): Promise<{ followers: number }> {
139
- "use step";
140
- const res = await fetch(`https://social.example.com/lookup?email=${contact.email}`);
141
- return res.json();
142
- }
143
-
144
- async function routeToEnterpriseSales(enriched: unknown): Promise<void> {
145
- "use step";
146
- // Assign to enterprise sales team
147
- }
148
-
149
- async function routeToSelfServe(enriched: unknown): Promise<void> {
150
- "use step";
151
- // Add to self-serve onboarding flow
152
- }
153
- ```
154
-
155
- ## Pattern: Multiple event sources
156
-
157
- When a workflow must wait for signals from different systems before proceeding, create one hook per source and use `Promise.all` with a deadline:
158
-
159
- ```typescript
160
- import { defineHook, sleep } from "workflow";
161
-
162
- export const orderSignal = defineHook<{ ok: true }>();
163
-
164
- const SIGNALS = ["payment", "inventory", "fraud"] as const;
165
-
166
- export async function waitForAllSignals(orderId: string) {
167
- "use workflow";
168
-
169
- const hooks = SIGNALS.map((kind) =>
170
- orderSignal.create({ token: `${kind}:${orderId}` }) // [!code highlight]
171
- );
172
-
173
- const outcome = await Promise.race([ // [!code highlight]
174
- Promise.all(hooks).then(() => ({ type: "ready" as const })), // [!code highlight]
175
- sleep("5m").then(() => ({ type: "timeout" as const })), // [!code highlight]
176
- ]);
177
-
178
- if (outcome.type === "timeout") {
179
- return { orderId, status: "timeout" };
180
- }
181
-
182
- await shipOrder(orderId);
183
- return { orderId, status: "shipped" };
184
- }
185
-
186
- async function shipOrder(orderId: string): Promise<void> {
187
- "use step";
188
- await fetch(`https://shipping.example.com/ship`, {
189
- method: "POST",
190
- body: JSON.stringify({ orderId }),
191
- });
192
- }
193
- ```
194
-
195
- ## Tips
196
-
197
- - **Workflow functions use standard JavaScript.** `if`/`else`, `switch`, ternaries -- any branching logic works. No special routing DSL needed.
198
- - **Each handler is an independent step.** This means each gets its own retries, its own error handling, and its own entry in the event log.
199
- - **Combine with enrichment** when downstream handlers need data from multiple sources. Fan out enrichment with `Promise.allSettled`, then route on the merged result.
200
- - **Use `defineHook` for event gateways** when the routing decision depends on external signals arriving asynchronously.
201
-
202
- ## Key APIs
203
-
204
- - [`"use workflow"`](/docs/api-reference/workflow/use-workflow) -- marks the orchestrator function
205
- - [`"use step"`](/docs/api-reference/workflow/use-step) -- marks each handler as a durable step
206
- - [`defineHook()`](/docs/api-reference/workflow/define-hook) -- creates hooks for event gateway patterns
207
- - [`sleep()`](/docs/api-reference/workflow/sleep) -- durable deadline for event gateways