workflow 5.0.0-beta.43 → 5.0.0-beta.44
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/README.md +6 -4
- package/dist/internal/builtins.d.ts +4 -4
- package/dist/internal/builtins.js +6 -6
- package/docs/ai/chat-session-modeling.mdx +23 -24
- package/docs/ai/defining-tools.mdx +5 -6
- package/docs/ai/human-in-the-loop.mdx +11 -11
- package/docs/ai/index.mdx +20 -20
- package/docs/ai/message-queueing.mdx +6 -6
- package/docs/ai/meta.json +1 -0
- package/docs/ai/resumable-streams.mdx +28 -28
- package/docs/ai/sleep-and-delays.mdx +9 -9
- package/docs/ai/streaming-updates-from-tools.mdx +4 -4
- package/docs/api-reference/vitest/index.mdx +8 -8
- package/docs/api-reference/workflow/create-hook.mdx +15 -15
- package/docs/api-reference/workflow/create-webhook.mdx +15 -15
- package/docs/api-reference/workflow/define-hook.mdx +10 -10
- package/docs/api-reference/workflow/fatal-error.mdx +2 -2
- package/docs/api-reference/workflow/fetch.mdx +7 -7
- package/docs/api-reference/workflow/get-step-metadata.mdx +2 -2
- package/docs/api-reference/workflow/get-workflow-metadata.mdx +3 -3
- package/docs/api-reference/workflow/get-writable.mdx +7 -7
- package/docs/api-reference/workflow/index.mdx +1 -1
- package/docs/api-reference/workflow/retryable-error.mdx +1 -1
- package/docs/api-reference/workflow/set-attributes.mdx +2 -2
- package/docs/api-reference/workflow/sleep.mdx +3 -3
- package/docs/api-reference/workflow-ai/durable-agent.mdx +42 -42
- package/docs/api-reference/workflow-ai/index.mdx +3 -3
- package/docs/api-reference/workflow-ai/workflow-chat-transport.mdx +28 -28
- package/docs/api-reference/workflow-api/get-hook-by-token.mdx +11 -11
- package/docs/api-reference/workflow-api/get-run.mdx +10 -10
- package/docs/api-reference/workflow-api/index.mdx +2 -4
- package/docs/api-reference/workflow-api/resume-hook.mdx +14 -14
- package/docs/api-reference/workflow-api/resume-webhook.mdx +3 -3
- package/docs/api-reference/workflow-api/start.mdx +16 -15
- package/docs/api-reference/workflow-astro/workflow.mdx +3 -3
- package/docs/api-reference/workflow-errors/entity-conflict-error.mdx +4 -4
- package/docs/api-reference/workflow-errors/hook-conflict-error.mdx +4 -4
- package/docs/api-reference/workflow-errors/hook-not-found-error.mdx +8 -8
- package/docs/api-reference/workflow-errors/index.mdx +6 -6
- package/docs/api-reference/workflow-errors/precondition-failed-error.mdx +9 -9
- package/docs/api-reference/workflow-errors/run-expired-error.mdx +2 -2
- package/docs/api-reference/workflow-errors/run-not-supported-error.mdx +4 -4
- package/docs/api-reference/workflow-errors/step-not-registered-error.mdx +5 -5
- package/docs/api-reference/workflow-errors/throttle-error.mdx +2 -2
- package/docs/api-reference/workflow-errors/too-early-error.mdx +2 -2
- package/docs/api-reference/workflow-errors/workflow-error.mdx +4 -4
- package/docs/api-reference/workflow-errors/workflow-not-registered-error.mdx +5 -6
- package/docs/api-reference/workflow-errors/workflow-run-cancelled-error.mdx +6 -6
- package/docs/api-reference/workflow-errors/workflow-run-failed-error.mdx +5 -5
- package/docs/api-reference/workflow-errors/workflow-run-not-completed-error.mdx +4 -4
- package/docs/api-reference/workflow-errors/workflow-run-not-found-error.mdx +4 -4
- package/docs/api-reference/workflow-errors/workflow-runtime-error.mdx +2 -2
- package/docs/api-reference/workflow-errors/workflow-world-error.mdx +8 -8
- package/docs/api-reference/workflow-globals.mdx +12 -11
- package/docs/api-reference/workflow-nest/configure-workflow-controller.mdx +2 -2
- package/docs/api-reference/workflow-nest/nest-local-builder.mdx +4 -4
- package/docs/api-reference/workflow-nest/workflow-controller.mdx +1 -1
- package/docs/api-reference/workflow-nest/workflow-module.mdx +4 -4
- package/docs/api-reference/workflow-next/with-workflow.mdx +16 -16
- package/docs/api-reference/workflow-nitro/index.mdx +6 -6
- package/docs/api-reference/workflow-nuxt/index.mdx +4 -4
- package/docs/api-reference/workflow-observability/hydrate-data.mdx +5 -5
- package/docs/api-reference/workflow-observability/hydrate-resource-io.mdx +4 -4
- package/docs/api-reference/workflow-observability/index.mdx +6 -8
- package/docs/api-reference/workflow-observability/observability-revivers.mdx +2 -2
- package/docs/api-reference/workflow-observability/parse-class-name.mdx +3 -3
- package/docs/api-reference/workflow-observability/parse-step-name.mdx +4 -4
- package/docs/api-reference/workflow-observability/parse-workflow-name.mdx +4 -4
- package/docs/api-reference/workflow-runtime/create-world.mdx +8 -8
- package/docs/api-reference/workflow-runtime/get-world-handlers.mdx +6 -6
- package/docs/api-reference/workflow-runtime/get-world.mdx +4 -4
- package/docs/api-reference/workflow-runtime/health-check.mdx +1 -1
- package/docs/api-reference/workflow-runtime/index.mdx +2 -4
- package/docs/api-reference/workflow-runtime/set-world.mdx +15 -13
- package/docs/api-reference/workflow-runtime/workflow-entrypoint.mdx +6 -5
- package/docs/api-reference/workflow-runtime/world/analytics.mdx +9 -9
- package/docs/api-reference/workflow-runtime/world/index.mdx +5 -3
- package/docs/api-reference/workflow-runtime/world/queue.mdx +11 -11
- package/docs/api-reference/workflow-runtime/world/storage.mdx +62 -28
- package/docs/api-reference/workflow-runtime/world/streams.mdx +7 -7
- package/docs/api-reference/workflow-serde/index.mdx +1 -1
- package/docs/api-reference/workflow-serde/workflow-deserialize.mdx +2 -2
- package/docs/api-reference/workflow-serde/workflow-serialize.mdx +7 -7
- package/docs/api-reference/workflow-sveltekit/workflow-plugin.mdx +3 -3
- package/docs/api-reference/workflow-vite/workflow.mdx +5 -5
- package/docs/changelog/attributes-mvp.mdx +39 -39
- package/docs/changelog/batched-event-writes.mdx +12 -12
- package/docs/changelog/eager-processing.mdx +63 -63
- package/docs/changelog/index.mdx +3 -3
- package/docs/changelog/lazy-event-creation.md +27 -27
- package/docs/changelog/resilient-resume.mdx +5 -5
- package/docs/changelog/resilient-start.mdx +14 -14
- package/docs/changelog/step-message-ownership.mdx +47 -47
- package/docs/changelog/turbo-mode.md +20 -20
- package/docs/comparisons/index.mdx +13 -13
- package/docs/comparisons/workflow-sdk-vs-aws-agentcore.mdx +15 -15
- package/docs/comparisons/workflow-sdk-vs-aws-step-functions.mdx +12 -12
- package/docs/comparisons/workflow-sdk-vs-cloudflare-workflows.mdx +11 -11
- package/docs/comparisons/workflow-sdk-vs-inngest.mdx +19 -19
- package/docs/comparisons/workflow-sdk-vs-temporal.mdx +23 -23
- package/docs/comparisons/workflow-sdk-vs-trigger-dev.mdx +18 -17
- package/docs/configuration/build-and-diagnostics.mdx +5 -5
- package/docs/configuration/cli-and-web-ui.mdx +4 -4
- package/docs/configuration/runtime-tuning.mdx +86 -23
- package/docs/configuration/worlds.mdx +28 -14
- package/docs/cookbook/advanced/child-workflows.mdx +25 -25
- package/docs/cookbook/advanced/publishing-libraries.mdx +40 -40
- package/docs/cookbook/advanced/serializable-steps.mdx +21 -21
- package/docs/cookbook/advanced/upgrading-workflows.mdx +31 -31
- package/docs/cookbook/agent-patterns/agent-cancellation.mdx +20 -20
- package/docs/cookbook/agent-patterns/human-in-the-loop.mdx +22 -22
- package/docs/cookbook/common-patterns/batching.mdx +14 -14
- package/docs/cookbook/common-patterns/idempotency.mdx +9 -9
- package/docs/cookbook/common-patterns/rate-limiting.mdx +3 -3
- package/docs/cookbook/common-patterns/saga.mdx +19 -19
- package/docs/cookbook/common-patterns/scheduling.mdx +23 -23
- package/docs/cookbook/common-patterns/sequential-and-parallel.mdx +26 -26
- package/docs/cookbook/common-patterns/timeouts.mdx +23 -23
- package/docs/cookbook/common-patterns/webhooks.mdx +6 -6
- package/docs/cookbook/common-patterns/workflow-composition.mdx +19 -19
- package/docs/cookbook/index.mdx +22 -22
- package/docs/cookbook/integrations/ai-sdk.mdx +43 -41
- package/docs/cookbook/integrations/chat-sdk.mdx +34 -34
- package/docs/cookbook/integrations/sandbox.mdx +46 -46
- package/docs/deploying.mdx +15 -15
- package/docs/errors/abort-signal-timeout-in-workflow.mdx +12 -12
- package/docs/errors/corrupted-event-log.mdx +11 -11
- package/docs/errors/deployment-mismatch.mdx +14 -14
- package/docs/errors/fetch-in-workflow.mdx +8 -8
- package/docs/errors/hook-conflict.mdx +11 -11
- package/docs/errors/index.mdx +1 -1
- package/docs/errors/node-js-module-in-workflow.mdx +5 -5
- package/docs/errors/replay-divergence.mdx +2 -2
- package/docs/errors/runtime-decryption-failed.mdx +12 -12
- package/docs/errors/serialization-failed.mdx +40 -12
- package/docs/errors/start-invalid-workflow-function.mdx +5 -5
- package/docs/errors/step-executed-multiple-times.mdx +2 -2
- package/docs/errors/step-not-registered.mdx +5 -5
- package/docs/errors/timeout-in-workflow.mdx +8 -8
- package/docs/errors/webhook-invalid-respond-with-value.mdx +18 -18
- package/docs/errors/webhook-response-not-sent.mdx +16 -16
- package/docs/errors/workflow-not-registered.mdx +5 -5
- package/docs/foundations/cancellation.mdx +31 -31
- package/docs/foundations/errors-and-retries.mdx +42 -11
- package/docs/foundations/hooks.mdx +35 -35
- package/docs/foundations/idempotency.mdx +9 -9
- package/docs/foundations/serialization.mdx +21 -22
- package/docs/foundations/starting-workflows.mdx +36 -37
- package/docs/foundations/streaming.mdx +46 -41
- package/docs/foundations/versioning.mdx +3 -3
- package/docs/foundations/workflows-and-steps.mdx +9 -9
- package/docs/getting-started/astro.mdx +16 -16
- package/docs/getting-started/express.mdx +8 -8
- package/docs/getting-started/fastify.mdx +8 -8
- package/docs/getting-started/hono.mdx +8 -8
- package/docs/getting-started/nestjs.mdx +18 -17
- package/docs/getting-started/next.mdx +11 -11
- package/docs/getting-started/nitro.mdx +16 -16
- package/docs/getting-started/nuxt.mdx +8 -8
- package/docs/getting-started/python.mdx +4 -4
- package/docs/getting-started/react-router/v7.mdx +1 -1
- package/docs/getting-started/react-router/v8.mdx +1 -1
- package/docs/getting-started/sveltekit.mdx +14 -14
- package/docs/getting-started/tanstack-start.mdx +12 -12
- package/docs/getting-started/vite.mdx +8 -8
- package/docs/how-it-works/cancellation.mdx +62 -62
- package/docs/how-it-works/code-transform.mdx +66 -54
- package/docs/how-it-works/encryption.mdx +25 -21
- package/docs/how-it-works/event-sourcing.mdx +53 -35
- package/docs/how-it-works/framework-integrations.mdx +12 -12
- package/docs/how-it-works/understanding-directives.mdx +21 -21
- package/docs/internal/index.mdx +6 -6
- package/docs/internal/nitro-native-build.mdx +2 -2
- package/docs/internal/nitro-web-ui.mdx +4 -4
- package/docs/internal/serializable-abort-controller.mdx +7 -7
- package/docs/observability/attributes.mdx +3 -3
- package/docs/observability/index.mdx +14 -10
- package/docs/observability/tracing.mdx +10 -10
- package/docs/testing/index.mdx +33 -33
- package/docs/testing/server-based.mdx +10 -10
- package/docs/whats-new.mdx +185 -0
- package/package.json +12 -12
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
title: Webhooks & External Callbacks
|
|
3
3
|
description: Receive HTTP callbacks from external services, process them durably, and respond inline.
|
|
4
4
|
type: guide
|
|
5
|
-
summary: Create webhook endpoints that your workflow can await, process incoming requests in steps, and respond to the caller
|
|
5
|
+
summary: Create webhook endpoints that your workflow can await, process incoming requests in steps, and respond to the caller, all within durable workflow context.
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
<CopyPrompt
|
|
@@ -17,7 +17,7 @@ Use webhooks when external services push events to your application via HTTP cal
|
|
|
17
17
|
- Waiting for third-party verification or processing results
|
|
18
18
|
- Any integration where an external system calls you back asynchronously
|
|
19
19
|
|
|
20
|
-
## Pattern:
|
|
20
|
+
## Pattern: processing webhook events
|
|
21
21
|
|
|
22
22
|
Create a webhook with manual response control, then iterate over incoming requests:
|
|
23
23
|
|
|
@@ -77,7 +77,7 @@ async function processEvent(
|
|
|
77
77
|
}
|
|
78
78
|
```
|
|
79
79
|
|
|
80
|
-
## Pattern:
|
|
80
|
+
## Pattern: async request-reply with timeout
|
|
81
81
|
|
|
82
82
|
Submit a request to an external service, pass it your webhook URL, then race the callback against a deadline:
|
|
83
83
|
|
|
@@ -128,7 +128,7 @@ async function processCallback(
|
|
|
128
128
|
}
|
|
129
129
|
```
|
|
130
130
|
|
|
131
|
-
## Pattern:
|
|
131
|
+
## Pattern: large payload by reference
|
|
132
132
|
|
|
133
133
|
When payloads are too large to serialize into the event log, pass a lightweight reference (a "claim check") instead. Use a hook to signal when the data is ready:
|
|
134
134
|
|
|
@@ -143,7 +143,7 @@ export async function importLargeFile(importId: string) {
|
|
|
143
143
|
// Suspend until the external system signals the blob is uploaded
|
|
144
144
|
const { blobToken } = await blobReady.create({ token: `upload:${importId}` }); // [!code highlight]
|
|
145
145
|
|
|
146
|
-
// Process by reference
|
|
146
|
+
// Process by reference. The full payload never enters the event log.
|
|
147
147
|
await processBlob(blobToken);
|
|
148
148
|
|
|
149
149
|
return { importId, blobToken, status: "indexed" };
|
|
@@ -177,7 +177,7 @@ export async function POST(request: Request) {
|
|
|
177
177
|
- **`for await` on a webhook** lets you process multiple events from the same URL. Use `break` to stop listening after a terminal event.
|
|
178
178
|
- **Webhooks auto-generate URLs** at `/.well-known/workflow/v1/webhook/:token`. Pass this URL to external services.
|
|
179
179
|
- **Race webhooks against `sleep()`** for deadlines. If the callback doesn't arrive in time, the workflow can take a fallback action.
|
|
180
|
-
- **For large payloads**, use a hook
|
|
180
|
+
- **For large payloads**, use a hook and reference token instead of passing the data through the workflow. The event log serializes all step inputs and outputs, so large payloads hurt performance.
|
|
181
181
|
|
|
182
182
|
## Key APIs
|
|
183
183
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
title: Workflow Composition
|
|
3
3
|
description: Call workflows from other workflows by direct await (flatten into the parent) or background spawn via start() (separate run).
|
|
4
4
|
type: guide
|
|
5
|
-
summary: Compose workflows two ways
|
|
5
|
+
summary: Compose workflows two ways. Direct await flattens the child into the parent's event log, while background spawn via start() runs the child as an independent run.
|
|
6
6
|
related:
|
|
7
7
|
- /cookbook/advanced/child-workflows
|
|
8
8
|
- /cookbook/common-patterns/idempotency
|
|
@@ -11,21 +11,21 @@ related:
|
|
|
11
11
|
---
|
|
12
12
|
|
|
13
13
|
<CopyPrompt
|
|
14
|
-
text="Compose these workflows. For direct composition, `await` the child workflow function from the parent "use workflow" function
|
|
14
|
+
text="Compose these workflows. For direct composition, `await` the child workflow function from the parent "use workflow" function: the child's steps flatten into the parent's event log and run as a single run sharing the parent's lifecycle. For independent background work, spawn the child with `start()` from `workflow/api` called directly in the workflow (in v5, `start()` is step-backed and records a deterministic step boundary) and return `run.runId` so callers can track it with `getRun()`. Choose flattening when the parent needs the child's result; choose background spawn when the child should have its own run, retries, and lifetime. Keep all inputs and outputs serializable. Verify flattened execution, background spawn with a separate runId, and replay determinism."
|
|
15
15
|
/>
|
|
16
16
|
|
|
17
|
-
Workflows can call other workflows. Choose between two composition modes depending on whether the parent needs the child's result inline (direct await) or
|
|
17
|
+
Workflows can call other workflows. Choose between two composition modes depending on whether the parent needs the child's result inline (direct await) or starts the child as an independent run (background spawn). For large fan-out operations with hook-based waiting and partial-failure handling, see [Child Workflows](/cookbook/advanced/child-workflows).
|
|
18
18
|
|
|
19
19
|
## When to use this
|
|
20
20
|
|
|
21
|
-
- **Direct await
|
|
22
|
-
- **Background spawn
|
|
21
|
+
- **Direct await**: the parent needs the child's result before continuing, and you want a single unified event log
|
|
22
|
+
- **Background spawn**: the parent doesn't need to wait, and you want the child to be observable as a separate run with its own `runId`
|
|
23
23
|
|
|
24
24
|
## Pattern
|
|
25
25
|
|
|
26
26
|
### Direct await (flattening)
|
|
27
27
|
|
|
28
|
-
Call a child workflow with `await` and the child's steps execute inline within the parent
|
|
28
|
+
Call a child workflow with `await` and the child's steps execute inline within the parent. They appear in the parent's event log as if you'd called them directly.
|
|
29
29
|
|
|
30
30
|
```typescript lineNumbers
|
|
31
31
|
declare function sendEmail(userId: string): Promise<void>; // @setup
|
|
@@ -80,21 +80,21 @@ export async function processOrder(orderId: string) {
|
|
|
80
80
|
}
|
|
81
81
|
```
|
|
82
82
|
|
|
83
|
-
The parent continues immediately after `start()` returns. The child runs independently and can be monitored separately using the returned `runId
|
|
83
|
+
The parent continues immediately after `start()` returns. The child runs independently and can be monitored separately using the returned `runId`, such as through [`getRun()`](/docs/api-reference/workflow-api/get-run).
|
|
84
84
|
|
|
85
85
|
<Callout type="info">
|
|
86
86
|
Each background spawn creates a separate run. If duplicate requests must route to one active child workflow, have the child create a deterministic hook token from the business key and use that hook as the idempotency point. If concurrent starts race, the losing child can detect the conflict early with `await hook.getConflict()`, which resolves with the active owner so the child can point callers at it. See [Run idempotency](/docs/foundations/idempotency#run-idempotency).
|
|
87
87
|
</Callout>
|
|
88
88
|
|
|
89
89
|
<Callout type="info">
|
|
90
|
-
If you want the child workflow to run on the latest deployment rather than the current one, pass [`deploymentId: "latest"`](/docs/api-reference/workflow-api/start#using-deploymentid-latest) in the `start()` options. See [Versioning](/docs/foundations/versioning) for the full model. This is currently a Vercel-specific feature, and other Worlds may map the concept to their own deployment runtimes. Be aware that the child workflow's function name, file path, argument types, and return type must remain compatible across deployments
|
|
90
|
+
If you want the child workflow to run on the latest deployment rather than the current one, pass [`deploymentId: "latest"`](/docs/api-reference/workflow-api/start#using-deploymentid-latest) in the `start()` options. See [Versioning](/docs/foundations/versioning) for the full model. This is currently a Vercel-specific feature, and other Worlds may map the concept to their own deployment runtimes. Be aware that the child workflow's function name, file path, argument types, and return type must remain compatible across deployments. Renaming the function or changing its location will change the workflow ID, and modifying expected inputs or outputs can cause serialization failures.
|
|
91
91
|
</Callout>
|
|
92
92
|
|
|
93
93
|
## How it works
|
|
94
94
|
|
|
95
|
-
1. **Direct await flattens
|
|
96
|
-
2. **`start()`
|
|
97
|
-
3. **`start()` can run inside workflows
|
|
95
|
+
1. **Direct await flattens the child workflow**: When a workflow function awaits another workflow function, the child's `"use workflow"` directive is treated as inline. The child's steps emit into the parent's event log and share the parent's run ID.
|
|
96
|
+
2. **`start()` creates a new run**: The child gets its own `runId`, event log, and retry boundary. The parent only sees the `runId` returned by `start()`.
|
|
97
|
+
3. **`start()` can run inside workflows**: In v5, `start()` is step-backed, so it can be called directly from a workflow function and still records a deterministic step boundary in the event log.
|
|
98
98
|
|
|
99
99
|
## Choosing between the two modes
|
|
100
100
|
|
|
@@ -108,14 +108,14 @@ If you want the child workflow to run on the latest deployment rather than the c
|
|
|
108
108
|
|
|
109
109
|
## Adapting to your use case
|
|
110
110
|
|
|
111
|
-
- **Spawn many children at once
|
|
112
|
-
- **Wait for a background child to finish
|
|
113
|
-
- **Pass results back from background children
|
|
111
|
+
- **Spawn many children at once**: call `start()` in a loop from the workflow. For more advanced fan-out (chunking, hook-based waiting, partial-failure handling), graduate to the [Child Workflows](/cookbook/advanced/child-workflows) recipe.
|
|
112
|
+
- **Wait for a background child to finish**: combine `start()` with a completion hook the child resumes when done. The [Child Workflows](/cookbook/advanced/child-workflows) page covers the recommended `startAndWait()` pattern.
|
|
113
|
+
- **Pass results back from background children**: the wrapped child resumes the parent's hook in `finally` with `{ status, value | error }`; the parent awaits the hook instead of polling `getRun().status`.
|
|
114
114
|
|
|
115
115
|
## Key APIs
|
|
116
116
|
|
|
117
|
-
- [`"use workflow"`](/docs/foundations/workflows-and-steps)
|
|
118
|
-
- [`"use step"`](/docs/foundations/workflows-and-steps)
|
|
119
|
-
- [`start()`](/docs/api-reference/workflow-api/start)
|
|
120
|
-
- [`getRun()`](/docs/api-reference/workflow-api/get-run)
|
|
121
|
-
- [Idempotency](/docs/foundations/idempotency)
|
|
117
|
+
- [`"use workflow"`](/docs/foundations/workflows-and-steps): marks the orchestrator function
|
|
118
|
+
- [`"use step"`](/docs/foundations/workflows-and-steps): marks functions with full Node.js access
|
|
119
|
+
- [`start()`](/docs/api-reference/workflow-api/start): spawn a child workflow as a separate run
|
|
120
|
+
- [`getRun()`](/docs/api-reference/workflow-api/get-run): retrieve a workflow run's status and return value
|
|
121
|
+
- [Idempotency](/docs/foundations/idempotency): deduplicate step side effects and workflow starts
|
package/docs/cookbook/index.mdx
CHANGED
|
@@ -4,35 +4,35 @@ description: Best-practice workflow patterns with copy-paste code examples.
|
|
|
4
4
|
type: overview
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Use these workflow patterns and copy-paste code examples to implement common use cases.
|
|
8
8
|
|
|
9
|
-
## Agent
|
|
9
|
+
## Agent patterns
|
|
10
10
|
|
|
11
|
-
- [**WorkflowAgent**](/cookbook/agent-patterns/durable-agent)
|
|
12
|
-
- [**Human-in-the-Loop**](/cookbook/agent-patterns/human-in-the-loop)
|
|
13
|
-
- [**Agent Cancellation**](/cookbook/agent-patterns/agent-cancellation)
|
|
11
|
+
- [**WorkflowAgent**](/cookbook/agent-patterns/durable-agent): Build durable, resumable AI agents with AI SDK's WorkflowAgent
|
|
12
|
+
- [**Human-in-the-Loop**](/cookbook/agent-patterns/human-in-the-loop): Pause an agent for human approval, then resume based on the decision
|
|
13
|
+
- [**Agent Cancellation**](/cookbook/agent-patterns/agent-cancellation): Stop a running agent immediately via `run.cancel()` or gracefully via a hook + `Promise.race`
|
|
14
14
|
|
|
15
|
-
## Common
|
|
15
|
+
## Common patterns
|
|
16
16
|
|
|
17
|
-
- [**Sequential & Parallel Execution**](/cookbook/common-patterns/sequential-and-parallel)
|
|
18
|
-
- [**Workflow Composition**](/cookbook/common-patterns/workflow-composition)
|
|
19
|
-
- [**Saga**](/cookbook/common-patterns/saga)
|
|
20
|
-
- [**Batching**](/cookbook/common-patterns/batching)
|
|
21
|
-
- [**Rate Limiting**](/cookbook/common-patterns/rate-limiting)
|
|
22
|
-
- [**Scheduling**](/cookbook/common-patterns/scheduling)
|
|
23
|
-
- [**Timeouts**](/cookbook/common-patterns/timeouts)
|
|
24
|
-
- [**Idempotency**](/cookbook/common-patterns/idempotency)
|
|
25
|
-
- [**Webhooks**](/cookbook/common-patterns/webhooks)
|
|
17
|
+
- [**Sequential & Parallel Execution**](/cookbook/common-patterns/sequential-and-parallel): Compose steps with `await`, `Promise.all`, and `Promise.race` against durable sleeps and webhooks
|
|
18
|
+
- [**Workflow Composition**](/cookbook/common-patterns/workflow-composition): Call workflows from other workflows by direct await or background spawn via `start()`
|
|
19
|
+
- [**Saga**](/cookbook/common-patterns/saga): Coordinate multi-step transactions with automatic rollback when a step fails
|
|
20
|
+
- [**Batching**](/cookbook/common-patterns/batching): Process large collections in parallel batches with failure isolation
|
|
21
|
+
- [**Rate Limiting**](/cookbook/common-patterns/rate-limiting): Handle 429 responses and transient failures with RetryableError and backoff
|
|
22
|
+
- [**Scheduling**](/cookbook/common-patterns/scheduling): Use durable sleep to schedule actions minutes, hours, or weeks ahead
|
|
23
|
+
- [**Timeouts**](/cookbook/common-patterns/timeouts): Add deadlines to slow steps, hooks, and webhooks by racing them against a durable sleep
|
|
24
|
+
- [**Idempotency**](/cookbook/common-patterns/idempotency): Ensure side effects and duplicate starts are safe to retry
|
|
25
|
+
- [**Webhooks**](/cookbook/common-patterns/webhooks): Receive HTTP callbacks from external services and process them durably
|
|
26
26
|
|
|
27
27
|
## Integrations
|
|
28
28
|
|
|
29
|
-
- [**AI SDK**](/cookbook/integrations/ai-sdk)
|
|
30
|
-
- [**Chat SDK**](/cookbook/integrations/chat-sdk)
|
|
31
|
-
- [**Sandbox**](/cookbook/integrations/sandbox)
|
|
29
|
+
- [**AI SDK**](/cookbook/integrations/ai-sdk): Use streamText() directly inside a workflow for lower-level control over model calls and tool execution
|
|
30
|
+
- [**Chat SDK**](/cookbook/integrations/chat-sdk): Build durable chat sessions with workflow persistence and AI SDK chat primitives
|
|
31
|
+
- [**Sandbox**](/cookbook/integrations/sandbox): Orchestrate Vercel Sandbox lifecycle inside durable workflows
|
|
32
32
|
|
|
33
33
|
## Advanced
|
|
34
34
|
|
|
35
|
-
- [**Child Workflows**](/cookbook/advanced/child-workflows)
|
|
36
|
-
- [**Upgrading Workflows**](/cookbook/advanced/upgrading-workflows)
|
|
37
|
-
- [**Serializable Steps**](/cookbook/advanced/serializable-steps)
|
|
38
|
-
- [**Publishing Libraries**](/cookbook/advanced/publishing-libraries)
|
|
35
|
+
- [**Child Workflows**](/cookbook/advanced/child-workflows): Spawn and orchestrate child workflows from a parent
|
|
36
|
+
- [**Upgrading Workflows**](/cookbook/advanced/upgrading-workflows): Identify a clean upgrade point in a long-running workflow and spawn a fresh run on the latest deployment carrying state forward
|
|
37
|
+
- [**Serializable Steps**](/cookbook/advanced/serializable-steps): Wrap non-serializable third-party objects so they cross the workflow boundary
|
|
38
|
+
- [**Publishing Libraries**](/cookbook/advanced/publishing-libraries): Ship npm packages that export reusable workflow functions
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
title: AI SDK
|
|
3
3
|
description: Use AI SDK's streamText directly inside durable workflows when you need the raw AI SDK API or a per-turn durability boundary.
|
|
4
4
|
type: guide
|
|
5
|
-
summary: Use streamText() inside a workflow when the durability boundary is an entire user turn, or when you need AI SDK APIs not exposed by WorkflowAgent. Individual tool calls and LLM calls inside a turn are not separately durable.
|
|
5
|
+
summary: Use streamText() inside a workflow when the durability boundary is an entire user turn, or when you need AI SDK APIs not exposed by WorkflowAgent. Individual tool calls and large language model (LLM) calls inside a turn are not separately durable.
|
|
6
6
|
related:
|
|
7
7
|
- /docs/ai
|
|
8
8
|
- /docs/ai/chat-session-modeling
|
|
@@ -15,27 +15,27 @@ related:
|
|
|
15
15
|
text="Implement the durable AI SDK multi-turn pattern. Use `streamText`, `stepCountIs`, and `createUIMessageStreamResponse` from `ai`; `defineHook`, `getWritable`, and `getWorkflowMetadata` from `workflow`; and `start`/`getRun` from `workflow/api`. Put the model call in a `"use step"` function such as `runTurn(messages)` and pipe `result.toUIMessageStream()` to `getWritable<UIMessageChunk>()` with `{ preventClose: true }`. In the workflow, create one hook with `turnHook.create({ token: workflowRunId })`, loop over turns, and await the hook between user messages. Add an API route that starts a run on first message, stores/returns the run ID in `x-workflow-run-id`, resumes the hook for follow-up messages, reads from `run.getReadable({ startIndex })`, and handles stale run IDs by starting fresh. Wire the client transport to send `runId` with each request and verify first turn, follow-up turn, `/done`, and reconnect behavior."
|
|
16
16
|
/>
|
|
17
17
|
|
|
18
|
-
[AI SDK](https://ai-sdk.dev/) is Vercel's framework-agnostic TypeScript toolkit for building AI-powered apps and agents
|
|
18
|
+
[AI SDK](https://ai-sdk.dev/) is Vercel's framework-agnostic TypeScript toolkit for building AI-powered apps and agents. It provides unified provider access, streaming, tool calling, structured output, and UI hooks. Workflow SDK makes the multi-turn loop durable, so the conversation state, hooks, and per-turn responses survive restarts and timeouts. In this pattern, the durability boundary is the entire turn, and individual tool calls inside a turn are **not** durable on their own (see [Pitfalls](#tools-are-not-individually-durable)).
|
|
19
19
|
|
|
20
|
-
For the full AI SDK reference
|
|
20
|
+
For the full AI SDK reference, including providers, `streamText`, `generateObject`, `useChat`, and tool calling, see the [AI SDK docs](https://ai-sdk.dev/docs). This page covers the Workflow-specific integration points.
|
|
21
21
|
|
|
22
22
|
<Callout type="info">
|
|
23
|
-
For most agent use cases, prefer AI SDK's [`WorkflowAgent`](https://ai-sdk.dev/v7/docs/agents/workflow-agent#workflowagent), which implements the same agent loop as [`streamText`](https://ai-sdk.dev/docs/reference/ai-sdk-core/stream-text), manages tool calling automatically, and runs tools at workflow scope
|
|
23
|
+
For most agent use cases, prefer AI SDK's [`WorkflowAgent`](https://ai-sdk.dev/v7/docs/agents/workflow-agent#workflowagent), which implements the same agent loop as [`streamText`](https://ai-sdk.dev/docs/reference/ai-sdk-core/stream-text), manages tool calling automatically, and runs tools at workflow scope: each tool can be marked `"use step"` for per-call durability and retries, or stay at workflow level to use primitives like `sleep()` and hooks. Use this page's raw `streamText()` pattern when you want the exact AI SDK API (for example `toUIMessageStream()`, `onChunk`, or `generateText`), or when the durability boundary should be an entire user turn in one step, accepting that tool calls inside that turn are not individually durable.
|
|
24
24
|
</Callout>
|
|
25
25
|
|
|
26
26
|
## When to use streamText directly
|
|
27
27
|
|
|
28
28
|
Use [`streamText()`](https://ai-sdk.dev/docs/reference/ai-sdk-core/stream-text) instead of `WorkflowAgent` when you need:
|
|
29
29
|
|
|
30
|
-
* **The raw AI SDK API
|
|
31
|
-
* **Per-turn durability
|
|
32
|
-
* **Custom multi-turn orchestration
|
|
30
|
+
* **The raw AI SDK API**: `streamText().toUIMessageStream()`, `onChunk`, `smoothStream`, or other options that map directly to the [`streamText`](https://ai-sdk.dev/docs/reference/ai-sdk-core/stream-text) return value rather than `WorkflowAgent.stream()`
|
|
31
|
+
* **Per-turn durability**: wrap the entire agent response (model + tools) in a single `"use step"` function so one user turn is the atomic retry unit; useful when you want all tool calls inside a turn to re-execute together
|
|
32
|
+
* **Custom multi-turn orchestration**: manual hook loops, per-turn stream slicing (`sliceUntilFinish`), or other workflow patterns shown below that don't map cleanly to `WorkflowAgent`
|
|
33
33
|
|
|
34
34
|
`WorkflowAgent` already supports `stopWhen`, `prepareStep`, lifecycle callbacks, structured output (`output`), per-step model switching, and [provider options](https://ai-sdk.dev/docs/ai-sdk-core/provider-options). See the [`WorkflowAgent` docs](https://ai-sdk.dev/v7/docs/agents/workflow-agent#workflowagent).
|
|
35
35
|
|
|
36
36
|
## Multi-turn pattern
|
|
37
37
|
|
|
38
|
-
One workflow run
|
|
38
|
+
One workflow run represents one full conversation. The workflow suspends between turns on a hook and resumes when the next user message arrives. Conversation state, tool history, and intermediate computation all live inside the run.
|
|
39
39
|
|
|
40
40
|
<Callout type="info">
|
|
41
41
|
Because the conversation is one workflow run, it stays on the deployment that started it. If each turn should run on the latest deployment while preserving selected state or streams, see [Versioning](/docs/foundations/versioning) for the child-run continuation pattern.
|
|
@@ -57,8 +57,8 @@ export const turnHook = defineHook({ // [!code highlight]
|
|
|
57
57
|
schema: z.object({ message: z.string() }),
|
|
58
58
|
});
|
|
59
59
|
|
|
60
|
-
// `streamText` runs tool
|
|
61
|
-
// are not individually durable
|
|
60
|
+
// `streamText` runs tool execution inside `runTurn` (a step), so tool calls
|
|
61
|
+
// are not individually durable: the entire turn retries together. See
|
|
62
62
|
// "Tools are not individually durable" below. Make side-effectful tools idempotent.
|
|
63
63
|
async function lookupOrder({ orderId }: { orderId: string }) {
|
|
64
64
|
const res = await fetch(`https://api.store.com/orders/${orderId}`);
|
|
@@ -86,7 +86,7 @@ const TOOLS = {
|
|
|
86
86
|
},
|
|
87
87
|
};
|
|
88
88
|
|
|
89
|
-
// Per-turn step
|
|
89
|
+
// Per-turn step: streams one agent response to the durable writable // [!code highlight]
|
|
90
90
|
async function runTurn(messages: ModelMessage[]) {
|
|
91
91
|
"use step";
|
|
92
92
|
|
|
@@ -99,7 +99,8 @@ async function runTurn(messages: ModelMessage[]) {
|
|
|
99
99
|
});
|
|
100
100
|
|
|
101
101
|
const writable = getWritable<UIMessageChunk>();
|
|
102
|
-
// preventClose keeps the durable writable open so the next turn can
|
|
102
|
+
// preventClose keeps the durable writable open so the next turn can write
|
|
103
|
+
// to it. Each turn still emits its own start and finish chunks.
|
|
103
104
|
await result.toUIMessageStream().pipeTo(writable, { preventClose: true }); // [!code highlight]
|
|
104
105
|
|
|
105
106
|
const response = await result.response;
|
|
@@ -110,7 +111,7 @@ export async function supportWorkflow(initialMessages: ModelMessage[]) {
|
|
|
110
111
|
"use workflow";
|
|
111
112
|
|
|
112
113
|
const { workflowRunId } = getWorkflowMetadata();
|
|
113
|
-
// Create the hook once, outside the loop
|
|
114
|
+
// Create the hook once, outside the loop: same token = HookConflictError // [!code highlight]
|
|
114
115
|
const hook = turnHook.create({ token: workflowRunId }); // [!code highlight]
|
|
115
116
|
let allMessages = initialMessages;
|
|
116
117
|
|
|
@@ -144,7 +145,8 @@ import { convertToModelMessages, createUIMessageStreamResponse } from "ai";
|
|
|
144
145
|
import { start, getRun } from "workflow/api";
|
|
145
146
|
import { supportWorkflow, turnHook } from "@/workflows/support";
|
|
146
147
|
|
|
147
|
-
// Pump the durable stream until this turn's `finish` chunk, then close
|
|
148
|
+
// Pump the durable stream until this turn's `finish` chunk, then close the
|
|
149
|
+
// HTTP response. The source reader is released (not canceled) so the
|
|
148
150
|
// workflow's durable stream keeps flowing for the next turn.
|
|
149
151
|
function sliceUntilFinish( // [!code highlight]
|
|
150
152
|
source: ReadableStream<UIMessageChunk>
|
|
@@ -229,7 +231,7 @@ export async function POST(req: Request) {
|
|
|
229
231
|
} catch (e: unknown) {
|
|
230
232
|
const msg = e instanceof Error ? e.message.toLowerCase() : "";
|
|
231
233
|
if (!msg.includes("not found") && !msg.includes("expired")) throw e;
|
|
232
|
-
// Stale runId
|
|
234
|
+
// Stale runId: fall through to start fresh
|
|
233
235
|
}
|
|
234
236
|
}
|
|
235
237
|
|
|
@@ -303,21 +305,21 @@ export function SupportChat() {
|
|
|
303
305
|
|
|
304
306
|
## How it works
|
|
305
307
|
|
|
306
|
-
1. **One workflow
|
|
307
|
-
2. **`runTurn` is the durability boundary
|
|
308
|
-
3. **
|
|
309
|
-
4. **`preventClose: true
|
|
310
|
-
5. **`sliceUntilFinish
|
|
311
|
-
6. **`startIndex: tailIndex + 1
|
|
312
|
-
7. **`/done
|
|
308
|
+
1. **One workflow represents one conversation**: The workflow loops on a hook, keeping `allMessages`, tool history, and state alive across turns.
|
|
309
|
+
2. **`runTurn` is the durability boundary**: Each turn is one step. The model request and all tool calls inside it run as plain inline functions within that step. If anything throws mid-turn, the whole `runTurn` retries. Individual tool calls are not separately durable. See [Pitfalls](#tools-are-not-individually-durable).
|
|
310
|
+
3. **The hook is created once**: Call `turnHook.create({ token: workflowRunId })` outside the loop. Calling it twice with the same token throws `HookConflictError`.
|
|
311
|
+
4. **`preventClose: true` keeps the writable open**: Set this option on `pipeTo` so the next turn can write to the durable writable.
|
|
312
|
+
5. **`sliceUntilFinish` closes each HTTP response**: The API reads chunks until `type === "finish"`, then closes the HTTP response. The source reader is released, not canceled, so the workflow stream keeps flowing.
|
|
313
|
+
6. **`startIndex: tailIndex + 1` returns only new chunks**: Each follow-up response avoids replaying previous turns.
|
|
314
|
+
7. **`/done` exits the workflow**: The route resumes the hook so the workflow exits cleanly, then returns synthetic `start` and `finish` chunks so `useChat` transitions out of "streaming".
|
|
313
315
|
|
|
314
316
|
## Pitfalls
|
|
315
317
|
|
|
316
|
-
|
|
318
|
+
Review these correctness details before adapting this pattern.
|
|
317
319
|
|
|
318
320
|
### Tools are not individually durable
|
|
319
321
|
|
|
320
|
-
`streamText()` is invoked from inside `runTurn` (a `"use step"` function), and the AI SDK calls each tool by directly invoking its `execute` function in that same step. Even if a tool body has its own `"use step"` directive, that directive is a [no-op when called from another step](/docs/foundations/workflows-and-steps#step-functions)
|
|
322
|
+
`streamText()` is invoked from inside `runTurn` (a `"use step"` function), and the AI SDK calls each tool by directly invoking its `execute` function in that same step. Even if a tool body has its own `"use step"` directive, that directive is a [no-op when called from another step](/docs/foundations/workflows-and-steps#step-functions): the function runs inline.
|
|
321
323
|
|
|
322
324
|
The consequences:
|
|
323
325
|
|
|
@@ -327,8 +329,8 @@ The consequences:
|
|
|
327
329
|
|
|
328
330
|
**Mitigations:**
|
|
329
331
|
|
|
330
|
-
- Make side-effectful tool implementations idempotent
|
|
331
|
-
- Or use AI SDK's [`WorkflowAgent`](https://ai-sdk.dev/v7/docs/agents/workflow-agent#workflowagent), which runs tools at workflow scope
|
|
332
|
+
- Make side-effectful tool implementations idempotent: deduplicate server-side on a stable key, such as `orderId` or an `Idempotency-Key` header.
|
|
333
|
+
- Or use AI SDK's [`WorkflowAgent`](https://ai-sdk.dev/v7/docs/agents/workflow-agent#workflowagent), which runs tools at workflow scope: each tool can be marked `"use step"` to become its own durable, retryable step, or stay at workflow level to use primitives like `sleep()` and hooks.
|
|
332
334
|
|
|
333
335
|
### Snapshot `tailIndex` *before* resuming the hook
|
|
334
336
|
|
|
@@ -352,11 +354,11 @@ A `TransformStream` with `controller.terminate()` on the `finish` chunk seems li
|
|
|
352
354
|
|
|
353
355
|
### Release the source reader, don't cancel it
|
|
354
356
|
|
|
355
|
-
In `sliceUntilFinish`, use `reader.releaseLock()` in the `finally` block rather than `source.cancel()`.
|
|
357
|
+
In `sliceUntilFinish`, use `reader.releaseLock()` in the `finally` block rather than `source.cancel()`. Canceling propagates upstream and closes the durable writable, breaking the next turn. Releasing the lock only detaches our reader; the durable stream keeps flowing.
|
|
356
358
|
|
|
357
359
|
### Handle stale `runId` gracefully
|
|
358
360
|
|
|
359
|
-
Clients can send a `runId` from a
|
|
361
|
+
Clients can send a `runId` from a workflow that no longer exists, such as after using local storage, navigating back, or restarting the server. Wrap the follow-up path in a `try/catch` for `not found` or `expired`, then use the first-turn code path to start a new workflow.
|
|
360
362
|
|
|
361
363
|
### Make the first turn idempotent when needed
|
|
362
364
|
|
|
@@ -368,10 +370,10 @@ This example stores the `runId` after the first response. For strict one-session
|
|
|
368
370
|
|---|---|---|
|
|
369
371
|
| **Tool loop** | AI SDK handles via `stopWhen` | Handles internally (AI SDK–compatible options) |
|
|
370
372
|
| **LLM call durability** | Re-executes with the parent turn | Each LLM call is a durable step |
|
|
371
|
-
| **Tool call durability** | Not individually durable
|
|
373
|
+
| **Tool call durability** | Not individually durable: re-executes with the parent turn | Per tool: mark `"use step"` for a durable, retryable step, or keep at workflow level for `sleep()` / hooks |
|
|
372
374
|
| **Stop conditions** | `stopWhen`, `prepareStep` | `stopWhen`, `prepareStep` |
|
|
373
375
|
| **Structured output** | `Output.object()`, `Output.array()` | `output` (`Output.object()`, `Output.text()`) |
|
|
374
|
-
| **Step callbacks** | `onStepFinish`, `onChunk`,
|
|
376
|
+
| **Step callbacks** | `onStepFinish`, `onChunk`, and others | `onStepFinish`, `onFinish`, `onError`, `onAbort` (`onChunk` not available) |
|
|
375
377
|
| **Setup** | Manual stream piping and turn slicing | Automatic |
|
|
376
378
|
|
|
377
379
|
Use `WorkflowAgent` for most agent use cases. Use `streamText` when you need the raw AI SDK surface or a per-turn durability boundary.
|
|
@@ -380,17 +382,17 @@ Use `WorkflowAgent` for most agent use cases. Use `streamText` when you need the
|
|
|
380
382
|
|
|
381
383
|
**AI SDK** ([docs](https://ai-sdk.dev/docs))
|
|
382
384
|
|
|
383
|
-
* [`streamText()`](https://ai-sdk.dev/docs/reference/ai-sdk-core/stream-text)
|
|
384
|
-
* [`tool()` / tool calling](https://ai-sdk.dev/docs/ai-sdk-core/tools-and-tool-calling)
|
|
385
|
-
* [`stepCountIs()` / `stopWhen`](https://ai-sdk.dev/docs/ai-sdk-core/agents#stop-conditions)
|
|
386
|
-
* [`convertToModelMessages()`](https://ai-sdk.dev/docs/reference/ai-sdk-ui/convert-to-model-messages) / [`createUIMessageStreamResponse()`](https://ai-sdk.dev/docs/reference/ai-sdk-ui/create-ui-message-stream-response)
|
|
387
|
-
* [`useChat()`](https://ai-sdk.dev/docs/reference/ai-sdk-ui/use-chat)
|
|
385
|
+
* [`streamText()`](https://ai-sdk.dev/docs/reference/ai-sdk-core/stream-text): core streaming function; `toUIMessageStream()` pipes into the durable writable
|
|
386
|
+
* [`tool()` / tool calling](https://ai-sdk.dev/docs/ai-sdk-core/tools-and-tool-calling): tools are plain async functions invoked by `streamText` inside the turn step; they are **not** individually durable in this pattern (see [Pitfalls](#tools-are-not-individually-durable))
|
|
387
|
+
* [`stepCountIs()` / `stopWhen`](https://ai-sdk.dev/docs/ai-sdk-core/agents#stop-conditions): bound the agent loop inside each turn
|
|
388
|
+
* [`convertToModelMessages()`](https://ai-sdk.dev/docs/reference/ai-sdk-ui/convert-to-model-messages) / [`createUIMessageStreamResponse()`](https://ai-sdk.dev/docs/reference/ai-sdk-ui/create-ui-message-stream-response): UI ↔ model message conversion at the API boundary
|
|
389
|
+
* [`useChat()`](https://ai-sdk.dev/docs/reference/ai-sdk-ui/use-chat): React hook that consumes the UI message stream on the client
|
|
388
390
|
|
|
389
391
|
**Workflow SDK**
|
|
390
392
|
|
|
391
|
-
* [`"use step"`](/docs/foundations/workflows-and-steps#step-functions)
|
|
392
|
-
* [`defineHook()`](/docs/api-reference/workflow/define-hook)
|
|
393
|
-
* [`getWritable()`](/docs/api-reference/workflow/get-writable)
|
|
394
|
-
* [`getRun()`](/docs/api-reference/workflow-api/get-run)
|
|
395
|
-
* [`WorkflowChatTransport`](/docs/api-reference/workflow-ai/workflow-chat-transport)
|
|
396
|
-
* [Idempotency](/docs/foundations/idempotency)
|
|
393
|
+
* [`"use step"`](/docs/foundations/workflows-and-steps#step-functions): applied to `runTurn` to make each turn a durable, retryable unit
|
|
394
|
+
* [`defineHook()`](/docs/api-reference/workflow/define-hook): suspension point for follow-up messages
|
|
395
|
+
* [`getWritable()`](/docs/api-reference/workflow/get-writable): resumable stream output
|
|
396
|
+
* [`getRun()`](/docs/api-reference/workflow-api/get-run): `run.getReadable({ startIndex })` for slicing per-turn streams
|
|
397
|
+
* [`WorkflowChatTransport`](/docs/api-reference/workflow-ai/workflow-chat-transport): passes `runId` between turns
|
|
398
|
+
* [Idempotency](/docs/foundations/idempotency): protect duplicate-sensitive first turns and side effects
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Chat SDK
|
|
3
|
-
description: Make Chat SDK bot sessions durable
|
|
3
|
+
description: Make Chat SDK bot sessions durable, with one workflow run per conversation thread and hooks bridging inbound platform events into long-running agent logic.
|
|
4
4
|
type: guide
|
|
5
|
-
summary: Chat SDK normalizes Slack, Teams, Discord, Telegram and
|
|
5
|
+
summary: Chat SDK normalizes Slack, Teams, Discord, Telegram, and similar platforms into one thread and message model. Workflow SDK gives each thread a durable run that owns multi-turn state, can sleep for hours, and survives restarts.
|
|
6
6
|
related:
|
|
7
7
|
- /docs/cookbook/integrations/ai-sdk
|
|
8
8
|
- /docs/cookbook/integrations/sandbox
|
|
@@ -15,13 +15,13 @@ related:
|
|
|
15
15
|
text="Make this Chat SDK bot durable with Workflow SDK. Install/use `workflow`. Create one exported workflow function with "use workflow" per chat thread. Store the Chat SDK thread ID, Workflow run ID, and any serialized conversation state in the project data store. Use `defineHook()` from `workflow` for incoming turns and call `resumeHook()` from `workflow/api` from the Chat SDK webhook or message handler. Put provider calls, database writes, and outbound platform messages in "use step" helper functions. Start a new run with `start(workflowFn, [initialThreadState])` when no run exists, otherwise resume the existing hook. Use `getRun(runId)` for status, cancellation, or stream reads. Verify first message, follow-up message, restart/reconnect, duplicate webhook, and failed-send retry behavior."
|
|
16
16
|
/>
|
|
17
17
|
|
|
18
|
-
[Chat SDK](https://chat-sdk.dev/) is a unified TypeScript SDK for building bots across Slack, Microsoft Teams, Google Chat, Discord, Telegram, GitHub, Linear, and WhatsApp.
|
|
18
|
+
[Chat SDK](https://chat-sdk.dev/) is a unified TypeScript SDK for building bots across Slack, Microsoft Teams, Google Chat, Discord, Telegram, GitHub, Linear, and WhatsApp. A single bot can support each platform. Chat SDK handles webhook verification, event normalization, subscriptions, and cross-platform features such as cards and modals.
|
|
19
19
|
|
|
20
20
|
Workflow SDK complements it by making bot **sessions** durable. Each conversation thread maps to a long-running workflow run that:
|
|
21
21
|
|
|
22
22
|
- Owns multi-turn state in the durable event log instead of Redis-by-hand bookkeeping
|
|
23
23
|
- Can `sleep()` for hours or days waiting for a user reply, an approval, or a scheduled follow-up
|
|
24
|
-
- Survives deploys, cold starts, and crashes
|
|
24
|
+
- Survives deploys, cold starts, and crashes: the session picks up from the last step on replay
|
|
25
25
|
- Receives follow-up messages via hooks, so the bot stays responsive while the workflow is still running
|
|
26
26
|
|
|
27
27
|
<Callout type="info">
|
|
@@ -30,9 +30,9 @@ One thread mapped to one workflow run also means the thread stays on the deploym
|
|
|
30
30
|
|
|
31
31
|
The rest of this page covers the integration pattern. For a full Slack + Next.js + Redis walkthrough, see the [Durable chat sessions guide](https://chat-sdk.dev/docs/guides/durable-chat-sessions-nextjs) on chat-sdk.dev.
|
|
32
32
|
|
|
33
|
-
## How
|
|
33
|
+
## How it fits together
|
|
34
34
|
|
|
35
|
-
Chat SDK owns the edge
|
|
35
|
+
Chat SDK owns the edge: webhook verification, event routing, `thread.post()` / `thread.stream()`. Workflow owns the session: state, loops, sleeps, retries. They meet at exactly two points:
|
|
36
36
|
|
|
37
37
|
```mermaid
|
|
38
38
|
flowchart TD
|
|
@@ -44,8 +44,8 @@ flowchart TD
|
|
|
44
44
|
E --> F[""use step" helpers<br/>thread.post(), thread.subscribe(), thread.setState(), …"]
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
-
- **Inbound
|
|
48
|
-
- **Outbound
|
|
47
|
+
- **Inbound**: Chat SDK handlers decide whether to `start(workflow, [thread, message])` or `resumeHook(runId, { message })`. The `runId` lives in Chat SDK's thread state (Redis, Postgres, or any state adapter).
|
|
48
|
+
- **Outbound**: the workflow calls Chat SDK APIs (`thread.post()`, `thread.subscribe()`, `thread.setState()`) from inside step functions. Never from the top level of a workflow file, since adapter packages use Node-only modules that aren't available in the workflow sandbox.
|
|
49
49
|
|
|
50
50
|
## Why Workflow + Chat SDK
|
|
51
51
|
|
|
@@ -60,11 +60,11 @@ Workflow replaces all of that with a single durable function. The bot can:
|
|
|
60
60
|
- Schedule a follow-up message 24 hours later via `sleep("24h")`
|
|
61
61
|
- Pause on sandbox snapshot, resume when the user sends the next command (see the [Sandbox integration](/docs/cookbook/integrations/sandbox))
|
|
62
62
|
|
|
63
|
-
Because the session *is* a workflow run, its history is recoverable from the event log
|
|
63
|
+
Because the session *is* a workflow run, its history is recoverable from the event log, so there's no separate message store to keep in sync.
|
|
64
64
|
|
|
65
|
-
## The
|
|
65
|
+
## The pattern: one thread = one workflow run
|
|
66
66
|
|
|
67
|
-
|
|
67
|
+
This pattern uses three files. The bot definition is separate from the workflow so adapter packages stay out of the workflow sandbox.
|
|
68
68
|
|
|
69
69
|
<Tabs items={['Bot Setup', 'Workflow', 'Event Handlers']}>
|
|
70
70
|
|
|
@@ -125,7 +125,7 @@ async function postAssistantMessage(
|
|
|
125
125
|
|
|
126
126
|
async function runTurn(text: string) {
|
|
127
127
|
"use step";
|
|
128
|
-
// Your AI SDK call, database lookup, tool loop,
|
|
128
|
+
// Your AI SDK call, database lookup, tool loop, and other operations.
|
|
129
129
|
return `You said: ${text}`;
|
|
130
130
|
}
|
|
131
131
|
|
|
@@ -157,7 +157,7 @@ export async function durableChatSession(payload: string) {
|
|
|
157
157
|
if (!(await handleMessage(thread, message))) return;
|
|
158
158
|
|
|
159
159
|
// Each hook resumption is one turn. The workflow stays suspended between
|
|
160
|
-
// messages
|
|
160
|
+
// messages: zero compute cost while idle.
|
|
161
161
|
while (true) {
|
|
162
162
|
const { message: nextRaw } = await hook; // [!code highlight]
|
|
163
163
|
const next = Message.fromJSON(nextRaw);
|
|
@@ -204,7 +204,7 @@ async function startSession(thread: Thread<ThreadState>, message: Message) {
|
|
|
204
204
|
async function routeTurn(thread: Thread<ThreadState>, message: Message) {
|
|
205
205
|
const state = await thread.state;
|
|
206
206
|
|
|
207
|
-
// No run yet, or the previous run finished
|
|
207
|
+
// No run yet, or the previous run finished: start fresh.
|
|
208
208
|
if (!state?.runId || !(await getRun(state.runId).exists)) {
|
|
209
209
|
await startSession(thread, message);
|
|
210
210
|
return;
|
|
@@ -217,7 +217,7 @@ async function routeTurn(thread: Thread<ThreadState>, message: Message) {
|
|
|
217
217
|
} catch (err) {
|
|
218
218
|
const msg = err instanceof Error ? err.message.toLowerCase() : "";
|
|
219
219
|
if (msg.includes("not found") || msg.includes("expired")) {
|
|
220
|
-
// Stale runId
|
|
220
|
+
// Stale runId: start a new session rather than dropping the message.
|
|
221
221
|
await startSession(thread, message);
|
|
222
222
|
return;
|
|
223
223
|
}
|
|
@@ -260,30 +260,30 @@ export async function POST(
|
|
|
260
260
|
|
|
261
261
|
</Tabs>
|
|
262
262
|
|
|
263
|
-
## How
|
|
263
|
+
## How it works
|
|
264
264
|
|
|
265
|
-
1. **Thread state stores the `runId
|
|
266
|
-
2. **
|
|
267
|
-
3. **Subsequent messages
|
|
268
|
-
4. **
|
|
269
|
-
5. **
|
|
265
|
+
1. **Thread state stores the `runId`**: Chat SDK's state adapter (Redis, Postgres, or memory) holds `{ runId }` per thread. This state connects the two SDKs.
|
|
266
|
+
2. **The first mention calls `start()`**: The handler serializes `thread` and `message` with `toJSON()`, passes them through `start(durableChatSession, [payload])`, and stores the returned `runId` in thread state.
|
|
267
|
+
3. **Subsequent messages call `resumeHook()`**: The handler looks up the `runId`, serializes the new message, and resumes the workflow's hook. The workflow continues on the next `await hook` iteration.
|
|
268
|
+
4. **The workflow posts through steps**: All Chat SDK side effects (`thread.post`, `thread.subscribe`, and `thread.setState`) happen inside `"use step"` helpers that dynamically import the bot. This keeps adapter packages outside the workflow sandbox.
|
|
269
|
+
5. **The session ends in two ways**: The workflow returns normally when the user sends `done` or an approval is granted, or the workflow throws. Either way, the run completes. The next inbound message with the stale `runId` falls through to `startSession()`.
|
|
270
270
|
|
|
271
271
|
The workflow is fully durable between turns: `await hook` suspends with zero compute cost, and platform webhooks can fire from anywhere without concern for which server instance handled the previous turn.
|
|
272
272
|
|
|
273
|
-
## Extending the
|
|
273
|
+
## Extending the pattern
|
|
274
274
|
|
|
275
|
-
Because the session is
|
|
275
|
+
Because the session is a workflow, everything else from the cookbook composes naturally:
|
|
276
276
|
|
|
277
|
-
- **Stream AI SDK responses into the thread.** Use the [AI SDK integration](/docs/cookbook/integrations/ai-sdk) pattern inside a step, then pass `result.fullStream` to `thread.post()
|
|
277
|
+
- **Stream AI SDK responses into the thread.** Use the [AI SDK integration](/docs/cookbook/integrations/ai-sdk) pattern inside a step, then pass `result.fullStream` to `thread.post()`. Chat SDK handles platform-specific streaming, including Slack edit-in-place and Telegram message-per-chunk.
|
|
278
278
|
- **Give the bot a sandbox.** Combine with the [Sandbox integration](/docs/cookbook/integrations/sandbox): each thread gets its own persistent sandbox session, snapshots on idle, resumes on the next message. That's effectively a coding-agent bot.
|
|
279
279
|
- **Human-in-the-loop approvals.** `Promise.race([hook, approvalHook])` inside the workflow, post buttons in the thread via [cards](https://chat-sdk.dev/docs/cards), resume `approvalHook` from `bot.onAction(...)`.
|
|
280
|
-
- **Scheduled follow-ups.** `sleep("24h")` before a proactive check-in.
|
|
280
|
+
- **Scheduled follow-ups.** Call `sleep("24h")` before a proactive check-in. The workflow preserves the timer across restarts.
|
|
281
281
|
|
|
282
282
|
## Pitfalls
|
|
283
283
|
|
|
284
284
|
### Don't import the bot at the top of workflow files
|
|
285
285
|
|
|
286
|
-
Adapter packages
|
|
286
|
+
Adapter packages such as `@chat-adapter/slack` and `@chat-adapter/telegram` depend on Node-only modules that aren't available in the workflow bundler's sandbox. Keep `import { bot } from "@/lib/bot"` inside `"use step"` functions with `await import(...)`. Use `reviver` from `chat` for deserialization inside the workflow: it's standalone and has no adapter dependencies.
|
|
287
287
|
|
|
288
288
|
### Register the bot as a singleton
|
|
289
289
|
|
|
@@ -307,14 +307,14 @@ One `chatTurnHook.create({ token: workflowRunId })` per workflow run, reused eve
|
|
|
307
307
|
|
|
308
308
|
### Platform timeouts are separate from workflow timeouts
|
|
309
309
|
|
|
310
|
-
Slack
|
|
310
|
+
Slack requires an HTTP 200 response within 3s. The webhook handler returns after `resumeHook`, then the workflow runs in the background and posts through `thread.post`. Don't `await` the whole turn inside the webhook handler because that synchronous integration exceeds the platform timeout.
|
|
311
311
|
|
|
312
312
|
## Key APIs
|
|
313
313
|
|
|
314
|
-
- [`Chat`](https://chat-sdk.dev/docs/api/chat) / [`Thread`](https://chat-sdk.dev/docs/api/thread) / [`Message`](https://chat-sdk.dev/docs/api/message)
|
|
315
|
-
- [`start()`](/docs/api-reference/workflow-api/start)
|
|
316
|
-
- [`resumeHook()`](/docs/api-reference/workflow-api/resume-hook)
|
|
317
|
-
- [`getRun()`](/docs/api-reference/workflow-api/get-run)
|
|
318
|
-
- [`defineHook()`](/docs/api-reference/workflow/define-hook)
|
|
319
|
-
- [`registerSingleton()`](https://chat-sdk.dev/docs/api/chat)
|
|
320
|
-
- [Idempotency](/docs/foundations/idempotency)
|
|
314
|
+
- [`Chat`](https://chat-sdk.dev/docs/api/chat) / [`Thread`](https://chat-sdk.dev/docs/api/thread) / [`Message`](https://chat-sdk.dev/docs/api/message): Chat SDK primitives. `toJSON()` / `fromJSON()` / `reviver` are the serialization layer.
|
|
315
|
+
- [`start()`](/docs/api-reference/workflow-api/start): start a new session workflow. Store the returned `runId` in thread state.
|
|
316
|
+
- [`resumeHook()`](/docs/api-reference/workflow-api/resume-hook): forward a new platform message to the running workflow.
|
|
317
|
+
- [`getRun()`](/docs/api-reference/workflow-api/get-run): `run.exists` before resuming, to detect stale `runId`s.
|
|
318
|
+
- [`defineHook()`](/docs/api-reference/workflow/define-hook): per-turn suspension point inside the workflow.
|
|
319
|
+
- [`registerSingleton()`](https://chat-sdk.dev/docs/api/chat): makes the bot resolvable from inside step functions.
|
|
320
|
+
- [Idempotency](/docs/foundations/idempotency): protect duplicate-sensitive first messages and side effects.
|