workflow 5.0.0-beta.5 → 5.0.0-beta.50
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 +68 -23
- package/dist/api-workflow.d.ts +1 -1
- package/dist/api-workflow.d.ts.map +1 -1
- package/dist/api-workflow.js +1 -1
- package/dist/api.d.ts +3 -3
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +5 -7
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -1
- package/dist/internal/builtins.d.ts +20 -3
- package/dist/internal/builtins.d.ts.map +1 -1
- package/dist/internal/builtins.js +68 -4
- 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/nest-builder.d.ts +2 -0
- package/dist/nest-builder.d.ts.map +1 -0
- package/dist/nest-builder.js +2 -0
- package/dist/nest-vercel-builder.d.ts +2 -0
- package/dist/nest-vercel-builder.d.ts.map +1 -0
- package/dist/nest-vercel-builder.js +2 -0
- package/dist/observability.d.ts +1 -1
- package/dist/observability.js +2 -2
- package/dist/runtime.d.ts +2 -1
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +4 -1
- package/docs/ai/chat-session-modeling.mdx +29 -26
- package/docs/ai/defining-tools.mdx +6 -7
- package/docs/ai/human-in-the-loop.mdx +11 -11
- package/docs/ai/index.mdx +50 -45
- package/docs/ai/message-queueing.mdx +16 -16
- package/docs/ai/meta.json +1 -0
- package/docs/ai/resumable-streams.mdx +40 -28
- package/docs/ai/sleep-and-delays.mdx +10 -10
- package/docs/ai/streaming-updates-from-tools.mdx +6 -6
- package/docs/api-reference/index.mdx +24 -0
- package/docs/api-reference/meta.json +8 -0
- package/docs/api-reference/vitest/index.mdx +9 -15
- package/docs/api-reference/workflow/create-hook.mdx +89 -10
- package/docs/api-reference/workflow/create-webhook.mdx +16 -15
- package/docs/api-reference/workflow/define-hook.mdx +35 -33
- package/docs/api-reference/workflow/fatal-error.mdx +30 -8
- package/docs/api-reference/workflow/fetch.mdx +14 -10
- 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 +4 -1
- package/docs/api-reference/workflow/retryable-error.mdx +1 -1
- package/docs/api-reference/workflow/set-attributes.mdx +61 -0
- package/docs/api-reference/workflow/sleep.mdx +4 -4
- package/docs/api-reference/workflow-ai/durable-agent.mdx +48 -86
- package/docs/api-reference/workflow-ai/index.mdx +3 -3
- package/docs/api-reference/workflow-ai/workflow-chat-transport.mdx +67 -24
- package/docs/api-reference/workflow-api/get-hook-by-token.mdx +26 -12
- package/docs/api-reference/workflow-api/get-run.mdx +43 -8
- package/docs/api-reference/workflow-api/index.mdx +6 -10
- package/docs/api-reference/workflow-api/resume-hook.mdx +73 -12
- package/docs/api-reference/workflow-api/resume-webhook.mdx +11 -9
- package/docs/api-reference/workflow-api/start.mdx +60 -13
- package/docs/api-reference/workflow-astro/index.mdx +18 -0
- package/docs/api-reference/workflow-astro/meta.json +4 -0
- package/docs/api-reference/workflow-astro/workflow.mdx +45 -0
- package/docs/api-reference/workflow-errors/entity-conflict-error.mdx +4 -4
- package/docs/api-reference/workflow-errors/hook-conflict-error.mdx +60 -0
- package/docs/api-reference/workflow-errors/hook-not-found-error.mdx +8 -8
- package/docs/api-reference/workflow-errors/index.mdx +88 -0
- package/docs/api-reference/workflow-errors/meta.json +6 -0
- package/docs/api-reference/workflow-errors/precondition-failed-error.mdx +68 -0
- package/docs/api-reference/workflow-errors/run-expired-error.mdx +2 -2
- package/docs/api-reference/workflow-errors/run-not-supported-error.mdx +58 -0
- 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 +52 -0
- 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 +58 -0
- package/docs/api-reference/workflow-errors/workflow-run-not-found-error.mdx +4 -4
- package/docs/api-reference/workflow-errors/workflow-runtime-error.mdx +58 -0
- package/docs/api-reference/workflow-errors/workflow-world-error.mdx +8 -8
- package/docs/api-reference/workflow-globals.mdx +14 -10
- package/docs/api-reference/workflow-nest/configure-workflow-controller.mdx +33 -0
- package/docs/api-reference/workflow-nest/index.mdx +31 -0
- package/docs/api-reference/workflow-nest/meta.json +9 -0
- package/docs/api-reference/workflow-nest/nest-local-builder.mdx +64 -0
- package/docs/api-reference/workflow-nest/workflow-controller.mdx +40 -0
- package/docs/api-reference/workflow-nest/workflow-module.mdx +74 -0
- package/docs/api-reference/workflow-next/with-workflow.mdx +39 -17
- package/docs/api-reference/workflow-nitro/index.mdx +60 -0
- package/docs/api-reference/workflow-nuxt/index.mdx +48 -0
- package/docs/api-reference/workflow-observability/hydrate-data.mdx +35 -0
- package/docs/api-reference/workflow-observability/hydrate-resource-io.mdx +62 -0
- package/docs/api-reference/workflow-observability/index.mdx +62 -0
- package/docs/api-reference/workflow-observability/meta.json +11 -0
- package/docs/api-reference/workflow-observability/observability-revivers.mdx +50 -0
- package/docs/api-reference/workflow-observability/parse-class-name.mdx +41 -0
- package/docs/api-reference/workflow-observability/parse-step-name.mdx +40 -0
- package/docs/api-reference/workflow-observability/parse-workflow-name.mdx +55 -0
- package/docs/api-reference/workflow-runtime/create-world.mdx +39 -0
- package/docs/api-reference/workflow-runtime/get-world-handlers.mdx +44 -0
- package/docs/api-reference/{workflow-api → workflow-runtime}/get-world.mdx +11 -14
- package/docs/api-reference/workflow-runtime/health-check.mdx +50 -0
- package/docs/api-reference/workflow-runtime/index.mdx +41 -0
- package/docs/api-reference/workflow-runtime/meta.json +12 -0
- package/docs/api-reference/workflow-runtime/set-world.mdx +51 -0
- package/docs/api-reference/workflow-runtime/workflow-entrypoint.mdx +43 -0
- package/docs/api-reference/workflow-runtime/world/analytics.mdx +315 -0
- package/docs/api-reference/workflow-runtime/world/index.mdx +60 -0
- package/docs/api-reference/workflow-runtime/world/meta.json +4 -0
- package/docs/api-reference/workflow-runtime/world/queue.mdx +88 -0
- package/docs/api-reference/{workflow-api → workflow-runtime}/world/storage.mdx +98 -34
- package/docs/api-reference/{workflow-api → workflow-runtime}/world/streams.mdx +8 -8
- package/docs/api-reference/workflow-serde/index.mdx +1 -2
- package/docs/api-reference/workflow-serde/workflow-deserialize.mdx +3 -4
- package/docs/api-reference/workflow-serde/workflow-serialize.mdx +8 -8
- package/docs/api-reference/workflow-sveltekit/index.mdx +18 -0
- package/docs/api-reference/workflow-sveltekit/meta.json +4 -0
- package/docs/api-reference/workflow-sveltekit/workflow-plugin.mdx +42 -0
- package/docs/api-reference/workflow-vite/index.mdx +18 -0
- package/docs/api-reference/workflow-vite/meta.json +4 -0
- package/docs/api-reference/workflow-vite/workflow.mdx +48 -0
- package/docs/changelog/attributes-mvp.mdx +380 -0
- package/docs/changelog/batched-event-writes.mdx +79 -0
- package/docs/changelog/eager-processing.mdx +110 -436
- package/docs/changelog/index.mdx +4 -2
- package/docs/changelog/lazy-event-creation.md +127 -0
- package/docs/changelog/lazy-hook-resume.mdx +78 -0
- package/docs/changelog/meta.json +11 -1
- package/docs/changelog/resilient-resume.mdx +32 -0
- package/docs/changelog/resilient-start.mdx +33 -285
- package/docs/changelog/step-message-ownership.mdx +360 -0
- package/docs/changelog/turbo-mode.md +87 -0
- package/docs/comparisons/index.mdx +66 -0
- package/docs/comparisons/meta.json +11 -0
- package/docs/comparisons/workflow-sdk-vs-aws-agentcore.mdx +55 -0
- package/docs/comparisons/workflow-sdk-vs-aws-step-functions.mdx +111 -0
- package/docs/comparisons/workflow-sdk-vs-cloudflare-workflows.mdx +71 -0
- package/docs/comparisons/workflow-sdk-vs-inngest.mdx +102 -0
- package/docs/comparisons/workflow-sdk-vs-temporal.mdx +123 -0
- package/docs/comparisons/workflow-sdk-vs-trigger-dev.mdx +104 -0
- package/docs/configuration/build-and-diagnostics.mdx +70 -0
- package/docs/configuration/cli-and-web-ui.mdx +241 -0
- package/docs/configuration/framework-options.mdx +165 -0
- package/docs/configuration/index.mdx +32 -0
- package/docs/configuration/meta.json +12 -0
- package/docs/configuration/runtime-tuning.mdx +376 -0
- package/docs/configuration/worlds.mdx +313 -0
- package/docs/cookbook/advanced/child-workflows.mdx +211 -264
- package/docs/cookbook/advanced/meta.json +6 -1
- package/docs/cookbook/advanced/publishing-libraries.mdx +65 -56
- package/docs/cookbook/advanced/serializable-steps.mdx +28 -20
- package/docs/cookbook/advanced/upgrading-workflows.mdx +199 -0
- package/docs/cookbook/agent-patterns/agent-cancellation.mdx +27 -19
- package/docs/cookbook/agent-patterns/durable-agent.mdx +14 -142
- package/docs/cookbook/agent-patterns/human-in-the-loop.mdx +30 -22
- package/docs/cookbook/common-patterns/batching.mdx +18 -14
- package/docs/cookbook/common-patterns/idempotency.mdx +41 -53
- package/docs/cookbook/common-patterns/rate-limiting.mdx +8 -4
- package/docs/cookbook/common-patterns/saga.mdx +23 -19
- package/docs/cookbook/common-patterns/scheduling.mdx +34 -22
- package/docs/cookbook/common-patterns/sequential-and-parallel.mdx +29 -25
- package/docs/cookbook/common-patterns/timeouts.mdx +26 -21
- package/docs/cookbook/common-patterns/webhooks.mdx +10 -6
- package/docs/cookbook/common-patterns/workflow-composition.mdx +30 -27
- package/docs/cookbook/index.mdx +22 -21
- package/docs/cookbook/integrations/ai-sdk.mdx +85 -47
- package/docs/cookbook/integrations/chat-sdk.mdx +50 -33
- package/docs/cookbook/integrations/sandbox.mdx +62 -45
- package/docs/deploying.mdx +95 -0
- package/docs/errors/abort-signal-timeout-in-workflow.mdx +16 -12
- package/docs/errors/corrupted-event-log.mdx +29 -18
- package/docs/errors/deployment-mismatch.mdx +71 -0
- package/docs/errors/fetch-in-workflow.mdx +11 -7
- package/docs/errors/hook-conflict.mdx +69 -13
- package/docs/errors/index.mdx +2 -36
- package/docs/errors/node-js-module-in-workflow.mdx +9 -5
- package/docs/errors/replay-divergence.mdx +27 -0
- package/docs/errors/run-expired.mdx +85 -0
- package/docs/errors/runtime-decryption-failed.mdx +77 -0
- package/docs/errors/serialization-failed.mdx +44 -12
- package/docs/errors/start-invalid-workflow-function.mdx +9 -5
- package/docs/errors/step-executed-multiple-times.mdx +23 -0
- package/docs/errors/step-not-registered.mdx +6 -6
- package/docs/errors/timeout-in-workflow.mdx +12 -8
- package/docs/errors/webhook-invalid-respond-with-value.mdx +18 -18
- package/docs/errors/webhook-response-not-sent.mdx +20 -16
- package/docs/errors/workflow-not-registered.mdx +5 -5
- package/docs/foundations/cancellation.mdx +31 -32
- package/docs/foundations/errors-and-retries.mdx +42 -11
- package/docs/foundations/hooks.mdx +64 -35
- package/docs/foundations/idempotency.mdx +244 -12
- package/docs/foundations/index.mdx +1 -23
- package/docs/foundations/meta.json +2 -1
- package/docs/foundations/serialization.mdx +21 -22
- package/docs/foundations/starting-workflows.mdx +106 -30
- package/docs/foundations/streaming.mdx +107 -59
- package/docs/foundations/versioning.mdx +263 -0
- package/docs/foundations/workflows-and-steps.mdx +9 -9
- package/docs/getting-started/astro.mdx +22 -18
- package/docs/getting-started/express.mdx +15 -11
- package/docs/getting-started/fastify.mdx +15 -11
- package/docs/getting-started/hono.mdx +15 -11
- package/docs/getting-started/index.mdx +10 -3
- package/docs/getting-started/meta.json +3 -1
- package/docs/getting-started/nestjs.mdx +87 -20
- package/docs/getting-started/next.mdx +22 -16
- package/docs/getting-started/nitro.mdx +22 -18
- package/docs/getting-started/nuxt.mdx +15 -11
- package/docs/getting-started/python.mdx +135 -40
- package/docs/getting-started/react-router/index.mdx +33 -0
- package/docs/getting-started/react-router/meta.json +5 -0
- package/docs/getting-started/react-router/v7.mdx +237 -0
- package/docs/getting-started/react-router/v8.mdx +232 -0
- package/docs/getting-started/sveltekit.mdx +20 -16
- package/docs/getting-started/tanstack-start.mdx +17 -13
- package/docs/getting-started/vite.mdx +15 -11
- package/docs/how-it-works/cancellation.mdx +63 -63
- package/docs/how-it-works/code-transform.mdx +83 -67
- package/docs/how-it-works/encryption.mdx +30 -26
- package/docs/how-it-works/event-sourcing.mdx +98 -34
- package/docs/how-it-works/framework-integrations.mdx +96 -337
- package/docs/how-it-works/understanding-directives.mdx +22 -22
- package/docs/internal/index.mdx +6 -4
- package/docs/internal/meta.json +6 -1
- package/docs/internal/nitro-native-build.mdx +38 -0
- package/docs/internal/nitro-web-ui.mdx +24 -0
- package/docs/internal/serializable-abort-controller.mdx +7 -7
- package/docs/meta.json +3 -2
- package/docs/observability/attributes.mdx +134 -0
- package/docs/observability/index.mdx +32 -10
- package/docs/observability/meta.json +1 -1
- package/docs/observability/retention.mdx +93 -0
- package/docs/observability/tracing.mdx +124 -0
- package/docs/testing/index.mdx +36 -36
- package/docs/testing/server-based.mdx +10 -10
- package/docs/whats-new.mdx +186 -0
- package/package.json +17 -14
- package/docs/api-reference/workflow-api/world/index.mdx +0 -58
- package/docs/api-reference/workflow-api/world/meta.json +0 -4
- package/docs/api-reference/workflow-api/world/observability.mdx +0 -164
- package/docs/api-reference/workflow-api/world/queue.mdx +0 -86
- package/docs/deploying/building-a-world.mdx +0 -251
- package/docs/deploying/index.mdx +0 -95
- package/docs/deploying/meta.json +0 -4
- package/docs/deploying/world/local-world.mdx +0 -84
- package/docs/deploying/world/meta.json +0 -4
- package/docs/deploying/world/postgres-world.mdx +0 -224
- package/docs/deploying/world/vercel-world.mdx +0 -179
- package/docs/migration-guides/index.mdx +0 -34
- package/docs/migration-guides/meta.json +0 -9
- package/docs/migration-guides/migrating-from-aws-step-functions.mdx +0 -363
- package/docs/migration-guides/migrating-from-inngest.mdx +0 -314
- package/docs/migration-guides/migrating-from-temporal.mdx +0 -318
- package/docs/migration-guides/migrating-from-trigger-dev.mdx +0 -337
package/docs/cookbook/index.mdx
CHANGED
|
@@ -4,34 +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
|
-
- [**
|
|
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
|
-
- [**
|
|
37
|
-
- [**
|
|
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
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: AI SDK
|
|
3
|
-
description: Use AI SDK's streamText directly inside durable workflows
|
|
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
|
|
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
|
|
@@ -11,26 +11,35 @@ related:
|
|
|
11
11
|
- /docs/api-reference/workflow-ai/durable-agent
|
|
12
12
|
---
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
<CopyPrompt
|
|
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
|
+
/>
|
|
15
17
|
|
|
16
|
-
|
|
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
|
+
|
|
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.
|
|
17
21
|
|
|
18
22
|
<Callout type="info">
|
|
19
|
-
For most agent use cases, prefer [`
|
|
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.
|
|
20
24
|
</Callout>
|
|
21
25
|
|
|
22
26
|
## When to use streamText directly
|
|
23
27
|
|
|
24
|
-
Use [`streamText()`](https://ai-sdk.dev/docs/reference/ai-sdk-core/stream-text) instead of `
|
|
28
|
+
Use [`streamText()`](https://ai-sdk.dev/docs/reference/ai-sdk-core/stream-text) instead of `WorkflowAgent` when you need:
|
|
29
|
+
|
|
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`
|
|
25
33
|
|
|
26
|
-
|
|
27
|
-
* **Structured output** — [`Output.object()`](https://ai-sdk.dev/docs/ai-sdk-core/generating-structured-data) or `Output.array()` alongside tool calling
|
|
28
|
-
* **Step-level callbacks** — `onStepFinish` for logging, metrics, or branching logic
|
|
29
|
-
* **Provider options** — per-step model switching, reasoning budgets, or custom [provider options](https://ai-sdk.dev/docs/ai-sdk-core/provider-options)
|
|
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).
|
|
30
35
|
|
|
31
36
|
## Multi-turn pattern
|
|
32
37
|
|
|
33
|
-
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
|
+
|
|
40
|
+
<Callout type="info">
|
|
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.
|
|
42
|
+
</Callout>
|
|
34
43
|
|
|
35
44
|
<Tabs items={['Workflow', 'API Route', 'Client']}>
|
|
36
45
|
|
|
@@ -48,14 +57,15 @@ export const turnHook = defineHook({ // [!code highlight]
|
|
|
48
57
|
schema: z.object({ message: z.string() }),
|
|
49
58
|
});
|
|
50
59
|
|
|
60
|
+
// `streamText` runs tool execution inside `runTurn` (a step), so tool calls
|
|
61
|
+
// are not individually durable: the entire turn retries together. See
|
|
62
|
+
// "Tools are not individually durable" below. Make side-effectful tools idempotent.
|
|
51
63
|
async function lookupOrder({ orderId }: { orderId: string }) {
|
|
52
|
-
"use step";
|
|
53
64
|
const res = await fetch(`https://api.store.com/orders/${orderId}`);
|
|
54
65
|
return res.json();
|
|
55
66
|
}
|
|
56
67
|
|
|
57
68
|
async function processRefund({ orderId, reason }: { orderId: string; reason: string }) {
|
|
58
|
-
"use step";
|
|
59
69
|
const res = await fetch("https://api.store.com/refunds", {
|
|
60
70
|
method: "POST",
|
|
61
71
|
body: JSON.stringify({ orderId, reason }),
|
|
@@ -76,7 +86,7 @@ const TOOLS = {
|
|
|
76
86
|
},
|
|
77
87
|
};
|
|
78
88
|
|
|
79
|
-
// Per-turn step
|
|
89
|
+
// Per-turn step: streams one agent response to the durable writable // [!code highlight]
|
|
80
90
|
async function runTurn(messages: ModelMessage[]) {
|
|
81
91
|
"use step";
|
|
82
92
|
|
|
@@ -89,7 +99,8 @@ async function runTurn(messages: ModelMessage[]) {
|
|
|
89
99
|
});
|
|
90
100
|
|
|
91
101
|
const writable = getWritable<UIMessageChunk>();
|
|
92
|
-
// 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.
|
|
93
104
|
await result.toUIMessageStream().pipeTo(writable, { preventClose: true }); // [!code highlight]
|
|
94
105
|
|
|
95
106
|
const response = await result.response;
|
|
@@ -100,7 +111,7 @@ export async function supportWorkflow(initialMessages: ModelMessage[]) {
|
|
|
100
111
|
"use workflow";
|
|
101
112
|
|
|
102
113
|
const { workflowRunId } = getWorkflowMetadata();
|
|
103
|
-
// Create the hook once, outside the loop
|
|
114
|
+
// Create the hook once, outside the loop: same token = HookConflictError // [!code highlight]
|
|
104
115
|
const hook = turnHook.create({ token: workflowRunId }); // [!code highlight]
|
|
105
116
|
let allMessages = initialMessages;
|
|
106
117
|
|
|
@@ -124,13 +135,18 @@ export async function supportWorkflow(initialMessages: ModelMessage[]) {
|
|
|
124
135
|
|
|
125
136
|
One endpoint handles first turn, follow-ups, and the `/done` exit. The client sends `runId` in the body to distinguish first vs follow-up.
|
|
126
137
|
|
|
138
|
+
<Callout type="info">
|
|
139
|
+
The first turn calls `start()` and then returns the `runId`. If your client or platform can retry that first request before it receives and stores the `runId`, use an atomic conversation or request key before starting the workflow. See [Run idempotency](/docs/foundations/idempotency#run-idempotency).
|
|
140
|
+
</Callout>
|
|
141
|
+
|
|
127
142
|
```typescript title="app/api/support/route.ts" lineNumbers
|
|
128
143
|
import type { UIMessage, UIMessageChunk } from "ai";
|
|
129
144
|
import { convertToModelMessages, createUIMessageStreamResponse } from "ai";
|
|
130
145
|
import { start, getRun } from "workflow/api";
|
|
131
146
|
import { supportWorkflow, turnHook } from "@/workflows/support";
|
|
132
147
|
|
|
133
|
-
// 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
|
|
134
150
|
// workflow's durable stream keeps flowing for the next turn.
|
|
135
151
|
function sliceUntilFinish( // [!code highlight]
|
|
136
152
|
source: ReadableStream<UIMessageChunk>
|
|
@@ -215,7 +231,7 @@ export async function POST(req: Request) {
|
|
|
215
231
|
} catch (e: unknown) {
|
|
216
232
|
const msg = e instanceof Error ? e.message.toLowerCase() : "";
|
|
217
233
|
if (!msg.includes("not found") && !msg.includes("expired")) throw e;
|
|
218
|
-
// Stale runId
|
|
234
|
+
// Stale runId: fall through to start fresh
|
|
219
235
|
}
|
|
220
236
|
}
|
|
221
237
|
|
|
@@ -240,7 +256,7 @@ Store the `runId` in a ref and pass it in the body of every follow-up. `Workflow
|
|
|
240
256
|
"use client";
|
|
241
257
|
|
|
242
258
|
import { useChat } from "@ai-sdk/react";
|
|
243
|
-
import { WorkflowChatTransport } from "@workflow
|
|
259
|
+
import { WorkflowChatTransport } from "@ai-sdk/workflow";
|
|
244
260
|
import { useMemo, useRef, useState } from "react";
|
|
245
261
|
|
|
246
262
|
export function SupportChat() {
|
|
@@ -289,16 +305,32 @@ export function SupportChat() {
|
|
|
289
305
|
|
|
290
306
|
## How it works
|
|
291
307
|
|
|
292
|
-
1. **One workflow
|
|
293
|
-
2.
|
|
294
|
-
3.
|
|
295
|
-
4. **`
|
|
296
|
-
5. **`
|
|
297
|
-
6.
|
|
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".
|
|
298
315
|
|
|
299
316
|
## Pitfalls
|
|
300
317
|
|
|
301
|
-
|
|
318
|
+
Review these correctness details before adapting this pattern.
|
|
319
|
+
|
|
320
|
+
### Tools are not individually durable
|
|
321
|
+
|
|
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.
|
|
323
|
+
|
|
324
|
+
The consequences:
|
|
325
|
+
|
|
326
|
+
- The atomic retry unit is the entire `runTurn`, not the individual tool call.
|
|
327
|
+
- If `processRefund` succeeds and then the model call (or a later tool) throws, the whole turn retries, and `processRefund` will run again.
|
|
328
|
+
- Tool calls do not appear as separate entries in the event log or observability dashboard.
|
|
329
|
+
|
|
330
|
+
**Mitigations:**
|
|
331
|
+
|
|
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.
|
|
302
334
|
|
|
303
335
|
### Snapshot `tailIndex` *before* resuming the hook
|
|
304
336
|
|
|
@@ -322,39 +354,45 @@ A `TransformStream` with `controller.terminate()` on the `finish` chunk seems li
|
|
|
322
354
|
|
|
323
355
|
### Release the source reader, don't cancel it
|
|
324
356
|
|
|
325
|
-
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.
|
|
326
358
|
|
|
327
359
|
### Handle stale `runId` gracefully
|
|
328
360
|
|
|
329
|
-
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.
|
|
362
|
+
|
|
363
|
+
### Make the first turn idempotent when needed
|
|
364
|
+
|
|
365
|
+
This example stores the `runId` after the first response. For strict one-session-per-thread behavior, use a deterministic hook token derived from the thread ID or conversation ID and route retries through the active hook. See [Idempotency](/docs/foundations/idempotency).
|
|
330
366
|
|
|
331
|
-
## streamText vs
|
|
367
|
+
## streamText vs WorkflowAgent
|
|
332
368
|
|
|
333
|
-
| | `streamText()` | `
|
|
369
|
+
| | `streamText()` (this pattern) | `WorkflowAgent` |
|
|
334
370
|
|---|---|---|
|
|
335
|
-
| **Tool loop** | AI SDK handles via `stopWhen` |
|
|
336
|
-
| **LLM call durability** | Re-executes
|
|
337
|
-
| **
|
|
338
|
-
| **
|
|
339
|
-
| **
|
|
340
|
-
| **
|
|
371
|
+
| **Tool loop** | AI SDK handles via `stopWhen` | Handles internally (AI SDK–compatible options) |
|
|
372
|
+
| **LLM call durability** | Re-executes with the parent turn | Each LLM call is a durable step |
|
|
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 |
|
|
374
|
+
| **Stop conditions** | `stopWhen`, `prepareStep` | `stopWhen`, `prepareStep` |
|
|
375
|
+
| **Structured output** | `Output.object()`, `Output.array()` | `output` (`Output.object()`, `Output.text()`) |
|
|
376
|
+
| **Step callbacks** | `onStepFinish`, `onChunk`, and others | `onStepFinish`, `onFinish`, `onError`, `onAbort` (`onChunk` not available) |
|
|
377
|
+
| **Setup** | Manual stream piping and turn slicing | Automatic |
|
|
341
378
|
|
|
342
|
-
Use `
|
|
379
|
+
Use `WorkflowAgent` for most agent use cases. Use `streamText` when you need the raw AI SDK surface or a per-turn durability boundary.
|
|
343
380
|
|
|
344
381
|
## Key APIs
|
|
345
382
|
|
|
346
383
|
**AI SDK** ([docs](https://ai-sdk.dev/docs))
|
|
347
384
|
|
|
348
|
-
* [`streamText()`](https://ai-sdk.dev/docs/reference/ai-sdk-core/stream-text)
|
|
349
|
-
* [`tool()` / tool calling](https://ai-sdk.dev/docs/ai-sdk-core/tools-and-tool-calling)
|
|
350
|
-
* [`stepCountIs()` / `stopWhen`](https://ai-sdk.dev/docs/ai-sdk-core/agents#stop-conditions)
|
|
351
|
-
* [`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)
|
|
352
|
-
* [`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
|
|
353
390
|
|
|
354
391
|
**Workflow SDK**
|
|
355
392
|
|
|
356
|
-
* [`"use step"`](/docs/
|
|
357
|
-
* [`defineHook()`](/docs/api-reference/workflow/define-hook)
|
|
358
|
-
* [`getWritable()`](/docs/api-reference/workflow/get-writable)
|
|
359
|
-
* [`getRun()`](/docs/api-reference/workflow-api/get-run)
|
|
360
|
-
* [`WorkflowChatTransport`](/docs/api-reference/workflow-ai/workflow-chat-transport)
|
|
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
|
|
@@ -11,20 +11,28 @@ related:
|
|
|
11
11
|
- /docs/api-reference/workflow-api/get-run
|
|
12
12
|
---
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
<CopyPrompt
|
|
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
|
+
/>
|
|
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. A single bot can support each platform. Chat SDK handles webhook verification, event normalization, subscriptions, and cross-platform features such as cards and modals.
|
|
15
19
|
|
|
16
20
|
Workflow SDK complements it by making bot **sessions** durable. Each conversation thread maps to a long-running workflow run that:
|
|
17
21
|
|
|
18
22
|
- Owns multi-turn state in the durable event log instead of Redis-by-hand bookkeeping
|
|
19
23
|
- Can `sleep()` for hours or days waiting for a user reply, an approval, or a scheduled follow-up
|
|
20
|
-
- Survives deploys, cold starts, and crashes
|
|
24
|
+
- Survives deploys, cold starts, and crashes: the session picks up from the last step on replay
|
|
21
25
|
- Receives follow-up messages via hooks, so the bot stays responsive while the workflow is still running
|
|
22
26
|
|
|
27
|
+
<Callout type="info">
|
|
28
|
+
One thread mapped to one workflow run also means the thread stays on the deployment that started it. For channels where each message should use newer code, see [Versioning](/docs/foundations/versioning) for explicit child-run and handoff patterns using `deploymentId: "latest"`.
|
|
29
|
+
</Callout>
|
|
30
|
+
|
|
23
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.
|
|
24
32
|
|
|
25
|
-
## How
|
|
33
|
+
## How it fits together
|
|
26
34
|
|
|
27
|
-
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:
|
|
28
36
|
|
|
29
37
|
```mermaid
|
|
30
38
|
flowchart TD
|
|
@@ -36,8 +44,8 @@ flowchart TD
|
|
|
36
44
|
E --> F[""use step" helpers<br/>thread.post(), thread.subscribe(), thread.setState(), …"]
|
|
37
45
|
```
|
|
38
46
|
|
|
39
|
-
- **Inbound
|
|
40
|
-
- **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.
|
|
41
49
|
|
|
42
50
|
## Why Workflow + Chat SDK
|
|
43
51
|
|
|
@@ -52,11 +60,11 @@ Workflow replaces all of that with a single durable function. The bot can:
|
|
|
52
60
|
- Schedule a follow-up message 24 hours later via `sleep("24h")`
|
|
53
61
|
- Pause on sandbox snapshot, resume when the user sends the next command (see the [Sandbox integration](/docs/cookbook/integrations/sandbox))
|
|
54
62
|
|
|
55
|
-
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.
|
|
56
64
|
|
|
57
|
-
## The
|
|
65
|
+
## The pattern: one thread = one workflow run
|
|
58
66
|
|
|
59
|
-
|
|
67
|
+
This pattern uses three files. The bot definition is separate from the workflow so adapter packages stay out of the workflow sandbox.
|
|
60
68
|
|
|
61
69
|
<Tabs items={['Bot Setup', 'Workflow', 'Event Handlers']}>
|
|
62
70
|
|
|
@@ -117,7 +125,7 @@ async function postAssistantMessage(
|
|
|
117
125
|
|
|
118
126
|
async function runTurn(text: string) {
|
|
119
127
|
"use step";
|
|
120
|
-
// Your AI SDK call, database lookup, tool loop,
|
|
128
|
+
// Your AI SDK call, database lookup, tool loop, and other operations.
|
|
121
129
|
return `You said: ${text}`;
|
|
122
130
|
}
|
|
123
131
|
|
|
@@ -149,7 +157,7 @@ export async function durableChatSession(payload: string) {
|
|
|
149
157
|
if (!(await handleMessage(thread, message))) return;
|
|
150
158
|
|
|
151
159
|
// Each hook resumption is one turn. The workflow stays suspended between
|
|
152
|
-
// messages
|
|
160
|
+
// messages: zero compute cost while idle.
|
|
153
161
|
while (true) {
|
|
154
162
|
const { message: nextRaw } = await hook; // [!code highlight]
|
|
155
163
|
const next = Message.fromJSON(nextRaw);
|
|
@@ -172,6 +180,10 @@ export type ChatTurnPayload = {
|
|
|
172
180
|
|
|
173
181
|
Handlers live outside the workflow file so adapter dependencies don't leak in. They decide whether to start a new workflow or resume an existing one, then store the `runId` in thread state:
|
|
174
182
|
|
|
183
|
+
<Callout type="info">
|
|
184
|
+
If the platform can deliver the same first message concurrently, use a deterministic hook token derived from the thread ID so duplicate handlers route to the active chat session hook. See [Run idempotency](/docs/foundations/idempotency#run-idempotency).
|
|
185
|
+
</Callout>
|
|
186
|
+
|
|
175
187
|
```typescript title="lib/chat-session-handlers.ts" lineNumbers
|
|
176
188
|
import type { Message, Thread } from "chat";
|
|
177
189
|
import { getRun, resumeHook, start } from "workflow/api";
|
|
@@ -192,7 +204,7 @@ async function startSession(thread: Thread<ThreadState>, message: Message) {
|
|
|
192
204
|
async function routeTurn(thread: Thread<ThreadState>, message: Message) {
|
|
193
205
|
const state = await thread.state;
|
|
194
206
|
|
|
195
|
-
// No run yet, or the previous run finished
|
|
207
|
+
// No run yet, or the previous run finished: start fresh.
|
|
196
208
|
if (!state?.runId || !(await getRun(state.runId).exists)) {
|
|
197
209
|
await startSession(thread, message);
|
|
198
210
|
return;
|
|
@@ -205,7 +217,7 @@ async function routeTurn(thread: Thread<ThreadState>, message: Message) {
|
|
|
205
217
|
} catch (err) {
|
|
206
218
|
const msg = err instanceof Error ? err.message.toLowerCase() : "";
|
|
207
219
|
if (msg.includes("not found") || msg.includes("expired")) {
|
|
208
|
-
// Stale runId
|
|
220
|
+
// Stale runId: start a new session rather than dropping the message.
|
|
209
221
|
await startSession(thread, message);
|
|
210
222
|
return;
|
|
211
223
|
}
|
|
@@ -248,30 +260,30 @@ export async function POST(
|
|
|
248
260
|
|
|
249
261
|
</Tabs>
|
|
250
262
|
|
|
251
|
-
## How
|
|
263
|
+
## How it works
|
|
252
264
|
|
|
253
|
-
1. **Thread state stores the `runId
|
|
254
|
-
2. **
|
|
255
|
-
3. **Subsequent messages
|
|
256
|
-
4. **
|
|
257
|
-
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()`.
|
|
258
270
|
|
|
259
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.
|
|
260
272
|
|
|
261
|
-
## Extending the
|
|
273
|
+
## Extending the pattern
|
|
262
274
|
|
|
263
|
-
Because the session is
|
|
275
|
+
Because the session is a workflow, everything else from the cookbook composes naturally:
|
|
264
276
|
|
|
265
|
-
- **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.
|
|
266
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.
|
|
267
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(...)`.
|
|
268
|
-
- **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.
|
|
269
281
|
|
|
270
282
|
## Pitfalls
|
|
271
283
|
|
|
272
284
|
### Don't import the bot at the top of workflow files
|
|
273
285
|
|
|
274
|
-
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.
|
|
275
287
|
|
|
276
288
|
### Register the bot as a singleton
|
|
277
289
|
|
|
@@ -285,19 +297,24 @@ Adapter packages (`@chat-adapter/slack`, `@chat-adapter/telegram`, etc.) depend
|
|
|
285
297
|
|
|
286
298
|
A workflow run ends but its `runId` is still cached in thread state. The next message calls `resumeHook` on a dead run and throws `not found` / `expired`. Gate on `getRun(runId).exists` before resuming, or catch the error and fall through to `startSession`. Either way the user's message must not be dropped.
|
|
287
299
|
|
|
300
|
+
### Make first-message routing atomic
|
|
301
|
+
|
|
302
|
+
Thread state is also the idempotency boundary for starting sessions. Back it with a state adapter or database operation that can atomically claim the thread before `startSession()` runs when duplicate sessions would be harmful.
|
|
303
|
+
|
|
288
304
|
### Keep the hook outside the loop
|
|
289
305
|
|
|
290
306
|
One `chatTurnHook.create({ token: workflowRunId })` per workflow run, reused every iteration. Creating a new hook with the same token throws `HookConflictError`. This is the same rule as the [AI SDK](/docs/cookbook/integrations/ai-sdk) and [Sandbox](/docs/cookbook/integrations/sandbox) session patterns.
|
|
291
307
|
|
|
292
308
|
### Platform timeouts are separate from workflow timeouts
|
|
293
309
|
|
|
294
|
-
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.
|
|
295
311
|
|
|
296
312
|
## Key APIs
|
|
297
313
|
|
|
298
|
-
- [`Chat`](https://chat-sdk.dev/docs/api/chat) / [`Thread`](https://chat-sdk.dev/docs/api/thread) / [`Message`](https://chat-sdk.dev/docs/api/message)
|
|
299
|
-
- [`start()`](/docs/api-reference/workflow-api/start)
|
|
300
|
-
- [`resumeHook()`](/docs/api-reference/workflow-api/resume-hook)
|
|
301
|
-
- [`getRun()`](/docs/api-reference/workflow-api/get-run)
|
|
302
|
-
- [`defineHook()`](/docs/api-reference/workflow/define-hook)
|
|
303
|
-
- [`registerSingleton()`](https://chat-sdk.dev/docs/api/chat)
|
|
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.
|