workflow 5.0.0-beta.9 → 5.0.1
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 +3 -1
- package/dist/api-workflow.d.ts.map +1 -1
- package/dist/api-workflow.js +2 -1
- package/dist/api.d.ts +5 -4
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +6 -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 +4 -4
- package/dist/internal/builtins.js +6 -6
- 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/runtime.d.ts +2 -1
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +4 -1
- package/docs/advanced/dynamic-workflows.mdx +227 -0
- package/docs/advanced/index.mdx +13 -0
- package/docs/advanced/meta.json +5 -0
- package/docs/ai/chat-session-modeling.mdx +176 -422
- package/docs/ai/defining-tools.mdx +6 -7
- package/docs/ai/human-in-the-loop.mdx +16 -12
- package/docs/ai/index.mdx +67 -72
- package/docs/ai/message-queueing.mdx +71 -110
- 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 +25 -1
- package/docs/api-reference/meta.json +8 -0
- package/docs/api-reference/vitest/index.mdx +68 -15
- package/docs/api-reference/workflow/create-hook.mdx +170 -10
- package/docs/api-reference/workflow/create-webhook.mdx +16 -15
- package/docs/api-reference/workflow/define-hook.mdx +41 -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 +3 -3
- package/docs/api-reference/workflow/retryable-error.mdx +1 -1
- package/docs/api-reference/workflow/set-attributes.mdx +63 -0
- package/docs/api-reference/workflow/sleep.mdx +4 -4
- package/docs/api-reference/workflow-ai/durable-agent.mdx +63 -101
- package/docs/api-reference/workflow-ai/index.mdx +5 -5
- package/docs/api-reference/workflow-ai/workflow-chat-transport.mdx +67 -24
- package/docs/api-reference/workflow-api/get-hook-by-token.mdx +37 -15
- package/docs/api-reference/workflow-api/get-run.mdx +43 -8
- package/docs/api-reference/workflow-api/index.mdx +8 -9
- package/docs/api-reference/workflow-api/register-lifecycle-hooks.mdx +87 -0
- package/docs/api-reference/workflow-api/resume-hook.mdx +77 -12
- package/docs/api-reference/workflow-api/resume-webhook.mdx +15 -9
- package/docs/api-reference/workflow-api/start.mdx +107 -12
- 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-force-claimed-error.mdx +70 -0
- package/docs/api-reference/workflow-errors/hook-not-found-error.mdx +8 -8
- package/docs/api-reference/workflow-errors/index.mdx +91 -0
- package/docs/api-reference/workflow-errors/meta.json +7 -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 +13 -6
- package/docs/api-reference/workflow-errors/workflow-run-failed-error.mdx +12 -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 +19 -11
- package/docs/api-reference/workflow-nest/configure-workflow-controller.mdx +37 -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 +46 -0
- package/docs/api-reference/workflow-nest/workflow-module.mdx +134 -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 +51 -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 +133 -35
- 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 +53 -41
- 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 +89 -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 +424 -0
- package/docs/configuration/worlds.mdx +341 -0
- package/docs/cookbook/advanced/child-workflows.mdx +33 -25
- package/docs/cookbook/advanced/publishing-libraries.mdx +65 -56
- package/docs/cookbook/advanced/serializable-steps.mdx +48 -68
- package/docs/cookbook/advanced/upgrading-workflows.mdx +35 -31
- package/docs/cookbook/agent-patterns/agent-cancellation.mdx +78 -60
- package/docs/cookbook/agent-patterns/durable-agent.mdx +23 -135
- package/docs/cookbook/agent-patterns/human-in-the-loop.mdx +180 -195
- package/docs/cookbook/common-patterns/batching.mdx +20 -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 +30 -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 +12 -7
- package/docs/cookbook/common-patterns/workflow-composition.mdx +27 -17
- package/docs/cookbook/index.mdx +22 -22
- package/docs/cookbook/integrations/ai-sdk.mdx +63 -48
- package/docs/cookbook/integrations/chat-sdk.mdx +46 -33
- package/docs/cookbook/integrations/sandbox.mdx +58 -45
- package/docs/deploying.mdx +106 -0
- package/docs/errors/abort-signal-timeout-in-workflow.mdx +16 -12
- package/docs/errors/corrupted-event-log.mdx +39 -18
- package/docs/errors/deployment-mismatch.mdx +71 -0
- package/docs/errors/fetch-in-workflow.mdx +15 -14
- package/docs/errors/hook-conflict.mdx +38 -11
- package/docs/errors/hook-force-claimed.mdx +96 -0
- package/docs/errors/index.mdx +24 -37
- 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 +54 -11
- package/docs/foundations/hooks.mdx +187 -36
- package/docs/foundations/idempotency.mdx +267 -12
- package/docs/foundations/index.mdx +1 -26
- package/docs/foundations/serialization.mdx +22 -22
- package/docs/foundations/starting-workflows.mdx +104 -30
- package/docs/foundations/streaming.mdx +108 -60
- package/docs/foundations/versioning.mdx +4 -4
- package/docs/foundations/workflows-and-steps.mdx +10 -10
- 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 +264 -21
- package/docs/getting-started/next.mdx +18 -14
- package/docs/getting-started/nitro.mdx +22 -18
- package/docs/getting-started/nuxt.mdx +15 -11
- package/docs/getting-started/python.mdx +190 -41
- 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 +82 -66
- package/docs/how-it-works/encryption.mdx +30 -26
- package/docs/how-it-works/event-sourcing.mdx +132 -35
- 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 +4 -2
- package/docs/observability/attributes.mdx +91 -21
- package/docs/observability/index.mdx +29 -15
- package/docs/observability/lifecycle-hooks.mdx +95 -0
- package/docs/observability/meta.json +1 -1
- package/docs/observability/retention.mdx +95 -0
- package/docs/observability/tracing.mdx +124 -0
- package/docs/testing/index.mdx +118 -38
- package/docs/testing/server-based.mdx +10 -10
- package/docs/whats-new.mdx +196 -0
- package/docs/worlds/building-a-world.mdx +600 -0
- package/docs/worlds/local.mdx +129 -0
- package/docs/worlds/meta.json +10 -0
- package/docs/worlds/postgres.mdx +428 -0
- package/docs/worlds/upgrading-to-v5.mdx +183 -0
- package/docs/worlds/vercel.mdx +389 -0
- package/package.json +17 -14
- package/docs/api-reference/workflow/experimental-set-attributes.mdx +0 -63
- 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 -181
- 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 -358
- package/docs/migration-guides/migrating-from-inngest.mdx +0 -304
- package/docs/migration-guides/migrating-from-temporal.mdx +0 -313
- package/docs/migration-guides/migrating-from-trigger-dev.mdx +0 -328
|
@@ -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,13 +11,17 @@ 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
|
|
|
23
27
|
<Callout type="info">
|
|
@@ -26,9 +30,9 @@ One thread mapped to one workflow run also means the thread stays on the deploym
|
|
|
26
30
|
|
|
27
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.
|
|
28
32
|
|
|
29
|
-
## How
|
|
33
|
+
## How it fits together
|
|
30
34
|
|
|
31
|
-
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:
|
|
32
36
|
|
|
33
37
|
```mermaid
|
|
34
38
|
flowchart TD
|
|
@@ -40,8 +44,8 @@ flowchart TD
|
|
|
40
44
|
E --> F[""use step" helpers<br/>thread.post(), thread.subscribe(), thread.setState(), …"]
|
|
41
45
|
```
|
|
42
46
|
|
|
43
|
-
- **Inbound
|
|
44
|
-
- **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.
|
|
45
49
|
|
|
46
50
|
## Why Workflow + Chat SDK
|
|
47
51
|
|
|
@@ -56,11 +60,11 @@ Workflow replaces all of that with a single durable function. The bot can:
|
|
|
56
60
|
- Schedule a follow-up message 24 hours later via `sleep("24h")`
|
|
57
61
|
- Pause on sandbox snapshot, resume when the user sends the next command (see the [Sandbox integration](/docs/cookbook/integrations/sandbox))
|
|
58
62
|
|
|
59
|
-
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.
|
|
60
64
|
|
|
61
|
-
## The
|
|
65
|
+
## The pattern: one thread = one workflow run
|
|
62
66
|
|
|
63
|
-
|
|
67
|
+
This pattern uses three files. The bot definition is separate from the workflow so adapter packages stay out of the workflow sandbox.
|
|
64
68
|
|
|
65
69
|
<Tabs items={['Bot Setup', 'Workflow', 'Event Handlers']}>
|
|
66
70
|
|
|
@@ -121,7 +125,7 @@ async function postAssistantMessage(
|
|
|
121
125
|
|
|
122
126
|
async function runTurn(text: string) {
|
|
123
127
|
"use step";
|
|
124
|
-
// Your AI SDK call, database lookup, tool loop,
|
|
128
|
+
// Your AI SDK call, database lookup, tool loop, and other operations.
|
|
125
129
|
return `You said: ${text}`;
|
|
126
130
|
}
|
|
127
131
|
|
|
@@ -153,7 +157,7 @@ export async function durableChatSession(payload: string) {
|
|
|
153
157
|
if (!(await handleMessage(thread, message))) return;
|
|
154
158
|
|
|
155
159
|
// Each hook resumption is one turn. The workflow stays suspended between
|
|
156
|
-
// messages
|
|
160
|
+
// messages: zero compute cost while idle.
|
|
157
161
|
while (true) {
|
|
158
162
|
const { message: nextRaw } = await hook; // [!code highlight]
|
|
159
163
|
const next = Message.fromJSON(nextRaw);
|
|
@@ -176,6 +180,10 @@ export type ChatTurnPayload = {
|
|
|
176
180
|
|
|
177
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:
|
|
178
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
|
+
|
|
179
187
|
```typescript title="lib/chat-session-handlers.ts" lineNumbers
|
|
180
188
|
import type { Message, Thread } from "chat";
|
|
181
189
|
import { getRun, resumeHook, start } from "workflow/api";
|
|
@@ -196,7 +204,7 @@ async function startSession(thread: Thread<ThreadState>, message: Message) {
|
|
|
196
204
|
async function routeTurn(thread: Thread<ThreadState>, message: Message) {
|
|
197
205
|
const state = await thread.state;
|
|
198
206
|
|
|
199
|
-
// No run yet, or the previous run finished
|
|
207
|
+
// No run yet, or the previous run finished: start fresh.
|
|
200
208
|
if (!state?.runId || !(await getRun(state.runId).exists)) {
|
|
201
209
|
await startSession(thread, message);
|
|
202
210
|
return;
|
|
@@ -209,7 +217,7 @@ async function routeTurn(thread: Thread<ThreadState>, message: Message) {
|
|
|
209
217
|
} catch (err) {
|
|
210
218
|
const msg = err instanceof Error ? err.message.toLowerCase() : "";
|
|
211
219
|
if (msg.includes("not found") || msg.includes("expired")) {
|
|
212
|
-
// Stale runId
|
|
220
|
+
// Stale runId: start a new session rather than dropping the message.
|
|
213
221
|
await startSession(thread, message);
|
|
214
222
|
return;
|
|
215
223
|
}
|
|
@@ -252,30 +260,30 @@ export async function POST(
|
|
|
252
260
|
|
|
253
261
|
</Tabs>
|
|
254
262
|
|
|
255
|
-
## How
|
|
263
|
+
## How it works
|
|
256
264
|
|
|
257
|
-
1. **Thread state stores the `runId
|
|
258
|
-
2. **
|
|
259
|
-
3. **Subsequent messages
|
|
260
|
-
4. **
|
|
261
|
-
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()`.
|
|
262
270
|
|
|
263
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.
|
|
264
272
|
|
|
265
|
-
## Extending the
|
|
273
|
+
## Extending the pattern
|
|
266
274
|
|
|
267
|
-
Because the session is
|
|
275
|
+
Because the session is a workflow, everything else from the cookbook composes naturally:
|
|
268
276
|
|
|
269
|
-
- **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.
|
|
270
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.
|
|
271
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(...)`.
|
|
272
|
-
- **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.
|
|
273
281
|
|
|
274
282
|
## Pitfalls
|
|
275
283
|
|
|
276
284
|
### Don't import the bot at the top of workflow files
|
|
277
285
|
|
|
278
|
-
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.
|
|
279
287
|
|
|
280
288
|
### Register the bot as a singleton
|
|
281
289
|
|
|
@@ -289,19 +297,24 @@ Adapter packages (`@chat-adapter/slack`, `@chat-adapter/telegram`, etc.) depend
|
|
|
289
297
|
|
|
290
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.
|
|
291
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
|
+
|
|
292
304
|
### Keep the hook outside the loop
|
|
293
305
|
|
|
294
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.
|
|
295
307
|
|
|
296
308
|
### Platform timeouts are separate from workflow timeouts
|
|
297
309
|
|
|
298
|
-
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.
|
|
299
311
|
|
|
300
312
|
## Key APIs
|
|
301
313
|
|
|
302
|
-
- [`Chat`](https://chat-sdk.dev/docs/api/chat) / [`Thread`](https://chat-sdk.dev/docs/api/thread) / [`Message`](https://chat-sdk.dev/docs/api/message)
|
|
303
|
-
- [`start()`](/docs/api-reference/workflow-api/start)
|
|
304
|
-
- [`resumeHook()`](/docs/api-reference/workflow-api/resume-hook)
|
|
305
|
-
- [`getRun()`](/docs/api-reference/workflow-api/get-run)
|
|
306
|
-
- [`defineHook()`](/docs/api-reference/workflow/define-hook)
|
|
307
|
-
- [`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.
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Sandbox
|
|
3
|
-
description: Model one Vercel Sandbox per workflow run
|
|
3
|
+
description: Model one Vercel Sandbox per workflow run, durable, idle-efficient, and not bound by the 5-hour sandbox hard cap.
|
|
4
4
|
type: guide
|
|
5
|
-
summary: Own a sandbox for the lifetime of a workflow run. Hibernate on idle via snapshot(), proactively refresh before the sandbox hard cap, and reconnect by runId
|
|
5
|
+
summary: Own a sandbox for the lifetime of a workflow run. Hibernate on idle via snapshot(), proactively refresh before the sandbox hard cap, and reconnect by `runId`, so one logical session can run effectively forever.
|
|
6
6
|
related:
|
|
7
7
|
- /docs/ai/defining-tools
|
|
8
8
|
- /docs/foundations/errors-and-retries
|
|
@@ -10,34 +10,38 @@ related:
|
|
|
10
10
|
- /docs/cookbook/agent-patterns/durable-agent
|
|
11
11
|
---
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
<CopyPrompt
|
|
14
|
+
text="Implement a durable Vercel Sandbox-backed coding-agent workflow. Install the Sandbox package used by this project and `workflow`. Create an exported workflow function with "use workflow" that owns the agent session. Put sandbox creation, command execution, snapshot, refresh, and cleanup into helper functions with "use step". Persist the sandbox ID, snapshot ID, and workflow run ID in the project data store so clients can reconnect. Use `getWritable()` from `workflow` to stream agent progress and command output. Use `sleep()` to hibernate, refresh, or enforce idle timeouts. Add API routes to start a session, reconnect by run ID, and stop/cleanup. Verify first run, reconnect after reload, snapshot restore, timeout, and cleanup behavior."
|
|
15
|
+
/>
|
|
16
|
+
|
|
17
|
+
[Vercel Sandbox](https://vercel.com/docs/sandbox) provides isolated code execution environments. The `@vercel/sandbox` package has first-class support for the Workflow SDK: the `Sandbox` class is serializable, and its methods (`create`, `runCommand`, `stop`, `snapshot`) implicitly run as steps. You can use `Sandbox` directly inside a workflow function without wrapping each call in a separate `"use step"` function.
|
|
14
18
|
|
|
15
19
|
## Why Workflow + Sandbox
|
|
16
20
|
|
|
17
|
-
A sandbox alone
|
|
21
|
+
A sandbox alone provides an isolated virtual machine (VM). A workflow provides a **durable controller** for that VM's entire lifetime:
|
|
18
22
|
|
|
19
23
|
- **One workflow run = one sandbox session.** The `runId` is the only state you need to persist on the client. Close the tab, come back a week later, POST the same `runId` and you're back in the same session.
|
|
20
|
-
- **Efficient resource use.** Active sandboxes cost money; hibernated workflows cost nothing. The workflow races a command hook against a `sleep()` timer
|
|
21
|
-
- **Beyond the 5-hour hard cap.** Every Vercel Sandbox has a maximum lifetime. The workflow tracks that deadline and proactively
|
|
24
|
+
- **Efficient resource use.** Active sandboxes cost money; hibernated workflows cost nothing. The workflow races a command hook against a `sleep()` timer. When idle, it calls `sandbox.snapshot()` (which also stops the VM) and waits indefinitely. When the next command arrives, the workflow starts a new sandbox from the snapshot with the filesystem, installed packages, and git history intact.
|
|
25
|
+
- **Beyond the 5-hour hard cap.** Every Vercel Sandbox has a maximum lifetime. The workflow tracks that deadline and proactively creates a snapshot and replacement before the cap, so the logical session outlives any one VM. This provides an effectively unbounded session on time-bounded infrastructure.
|
|
22
26
|
- **Automatic cleanup.** `try/finally` in the workflow guarantees the VM is stopped on failure or destroy.
|
|
23
27
|
|
|
24
28
|
<Callout type="info">
|
|
25
29
|
An effectively unbounded sandbox session is still one workflow run, so it stays on the deployment that started it. If the controller or agent code should upgrade over time, use an explicit version boundary and pass the serialized state or stream handles forward. See [Versioning](/docs/foundations/versioning).
|
|
26
30
|
</Callout>
|
|
27
31
|
|
|
28
|
-
## Use
|
|
32
|
+
## Use case: coding agents
|
|
29
33
|
|
|
30
|
-
This is the pattern [Open Agents](https://open-agents.dev/) uses to spawn coding agents that run "infinitely in the cloud." Each agent session gets its own sandbox
|
|
34
|
+
This is the pattern [Open Agents](https://open-agents.dev/) uses to spawn coding agents that run "infinitely in the cloud." Each agent session gets its own sandbox (full filesystem, network, and runtime access), and the durable workflow keeps the agent loop resumable across restarts, auto-hibernates when the user walks away, and reconnects instantly when they return.
|
|
31
35
|
|
|
32
36
|
Most coding-agent workloads look like this:
|
|
33
37
|
|
|
34
|
-
-
|
|
35
|
-
-
|
|
36
|
-
-
|
|
38
|
+
- The user sends a task, and the agent plans, reads files, runs shell commands, and commits.
|
|
39
|
+
- If the user leaves mid-run, the agent continues and eventually waits for input.
|
|
40
|
+
- When the user returns, the same branch, filesystem, and conversation history remain available.
|
|
37
41
|
|
|
38
|
-
Without durable workflows you'd need a separate state store for the agent loop, a
|
|
42
|
+
Without durable workflows, you'd need a separate state store for the agent loop, a job queue for retries, a scheduler for idle cleanup, and custom reconnection logic. The pattern below keeps the workflow controller in one file.
|
|
39
43
|
|
|
40
|
-
## Quickstart:
|
|
44
|
+
## Quickstart: one-shot pipeline
|
|
41
45
|
|
|
42
46
|
Before the full session pattern, the simplest shape. Each sandbox method is an implicit step, so the event log records every command and the workflow replays from the last completed call on restart.
|
|
43
47
|
|
|
@@ -70,17 +74,17 @@ export async function sandboxPipeline(input: { commands: string[] }) {
|
|
|
70
74
|
}
|
|
71
75
|
```
|
|
72
76
|
|
|
73
|
-
## Session
|
|
77
|
+
## Session pattern: persistent sandbox beyond the hard cap
|
|
74
78
|
|
|
75
79
|
One workflow run owns a sandbox for its whole lifetime. The workflow's loop does two jobs simultaneously:
|
|
76
80
|
|
|
77
|
-
1. **Command pipeline
|
|
78
|
-
2. **Sandbox lifecycle
|
|
81
|
+
1. **Command pipeline**: await a hook, run the next user command, stream output, loop.
|
|
82
|
+
2. **Sandbox lifecycle**: race the hook against a `sleep()` timer armed for whichever comes first: the idle deadline or the sandbox's refresh deadline (a safety margin before its hard cap).
|
|
79
83
|
|
|
80
84
|
When the timer wins:
|
|
81
85
|
|
|
82
|
-
- **Idle
|
|
83
|
-
- **Near sandbox hard cap
|
|
86
|
+
- **Idle**: Call `sandbox.snapshot()` and wait indefinitely for the next command. The workflow uses no compute while suspended.
|
|
87
|
+
- **Near the sandbox hard cap**: Call `sandbox.snapshot()` and immediately create a new sandbox from the snapshot. The session remains continuous while the underlying VM rotates.
|
|
84
88
|
|
|
85
89
|
The only way out is an explicit `/destroy` command.
|
|
86
90
|
|
|
@@ -166,7 +170,7 @@ export async function sandboxSessionWorkflow() {
|
|
|
166
170
|
"use workflow";
|
|
167
171
|
|
|
168
172
|
const { workflowRunId } = getWorkflowMetadata();
|
|
169
|
-
// Create the hook once, outside the loop
|
|
173
|
+
// Create the hook once, outside the loop: reusing the same token from inside // [!code highlight]
|
|
170
174
|
// the loop would throw HookConflictError. // [!code highlight]
|
|
171
175
|
const hook = commandHook.create({ token: workflowRunId });
|
|
172
176
|
|
|
@@ -201,8 +205,8 @@ export async function sandboxSessionWorkflow() {
|
|
|
201
205
|
try {
|
|
202
206
|
while (!destroyed) {
|
|
203
207
|
if (hibernated && snapshot) {
|
|
204
|
-
// While hibernated, the VM is already stopped.
|
|
205
|
-
// command
|
|
208
|
+
// While hibernated, the VM is already stopped. Wait for the next
|
|
209
|
+
// command: no idle timer, no compute cost.
|
|
206
210
|
const payload = await hook;
|
|
207
211
|
if (payload.command === "/destroy") { destroyed = true; break; }
|
|
208
212
|
|
|
@@ -227,7 +231,7 @@ export async function sandboxSessionWorkflow() {
|
|
|
227
231
|
continue;
|
|
228
232
|
}
|
|
229
233
|
|
|
230
|
-
// Active
|
|
234
|
+
// Active. Wake at whichever comes first: idle-deadline or refresh-deadline.
|
|
231
235
|
const idleDeadline = lastActivityAt + HIBERNATE_AFTER_MS;
|
|
232
236
|
const refreshDeadline = sandboxExpiresAt - REFRESH_SAFETY_MS;
|
|
233
237
|
const wakeAt = Math.min(idleDeadline, refreshDeadline);
|
|
@@ -242,7 +246,7 @@ export async function sandboxSessionWorkflow() {
|
|
|
242
246
|
const nearExpiry = Date.now() >= refreshDeadline;
|
|
243
247
|
|
|
244
248
|
if (nearExpiry) {
|
|
245
|
-
// Proactive refresh
|
|
249
|
+
// Proactive refresh: snapshot and immediately recreate so the
|
|
246
250
|
// session outlives the sandbox hard cap.
|
|
247
251
|
await emit({ type: "status", state: "refreshing", at: Date.now() });
|
|
248
252
|
const snap = await sandbox.snapshot(); // [!code highlight]
|
|
@@ -259,7 +263,7 @@ export async function sandboxSessionWorkflow() {
|
|
|
259
263
|
});
|
|
260
264
|
lastActivityAt = Date.now();
|
|
261
265
|
} else {
|
|
262
|
-
// Idle
|
|
266
|
+
// Idle: snapshot and hibernate indefinitely.
|
|
263
267
|
await emit({ type: "status", state: "hibernating", at: Date.now() });
|
|
264
268
|
snapshot = await sandbox.snapshot(); // [!code highlight]
|
|
265
269
|
hibernated = true;
|
|
@@ -298,7 +302,11 @@ export async function sandboxSessionWorkflow() {
|
|
|
298
302
|
|
|
299
303
|
<Tab value="API Routes">
|
|
300
304
|
|
|
301
|
-
Two endpoints. `/start` accepts an optional `{ runId }
|
|
305
|
+
Two endpoints manage the session. `/start` accepts an optional `{ runId }`: if the run still exists, it replays the event log from index 0 so a returning client fully rehydrates. `/command` resumes the hook and returns immediately; command output lands on the `/start` stream.
|
|
306
|
+
|
|
307
|
+
<Callout type="info">
|
|
308
|
+
This example starts a fresh sandbox session when no `runId` is provided. If your product needs one sandbox session per user, project, or task, use a deterministic hook token derived from that session key and route retries through the active hook. See [Run idempotency](/docs/foundations/idempotency#run-idempotency).
|
|
309
|
+
</Callout>
|
|
302
310
|
|
|
303
311
|
```typescript title="app/api/sandbox/start/route.ts" lineNumbers
|
|
304
312
|
import { start, getRun } from "workflow/api";
|
|
@@ -326,7 +334,7 @@ export async function POST(req: Request) {
|
|
|
326
334
|
},
|
|
327
335
|
});
|
|
328
336
|
}
|
|
329
|
-
// Stale runId
|
|
337
|
+
// Stale runId: fall through to start fresh.
|
|
330
338
|
}
|
|
331
339
|
|
|
332
340
|
const run = await start(sandboxSessionWorkflow, []);
|
|
@@ -375,7 +383,7 @@ export async function POST(req: Request) {
|
|
|
375
383
|
|
|
376
384
|
<Tab value="Client">
|
|
377
385
|
|
|
378
|
-
On mount,
|
|
386
|
+
On mount, reconnect to the existing run if `localStorage` contains a `runId`. Otherwise, start a new run. Send commands to `/command` with POST requests. Output arrives on the `/start` stream.
|
|
379
387
|
|
|
380
388
|
```tsx title="components/sandbox-runner.tsx" lineNumbers
|
|
381
389
|
"use client";
|
|
@@ -468,22 +476,22 @@ export function SandboxRunner() {
|
|
|
468
476
|
|
|
469
477
|
</Tabs>
|
|
470
478
|
|
|
471
|
-
## How
|
|
479
|
+
## How it works
|
|
472
480
|
|
|
473
|
-
1. **One workflow
|
|
474
|
-
2. **
|
|
475
|
-
3. **Two timer branches
|
|
476
|
-
4. **Proactive refresh
|
|
477
|
-
5. **`sandbox.snapshot()` stops the VM
|
|
478
|
-
6. **Resume
|
|
479
|
-
7. **Reconnect by runId
|
|
480
|
-
8. **Exit only on `/destroy
|
|
481
|
+
1. **One workflow represents one session**: The workflow owns a sandbox for its entire lifetime. The `runId` is the only state the client has to remember.
|
|
482
|
+
2. **Create the hook once**: Call `commandHook.create({ token: workflowRunId })` outside the loop. Creating it twice with the same token throws `HookConflictError`.
|
|
483
|
+
3. **Two timer branches control wake-up**: The active-state race wakes on the earlier of `idleDeadline` and `refreshDeadline`. The hibernated state awaits the hook alone, with no timer or compute.
|
|
484
|
+
4. **Proactive refresh replaces the sandbox**: When `refreshDeadline = sandboxExpiresAt - REFRESH_SAFETY_MS` arrives, the workflow takes a snapshot and immediately creates a new sandbox from it. This rolls over the hard cap without user intervention.
|
|
485
|
+
5. **`sandbox.snapshot()` stops the VM**: The snapshot process stops the VM, so don't call `stop()` separately.
|
|
486
|
+
6. **Resume creates a new sandbox**: `Sandbox.create({ source: { type: "snapshot", snapshotId } })` creates a new VM from the snapshot. The new sandbox has a different `sandboxId`; the filesystem, installed packages, and git history are preserved.
|
|
487
|
+
7. **Reconnect by `runId`**: `getRun(runId).getReadable({ startIndex: 0 })` replays the durable event log to a returning client, which rebuilds UI state from the replay.
|
|
488
|
+
8. **Exit only on `/destroy`**: The workflow loop has no hard deadline of its own. Individual sandboxes time out, but the session doesn't.
|
|
481
489
|
|
|
482
490
|
## Pitfalls
|
|
483
491
|
|
|
484
492
|
### `sandbox.stop()` is terminal
|
|
485
493
|
|
|
486
|
-
A stopped sandbox cannot be restarted
|
|
494
|
+
A stopped sandbox cannot be restarted: you have to create a new one. Hibernation is only possible via `snapshot()` + new-sandbox-from-snapshot. Don't try to "pause" an active sandbox with `stop()` and resume later.
|
|
487
495
|
|
|
488
496
|
### `snapshot()` already stops the VM
|
|
489
497
|
|
|
@@ -495,7 +503,7 @@ Both `resuming` (idle → command) and `refreshing` (near-hard-cap rotation) cre
|
|
|
495
503
|
|
|
496
504
|
### Keep the refresh margin generous
|
|
497
505
|
|
|
498
|
-
`snapshot()`
|
|
506
|
+
`snapshot()` followed by `Sandbox.create({ source })` takes time, typically tens of seconds. If `REFRESH_SAFETY_MS` is too small, the old sandbox hits its hard cap mid-snapshot. Leave at least 60–90 seconds; the example uses 5 minutes.
|
|
499
507
|
|
|
500
508
|
### Don't call `writable.close()` inside a workflow function
|
|
501
509
|
|
|
@@ -505,16 +513,21 @@ Stream closure must happen inside a `"use step"` function. Calling `writable.clo
|
|
|
505
513
|
|
|
506
514
|
Clients can hold `runId`s from long-gone workflow runs (localStorage, back button, server restart). Gate the reconnect path on `run.exists` and fall through to starting fresh. On `hook.resume`, catch `not found` / `expired` and return 410 so the client clears its state.
|
|
507
515
|
|
|
516
|
+
### Decide whether `/start` should be idempotent
|
|
517
|
+
|
|
518
|
+
The sample treats a missing or stale `runId` as a request for a new session. For one-session-per-resource behavior, use a durable resource key, such as `projectId` or `taskId`, to claim or retrieve the run before starting a new one.
|
|
519
|
+
|
|
508
520
|
### Keep the hook outside the loop
|
|
509
521
|
|
|
510
522
|
Each iteration's `hook.then(...)` attaches a listener to the same hook instance. Creating a new hook per iteration with the same token throws `HookConflictError`. One hook, one token (`workflowRunId`), reused every iteration.
|
|
511
523
|
|
|
512
524
|
## Key APIs
|
|
513
525
|
|
|
514
|
-
- [`Sandbox.create`](https://vercel.com/docs/sandbox)
|
|
515
|
-
- [`sandbox.runCommand`](https://vercel.com/docs/sandbox)
|
|
516
|
-
- [`sandbox.snapshot`](https://vercel.com/docs/sandbox)
|
|
517
|
-
- [`defineHook()`](/docs/api-reference/workflow/define-hook)
|
|
518
|
-
- [`sleep()`](/docs/api-reference/workflow/sleep)
|
|
519
|
-
- [`getRun()`](/docs/api-reference/workflow-api/get-run)
|
|
520
|
-
- [`getWritable()`](/docs/api-reference/workflow/get-writable)
|
|
526
|
+
- [`Sandbox.create`](https://vercel.com/docs/sandbox): provision a VM (runtime, source, timeout)
|
|
527
|
+
- [`sandbox.runCommand`](https://vercel.com/docs/sandbox): execute a command; implicit step
|
|
528
|
+
- [`sandbox.snapshot`](https://vercel.com/docs/sandbox): save state and stop the VM; returns `Snapshot`
|
|
529
|
+
- [`defineHook()`](/docs/api-reference/workflow/define-hook): suspension point for user commands
|
|
530
|
+
- [`sleep()`](/docs/api-reference/workflow/sleep): durable timer that powers both idle hibernation and proactive refresh
|
|
531
|
+
- [`getRun()`](/docs/api-reference/workflow-api/get-run): look up a run and replay its event log for reconnection
|
|
532
|
+
- [`getWritable()`](/docs/api-reference/workflow/get-writable): resumable newline-delimited JSON (NDJSON) event stream
|
|
533
|
+
- [Idempotency](/docs/foundations/idempotency): choose when `/start` should reuse an existing run
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Deploying
|
|
3
|
+
description: Deploy workflows locally, on Vercel, or anywhere using pluggable World adapters.
|
|
4
|
+
type: overview
|
|
5
|
+
summary: Learn how to deploy workflows to different environments using World adapters.
|
|
6
|
+
related:
|
|
7
|
+
- /worlds/local
|
|
8
|
+
- /worlds/postgres
|
|
9
|
+
- /worlds/vercel
|
|
10
|
+
- /worlds/building-a-world
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
The same workflow code can run locally during development, on Vercel with zero configuration, or on any infrastructure using **Worlds**, which are pluggable adapters that handle storage, queuing, and communication.
|
|
14
|
+
|
|
15
|
+
## Local development
|
|
16
|
+
|
|
17
|
+
During local development, workflows use the **Local World** without configuration. The Local World stores workflow data in a `.workflow-data/` directory and processes steps synchronously for development and testing.
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
# Run your dev server to use workflows locally
|
|
21
|
+
npm run dev
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
You can inspect local workflow data using the CLI:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npx workflow inspect runs
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
<Callout>
|
|
31
|
+
Learn more about the [Local World](/worlds/local) configuration and internals.
|
|
32
|
+
</Callout>
|
|
33
|
+
|
|
34
|
+
## Deploying to Vercel
|
|
35
|
+
|
|
36
|
+
Deploy workflows to production on Vercel without configuration. Workflows deployed to Vercel use the **Vercel World**.
|
|
37
|
+
|
|
38
|
+
The Vercel World provides:
|
|
39
|
+
|
|
40
|
+
- **Durable storage**: Workflow state persists across function invocations
|
|
41
|
+
- **Managed queuing**: The queue processes steps reliably with automatic retries
|
|
42
|
+
- **Automatic scaling**: Workflows scale with your application
|
|
43
|
+
- **Built-in observability**: View workflow runs in the Vercel dashboard
|
|
44
|
+
- **Multi-region**: The region that creates each run also hosts its workflow data, queue, and streams, keeping them close to your users
|
|
45
|
+
|
|
46
|
+
Deploy your application:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
vercel deploy
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
<FluidComputeCallout />
|
|
53
|
+
|
|
54
|
+
<Callout type="warn">
|
|
55
|
+
**Enable access to System Environment Variables before deploying.** Workflow
|
|
56
|
+
recognizes a Vercel deployment by `VERCEL_DEPLOYMENT_ID`, which Vercel exposes
|
|
57
|
+
to your build and your functions only when the **Enable access to System
|
|
58
|
+
Environment Variables** checkbox is selected under **Settings**, then
|
|
59
|
+
**Environment Variables**, in your project. With it cleared, the deployment
|
|
60
|
+
falls back to the Local World and every run fails on the read-only
|
|
61
|
+
filesystem. See
|
|
62
|
+
[System environment variables](/worlds/vercel#system-environment-variables).
|
|
63
|
+
</Callout>
|
|
64
|
+
|
|
65
|
+
<Callout>
|
|
66
|
+
Learn more about the [Vercel World](/worlds/vercel) and its capabilities, including [multi-region](/worlds/vercel#multi-region).
|
|
67
|
+
</Callout>
|
|
68
|
+
|
|
69
|
+
## Self-hosting & other providers
|
|
70
|
+
|
|
71
|
+
For self-hosting or deploying to other cloud providers, you can use community-maintained Worlds or build your own.
|
|
72
|
+
|
|
73
|
+
<Cards>
|
|
74
|
+
<Card title="Explore Worlds" href="/worlds">
|
|
75
|
+
Browse official and community World implementations with compatibility status and performance benchmarks.
|
|
76
|
+
</Card>
|
|
77
|
+
<Card title="Build Your Own" href="/worlds/building-a-world">
|
|
78
|
+
Learn how to implement a custom World for your infrastructure.
|
|
79
|
+
</Card>
|
|
80
|
+
</Cards>
|
|
81
|
+
|
|
82
|
+
### Using a third-party World
|
|
83
|
+
|
|
84
|
+
To use a different World implementation, set the `WORKFLOW_TARGET_WORLD` environment variable:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
export WORKFLOW_TARGET_WORLD=@workflow/world-postgres
|
|
88
|
+
# Plus any world-specific configuration
|
|
89
|
+
export DATABASE_URL=postgres://...
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Each World may have its own configuration requirements. Refer to that World's documentation for details.
|
|
93
|
+
|
|
94
|
+
## Observability
|
|
95
|
+
|
|
96
|
+
The [Observability tools](/docs/observability) work with any World backend. By default, they connect to your local environment, but you can configure them to inspect remote deployments:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
# Inspect local workflows
|
|
100
|
+
npx workflow inspect runs
|
|
101
|
+
|
|
102
|
+
# Inspect remote workflows
|
|
103
|
+
npx workflow inspect runs --backend @workflow/world-postgres
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Learn more about [Observability](/docs/observability) tools.
|
|
@@ -11,23 +11,27 @@ related:
|
|
|
11
11
|
- /docs/errors/timeout-in-workflow
|
|
12
12
|
---
|
|
13
13
|
|
|
14
|
+
<CopyPrompt
|
|
15
|
+
text="Replace `AbortSignal.timeout()` inside workflow functions. Find the failing `AbortSignal.timeout(ms)` call in a `"use workflow"` function. Import `sleep` from `workflow`. Create `const controller = new AbortController()`, pass `controller.signal` into the async work that supports cancellation, and start a deterministic timeout with `void sleep("10s").then(() => controller.abort())` or a `Promise.race` between the work and `sleep(...)`. Keep actual fetch/SDK side effects inside `"use step"` helpers when they need Node.js APIs. Treat intentional aborts as non-retryable if appropriate by throwing `FatalError`. Verify the operation succeeds before the timeout, aborts after the timeout, and replays without abort-signal-timeout-in-workflow."
|
|
16
|
+
/>
|
|
17
|
+
|
|
14
18
|
## Error
|
|
15
19
|
|
|
16
|
-
```
|
|
20
|
+
```text
|
|
17
21
|
AbortSignal.timeout() is not supported in workflow functions.
|
|
18
22
|
Use sleep() with an AbortController instead.
|
|
19
23
|
```
|
|
20
24
|
|
|
21
|
-
## Why
|
|
25
|
+
## Why this happens
|
|
22
26
|
|
|
23
|
-
`AbortSignal.timeout()` creates a signal that aborts after a real-time delay using an internal timer. Workflow functions must be [deterministic](/docs/foundations/workflows-and-steps) to support replay
|
|
27
|
+
`AbortSignal.timeout()` creates a signal that aborts after a real-time delay using an internal timer. Workflow functions must be [deterministic](/docs/foundations/workflows-and-steps) to support replay: they run the same code multiple times during the workflow's lifecycle, using the [event log](/docs/how-it-works/event-sourcing) to resume execution to the correct point.
|
|
24
28
|
|
|
25
29
|
Real-time timers break this determinism because:
|
|
26
|
-
- On the first execution, the timer might fire after 10 seconds
|
|
27
|
-
- On replay, the timer would fire again, but the event log may have already advanced past that point
|
|
28
|
-
- The timer's behavior depends on wall-clock time, which varies between executions
|
|
30
|
+
- On the first execution, the timer might fire after 10 seconds.
|
|
31
|
+
- On replay, the timer would fire again, but the event log may have already advanced past that point.
|
|
32
|
+
- The timer's behavior depends on wall-clock time, which varies between executions.
|
|
29
33
|
|
|
30
|
-
## How to
|
|
34
|
+
## How to fix
|
|
31
35
|
|
|
32
36
|
Use [`sleep()`](/docs/api-reference/workflow/sleep) with an `AbortController` to create a deterministic timeout that cancels in-flight work:
|
|
33
37
|
|
|
@@ -66,7 +70,7 @@ async function fetchData(signal: AbortSignal) {
|
|
|
66
70
|
}
|
|
67
71
|
```
|
|
68
72
|
|
|
69
|
-
The `sleep()`
|
|
73
|
+
The `sleep()` and `AbortController` pattern is the durable equivalent of `AbortSignal.timeout()`. The sleep is recorded in the event log, so it replays deterministically. If `fetchData` finishes within 10 seconds, you get the response. Otherwise, the timer calls `controller.abort()`, `fetch` rejects with an `AbortError`, and the step's failure propagates to the workflow as a `FatalError` (no retries because the abort is intentional cancellation).
|
|
70
74
|
|
|
71
75
|
<Callout type="info">
|
|
72
76
|
`AbortSignal.timeout()` works normally inside step functions, since steps have full Node.js runtime access and are not replayed.
|
|
@@ -74,7 +78,7 @@ The `sleep()` + `AbortController` pattern is the durable equivalent of `AbortSig
|
|
|
74
78
|
|
|
75
79
|
## Related
|
|
76
80
|
|
|
77
|
-
- [Cancellation](/docs/foundations/cancellation)
|
|
78
|
-
- [`sleep()` API Reference](/docs/api-reference/workflow/sleep)
|
|
79
|
-
- [Workflows and Steps](/docs/foundations/workflows-and-steps)
|
|
80
|
-
- [`setTimeout` in Workflow](/docs/errors/timeout-in-workflow)
|
|
81
|
+
- [Cancellation](/docs/foundations/cancellation): Patterns for canceling in-flight work
|
|
82
|
+
- [`sleep()` API Reference](/docs/api-reference/workflow/sleep): Durable sleep primitive
|
|
83
|
+
- [Workflows and Steps](/docs/foundations/workflows-and-steps): Why workflow functions must be deterministic
|
|
84
|
+
- [`setTimeout` in Workflow](/docs/errors/timeout-in-workflow): Similar restriction on `setTimeout`
|