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
|
@@ -11,9 +11,9 @@ related:
|
|
|
11
11
|
- /docs/ai/human-in-the-loop
|
|
12
12
|
---
|
|
13
13
|
|
|
14
|
-
Hooks
|
|
14
|
+
Hooks pause workflow execution and resume it later with external data. Workflows can wait for external events, user interactions (also known as "human in the loop"), or HTTP requests.
|
|
15
15
|
|
|
16
|
-
## Understanding
|
|
16
|
+
## Understanding hooks
|
|
17
17
|
|
|
18
18
|
At their core, **Hooks** are a low-level primitive that allows you to pause a workflow and resume it later with arbitrary [serializable data](/docs/foundations/serialization). Think of them as suspension points in your workflow where you're waiting for external input.
|
|
19
19
|
|
|
@@ -23,9 +23,9 @@ When you create a hook, it generates a unique token that external systems can us
|
|
|
23
23
|
- Receiving data from an external system or service
|
|
24
24
|
- Implementing event-driven workflows that react to multiple events over time
|
|
25
25
|
|
|
26
|
-
### Creating
|
|
26
|
+
### Creating your first hook
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
This workflow creates a hook and waits for external data:
|
|
29
29
|
|
|
30
30
|
```typescript lineNumbers
|
|
31
31
|
import { createHook } from "workflow";
|
|
@@ -59,7 +59,7 @@ We recommend using the `using` keyword which implements the [TC39 Explicit Resou
|
|
|
59
59
|
See the full API reference for [`createHook()`](/docs/api-reference/workflow/create-hook) for all available options.
|
|
60
60
|
</Callout>
|
|
61
61
|
|
|
62
|
-
### Resuming a
|
|
62
|
+
### Resuming a hook
|
|
63
63
|
|
|
64
64
|
To send data to a waiting workflow, use [`resumeHook()`](/docs/api-reference/workflow-api/resume-hook) from an API route, server action, or any other external context:
|
|
65
65
|
|
|
@@ -85,9 +85,38 @@ The key points:
|
|
|
85
85
|
- You need the hook's `token` to resume it
|
|
86
86
|
- The workflow will resume execution right where it left off
|
|
87
87
|
|
|
88
|
-
###
|
|
88
|
+
### Checking for token conflicts
|
|
89
89
|
|
|
90
|
-
|
|
90
|
+
Sometimes you need to know that a hook token has been claimed, but you do not want to wait for external data yet. Await `hook.getConflict()` for that:
|
|
91
|
+
|
|
92
|
+
```typescript lineNumbers
|
|
93
|
+
import { createHook } from "workflow";
|
|
94
|
+
|
|
95
|
+
declare function processOrder(orderId: string): Promise<void>; // @setup
|
|
96
|
+
|
|
97
|
+
export async function orderWorkflow(orderId: string) {
|
|
98
|
+
"use workflow";
|
|
99
|
+
|
|
100
|
+
using hook = createHook({
|
|
101
|
+
token: `order:${orderId}`
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const conflict = await hook.getConflict(); // [!code highlight]
|
|
105
|
+
if (conflict) { // [!code highlight]
|
|
106
|
+
// Another active run already owns this token.
|
|
107
|
+
return { dedupedTo: conflict.runId };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// The hook token is registered and reserved here.
|
|
111
|
+
await processOrder(orderId);
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Calling `createHook()` on its own does not register the hook; registration is only committed when the workflow suspends. Awaiting `hook.getConflict()` suspends the workflow to commit the hook registration, then resolves with `null` once the hook is registered and ready to receive payloads, or with a `Run` handle for the run that owns the token (see [`HookConflictError`](/docs/errors/hook-conflict)). For `hook_conflict` events persisted by older worlds that did not record the owning run's ID, `getConflict()` rejects with `HookConflictError` instead of resolving with an incomplete handle. The conflicting run's accessors are durable steps, so the workflow can inspect `await conflict.status`, wait on `await conflict.returnValue`, or cancel the owner with `await conflict.cancel()`. See [Run idempotency](/docs/foundations/idempotency#run-idempotency) for these strategies.
|
|
116
|
+
|
|
117
|
+
### Custom tokens for deterministic hooks
|
|
118
|
+
|
|
119
|
+
By default, hooks generate their own token. However, you often want to use a **custom token** that external systems can reconstruct. This is especially useful for long-running workflows where the same workflow instance should handle multiple events.
|
|
91
120
|
|
|
92
121
|
For example, imagine a Slack bot where each channel should have its own workflow instance:
|
|
93
122
|
|
|
@@ -139,9 +168,9 @@ export async function POST(request: Request) {
|
|
|
139
168
|
}
|
|
140
169
|
```
|
|
141
170
|
|
|
142
|
-
### Receiving
|
|
171
|
+
### Receiving multiple events
|
|
143
172
|
|
|
144
|
-
Hooks are _reusable_
|
|
173
|
+
Hooks are _reusable_. They implement `AsyncIterable`, which means you can use `for await...of` to receive multiple events over time:
|
|
145
174
|
|
|
146
175
|
```typescript lineNumbers
|
|
147
176
|
import { createHook } from "workflow";
|
|
@@ -169,7 +198,7 @@ export async function dataCollectionWorkflow() {
|
|
|
169
198
|
|
|
170
199
|
Each time you call `resumeHook()` with the same token, the loop receives another value.
|
|
171
200
|
|
|
172
|
-
### Disposing
|
|
201
|
+
### Disposing hooks early
|
|
173
202
|
|
|
174
203
|
When a workflow ends, hooks are automatically disposed. However, you may want to release a hook token early so another workflow can use it while your workflow continues running. Use a block scope with `using` to control when disposal happens:
|
|
175
204
|
|
|
@@ -211,9 +240,9 @@ hook.dispose(); // Manually release the token
|
|
|
211
240
|
After disposal, the hook will no longer receive events and the async iterator will stop yielding values.
|
|
212
241
|
</Callout>
|
|
213
242
|
|
|
214
|
-
## Understanding
|
|
243
|
+
## Understanding webhooks
|
|
215
244
|
|
|
216
|
-
|
|
245
|
+
Hooks require you to manually handle HTTP requests and route them to workflows. **Webhooks** provide a higher-level abstraction built on top of hooks that:
|
|
217
246
|
|
|
218
247
|
1. Automatically serializes the entire HTTP [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) object
|
|
219
248
|
2. Provides an automatically addressable `url` property pointing to the generated webhook endpoint
|
|
@@ -222,16 +251,16 @@ While hooks are powerful, they require you to manually handle HTTP requests and
|
|
|
222
251
|
When using Workflow SDK, webhooks are automatically wired up at `/.well-known/workflow/v1/webhook/:token` without any additional setup.
|
|
223
252
|
|
|
224
253
|
<Callout type="warn">
|
|
225
|
-
`createWebhook()` exposes a public route at `/.well-known/workflow/v1/webhook/:token`, and the token in that URL is the only authorization performed for incoming requests. This is convenient for prototypes
|
|
254
|
+
`createWebhook()` exposes a public route at `/.well-known/workflow/v1/webhook/:token`, and the token in that URL is the only authorization performed for incoming requests. This is convenient for prototypes because you can share the webhook URL (endpoint) without creating another route, but if you need stronger security, prefer [`createHook()`](/docs/api-reference/workflow/create-hook) behind your own route and authorize the request before calling [`resumeHook()`](/docs/api-reference/workflow-api/resume-hook) to avoid unauthenticated workflow resumptions.
|
|
226
255
|
</Callout>
|
|
227
256
|
|
|
228
257
|
<Callout type="info">
|
|
229
258
|
See the full API reference for [`createWebhook()`](/docs/api-reference/workflow/create-webhook) for all available options.
|
|
230
259
|
</Callout>
|
|
231
260
|
|
|
232
|
-
### Creating
|
|
261
|
+
### Creating your first webhook
|
|
233
262
|
|
|
234
|
-
Here's a
|
|
263
|
+
Here's a webhook that receives HTTP requests. Like hooks, webhooks support the `using` keyword for automatic cleanup:
|
|
235
264
|
|
|
236
265
|
```typescript lineNumbers
|
|
237
266
|
import { createWebhook } from "workflow";
|
|
@@ -255,13 +284,13 @@ export async function webhookWorkflow() {
|
|
|
255
284
|
}
|
|
256
285
|
```
|
|
257
286
|
|
|
258
|
-
The webhook will automatically respond with a `202 Accepted` status by default. External systems can
|
|
287
|
+
The webhook will automatically respond with a `202 Accepted` status by default. External systems can make an HTTP request to the `webhook.url` to resume your workflow.
|
|
259
288
|
|
|
260
|
-
### Sending
|
|
289
|
+
### Sending custom responses
|
|
261
290
|
|
|
262
291
|
Webhooks provide two ways to send custom HTTP responses: **static responses** and **dynamic responses**.
|
|
263
292
|
|
|
264
|
-
#### Static
|
|
293
|
+
#### Static responses
|
|
265
294
|
|
|
266
295
|
Use the `respondWith` option to provide a static response that will be sent automatically for every request:
|
|
267
296
|
|
|
@@ -290,7 +319,7 @@ async function processData(data: any) {
|
|
|
290
319
|
}
|
|
291
320
|
```
|
|
292
321
|
|
|
293
|
-
#### Dynamic
|
|
322
|
+
#### Dynamic responses (manual mode)
|
|
294
323
|
|
|
295
324
|
For dynamic responses based on the request content, set `respondWith: "manual"` and call the `respondWith()` method on the request:
|
|
296
325
|
|
|
@@ -336,7 +365,7 @@ export async function webhookWithDynamicResponse() {
|
|
|
336
365
|
When using `respondWith: "manual"`, the `respondWith()` method **must** be called from within a step function due to serialization requirements. This requirement may be removed in the future.
|
|
337
366
|
</Callout>
|
|
338
367
|
|
|
339
|
-
### Handling
|
|
368
|
+
### Handling multiple webhook requests
|
|
340
369
|
|
|
341
370
|
Like hooks, webhooks support iteration:
|
|
342
371
|
|
|
@@ -376,7 +405,7 @@ export async function eventCollectorWorkflow() {
|
|
|
376
405
|
}
|
|
377
406
|
```
|
|
378
407
|
|
|
379
|
-
## Hooks vs.
|
|
408
|
+
## Hooks vs. webhooks: when to use each
|
|
380
409
|
|
|
381
410
|
| Feature | Hooks | Webhooks |
|
|
382
411
|
|---------|-------|----------|
|
|
@@ -386,19 +415,19 @@ export async function eventCollectorWorkflow() {
|
|
|
386
415
|
| **Use Case** | Custom integrations, type-safe payloads | HTTP webhooks, standard REST APIs |
|
|
387
416
|
| **Resuming** | [`resumeHook()`](/docs/api-reference/workflow-api/resume-hook) | Automatic via HTTP, or [`resumeWebhook()`](/docs/api-reference/workflow-api/resume-webhook) |
|
|
388
417
|
|
|
389
|
-
**Use
|
|
418
|
+
**Use hooks when:**
|
|
390
419
|
- You need full control over the payload structure
|
|
391
420
|
- You're integrating with custom event sources
|
|
392
421
|
- You want strong TypeScript typing with [`defineHook()`](/docs/api-reference/workflow/define-hook)
|
|
393
422
|
|
|
394
|
-
**Use
|
|
423
|
+
**Use webhooks when:**
|
|
395
424
|
- You're receiving HTTP requests from external services
|
|
396
425
|
- You need to send HTTP responses back to the caller
|
|
397
426
|
- You want automatic URL routing without writing API handlers
|
|
398
427
|
|
|
399
|
-
## Advanced
|
|
428
|
+
## Advanced patterns
|
|
400
429
|
|
|
401
|
-
### Type-
|
|
430
|
+
### Type-safe hooks with `defineHook()`
|
|
402
431
|
|
|
403
432
|
The [`defineHook()`](/docs/api-reference/workflow/define-hook) helper provides type safety and runtime validation between creating and resuming hooks using [Standard Schema v1](https://standardschema.dev). Use any compliant validator like Zod or Valibot:
|
|
404
433
|
|
|
@@ -447,25 +476,25 @@ export async function POST(request: Request) {
|
|
|
447
476
|
|
|
448
477
|
This pattern is especially valuable in larger applications where the workflow and API code are in separate files, providing both compile-time type safety and runtime validation.
|
|
449
478
|
|
|
450
|
-
## Best
|
|
479
|
+
## Best practices
|
|
451
480
|
|
|
452
|
-
### Token
|
|
481
|
+
### Token design
|
|
453
482
|
|
|
454
|
-
Custom tokens are available for `createHook()` with server-side `resumeHook()` only. Webhooks (`createWebhook()`) always
|
|
483
|
+
Custom tokens are available for `createHook()` with server-side `resumeHook()` only. Webhooks (`createWebhook()`) always generate their own unique tokens. A generated token is not trivial to guess, but it is not a strong security contract either, so anyone who obtains the URL can invoke an unintended webhook resumption. To prevent unauthenticated run resumptions entirely, prefer a **hook** over the **webhook** convenience and implement your own authentication on the route that calls `resumeHook()`.
|
|
455
484
|
|
|
456
485
|
When using custom tokens with `createHook()`:
|
|
457
486
|
|
|
458
|
-
- **Make them deterministic**: Base them on data the external system can reconstruct
|
|
459
|
-
- **Use namespacing**: Prefix tokens to avoid conflicts
|
|
460
|
-
- **Include routing information**: Ensure the token contains enough information to identify the correct workflow instance
|
|
487
|
+
- **Make them deterministic**: Base them on data the external system can reconstruct, such as channel IDs or user IDs.
|
|
488
|
+
- **Use namespacing**: Prefix tokens to avoid conflicts, such as `slack:${channelId}` or `github:${repoId}`.
|
|
489
|
+
- **Include routing information**: Ensure the token contains enough information to identify the correct workflow instance.
|
|
461
490
|
|
|
462
|
-
### Response
|
|
491
|
+
### Response handling in webhooks
|
|
463
492
|
|
|
464
|
-
- Use **static responses** (`respondWith: Response`) for
|
|
493
|
+
- Use **static responses** (`respondWith: Response`) for acknowledgments
|
|
465
494
|
- Use **manual mode** (`respondWith: "manual"`) when responses depend on request processing
|
|
466
495
|
- Remember that `respondWith()` must be called from within a step function
|
|
467
496
|
|
|
468
|
-
### Iterating
|
|
497
|
+
### Iterating over events
|
|
469
498
|
|
|
470
499
|
Both hooks and webhooks support iteration, making them perfect for long-running event loops:
|
|
471
500
|
|
|
@@ -484,7 +513,7 @@ for await (const event of hook) {
|
|
|
484
513
|
|
|
485
514
|
This pattern allows a single workflow instance to handle multiple events over time, maintaining state between events.
|
|
486
515
|
|
|
487
|
-
## Related
|
|
516
|
+
## Related documentation
|
|
488
517
|
|
|
489
518
|
- [Serialization](/docs/foundations/serialization) - Understanding what data can be passed through hooks
|
|
490
519
|
- [`createHook()` API Reference](/docs/api-reference/workflow/create-hook)
|
|
@@ -1,23 +1,27 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Idempotency
|
|
3
|
-
description:
|
|
3
|
+
description: Make step retries safe and coordinate duplicate workflow starts with hook tokens.
|
|
4
4
|
type: conceptual
|
|
5
|
-
summary:
|
|
5
|
+
summary: Use step IDs for retry-safe external calls, and route duplicate workflow-start requests through deterministic hook tokens.
|
|
6
6
|
prerequisites:
|
|
7
7
|
- /docs/foundations/workflows-and-steps
|
|
8
8
|
related:
|
|
9
9
|
- /docs/foundations/errors-and-retries
|
|
10
|
+
- /docs/foundations/starting-workflows
|
|
11
|
+
- /docs/foundations/hooks
|
|
10
12
|
---
|
|
11
13
|
|
|
12
|
-
Idempotency is a property of an operation that ensures
|
|
14
|
+
Idempotency is a property of an operation that ensures repeated attempts have the same effect as a single attempt.
|
|
13
15
|
|
|
14
|
-
In
|
|
16
|
+
In Workflow, idempotency shows up in two related places: step idempotency makes external calls safe when a step retries, and run idempotency coordinates duplicate requests that try to start the same workflow.
|
|
17
|
+
|
|
18
|
+
## Step idempotency
|
|
19
|
+
|
|
20
|
+
In distributed systems (calling external APIs), it is not always possible to ensure an operation has only been performed once by seeing if it succeeds.
|
|
15
21
|
Consider a payment API that charges the user $10, but due to network failures, the confirmation response is lost. When the step retries (because the previous attempt was considered a failure), it will charge the user again.
|
|
16
22
|
|
|
17
23
|
To prevent this, many external APIs support idempotency keys. An idempotency key is a unique identifier for an operation that can be used to deduplicate requests.
|
|
18
24
|
|
|
19
|
-
## The core pattern: use the step ID as your idempotency key
|
|
20
|
-
|
|
21
25
|
Every step invocation has a stable `stepId` that stays the same across retries.
|
|
22
26
|
Use it as the idempotency key when calling third-party APIs.
|
|
23
27
|
|
|
@@ -27,7 +31,7 @@ import { getStepMetadata } from "workflow";
|
|
|
27
31
|
async function chargeUser(userId: string, amount: number) {
|
|
28
32
|
"use step";
|
|
29
33
|
|
|
30
|
-
const { stepId } = getStepMetadata();
|
|
34
|
+
const { stepId } = getStepMetadata(); // [!code highlight]
|
|
31
35
|
|
|
32
36
|
// Example: Stripe-style idempotency key
|
|
33
37
|
// This guarantees only one charge is created even if the step retries
|
|
@@ -49,14 +53,242 @@ Why this works:
|
|
|
49
53
|
- **Stable across retries**: `stepId` does not change between attempts.
|
|
50
54
|
- **Globally unique per step**: Fulfills the uniqueness requirement for an idempotency key.
|
|
51
55
|
|
|
52
|
-
##
|
|
56
|
+
## Run idempotency
|
|
57
|
+
|
|
58
|
+
Step idempotency protects side effects **inside** a workflow run. Run idempotency answers a different question: if the same API request is sent twice, should it create one workflow run or two?
|
|
59
|
+
|
|
60
|
+
Because [hooks](/docs/foundations/hooks) already ensure globally unique active tokens, Workflow can use the same mechanism to coordinate duplicate requests while a run is active.
|
|
61
|
+
|
|
62
|
+
Use a hook token as the idempotency key for an active workflow run. Hook tokens are globally unique while they are active: if another run tries to create a hook with the same token, the runtime records a conflict, `hook.getConflict()` resolves with a `Run` handle for the run that owns the token, and the hook rejects with [`HookConflictError`](/docs/errors/hook-conflict) when the workflow awaits or iterates its payload.
|
|
63
|
+
|
|
64
|
+
The token should come from your domain, such as an order ID, invoice ID, import ID, or request ID. Create the hook near the beginning of the workflow and check `await hook.getConflict()` before doing duplicate-sensitive work that depends on owning the active token. Calling `createHook()` alone does not register the hook; awaiting `getConflict()` suspends the workflow to commit the registration.
|
|
65
|
+
|
|
66
|
+
```typescript lineNumbers
|
|
67
|
+
import { createHook } from "workflow";
|
|
68
|
+
|
|
69
|
+
type OrderRequest = { confirmed: boolean };
|
|
70
|
+
type OrderResult =
|
|
71
|
+
| { status: "processed" | "cancelled" }
|
|
72
|
+
| { status: "duplicate"; runId: string };
|
|
73
|
+
declare function chargeOrder(orderId: string): Promise<void>; // @setup
|
|
74
|
+
|
|
75
|
+
export async function processOrder(orderId: string): Promise<OrderResult> {
|
|
76
|
+
"use workflow";
|
|
77
|
+
|
|
78
|
+
using request = createHook<OrderRequest>({ // [!code highlight]
|
|
79
|
+
token: `order:${orderId}`, // [!code highlight]
|
|
80
|
+
}); // [!code highlight]
|
|
81
|
+
|
|
82
|
+
const conflict = await request.getConflict(); // [!code highlight]
|
|
83
|
+
if (conflict) { // [!code highlight]
|
|
84
|
+
// Another active run already owns this order's token. // [!code highlight]
|
|
85
|
+
return { status: "duplicate" as const, runId: conflict.runId }; // [!code highlight]
|
|
86
|
+
} // [!code highlight]
|
|
87
|
+
|
|
88
|
+
const { confirmed } = await request;
|
|
89
|
+
|
|
90
|
+
if (!confirmed) {
|
|
91
|
+
return { status: "cancelled" as const };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
await chargeOrder(orderId);
|
|
95
|
+
return { status: "processed" as const };
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
The runtime creates the hook atomically. At most one hook can own `order:${orderId}`, so duplicate workflow runs converge on one owner. A duplicate run observes `getConflict()` resolving with the owner's `Run` and returns before it reaches `chargeOrder()`. The conflicting run's accessors (`status`, `returnValue`, `cancel()`, …) are durable steps, so the duplicate run can do more than report the owner. See [conflict-handling strategies](#conflict-handling-strategies) below.
|
|
100
|
+
|
|
101
|
+
Outside the workflow, try to resume the hook first. If the hook is not registered yet, start the workflow and retry the resume until the new run creates the hook:
|
|
102
|
+
|
|
103
|
+
```typescript lineNumbers
|
|
104
|
+
import { resumeHook, start } from "workflow/api";
|
|
105
|
+
import { HookNotFoundError } from "workflow/errors";
|
|
106
|
+
import { processOrder } from "./workflows/process-order";
|
|
107
|
+
|
|
108
|
+
type OrderRequest = { confirmed: boolean };
|
|
109
|
+
|
|
110
|
+
async function resumeOrder(token: string, payload: OrderRequest) {
|
|
111
|
+
for (let attempt = 0; attempt < 5; attempt++) {
|
|
112
|
+
try {
|
|
113
|
+
return await resumeHook(token, payload); // [!code highlight]
|
|
114
|
+
} catch (error) {
|
|
115
|
+
if (!HookNotFoundError.is(error)) throw error;
|
|
116
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
throw new Error("Order workflow did not register its hook in time");
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export async function POST(request: Request) {
|
|
124
|
+
const { orderId, confirmed } = await request.json();
|
|
125
|
+
const token = `order:${orderId}`;
|
|
126
|
+
const payload = { confirmed };
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
const hook = await resumeHook(token, payload); // [!code highlight]
|
|
130
|
+
return Response.json({ runId: hook.runId, reused: true });
|
|
131
|
+
} catch (error) {
|
|
132
|
+
if (!HookNotFoundError.is(error)) throw error;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const run = await start(processOrder, [orderId]); // [!code highlight]
|
|
136
|
+
const resumed = await resumeOrder(token, payload);
|
|
137
|
+
|
|
138
|
+
// A concurrent request's run may have won the race between `start()` // [!code highlight]
|
|
139
|
+
// and hook registration. The resume always reaches the actual active // [!code highlight]
|
|
140
|
+
// owner, so compare run IDs instead of waiting for this run to finish. // [!code highlight]
|
|
141
|
+
return Response.json({ // [!code highlight]
|
|
142
|
+
runId: resumed.runId, // [!code highlight]
|
|
143
|
+
reused: resumed.runId !== run.runId, // [!code highlight]
|
|
144
|
+
}); // [!code highlight]
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
<Callout type="warn">
|
|
149
|
+
This avoids creating a new run only after the first run has registered its hook. Because `start()` returns before the run body executes and calls `createHook()`, two concurrent requests can both observe "no hook yet" and each call `start()`. The race is resolved inside the workflow body, where the losing run observes `getConflict()` resolving with the active owner and returns without doing duplicate-sensitive work, and the route detects it by comparing the resumed hook's `runId` against the run it just started, without waiting for either run to finish. A native API for atomically starting a run and registering a hook is in the works. Until then, model recovery inside the workflow by checking `hook.getConflict()`.
|
|
150
|
+
</Callout>
|
|
151
|
+
|
|
152
|
+
This coordinates active runs by default: the token becomes available when its workflow ends. Set `experimental_minRetention` to keep it unavailable to late duplicates. After the workflow ends, the Hook can still be found with `getHookByToken()` until retention ends, but it cannot be resumed. See [`createHook()` minimum retention](/docs/api-reference/workflow/create-hook#keep-a-token-unavailable-after-the-run-ends) for examples and supported values.
|
|
153
|
+
|
|
154
|
+
### Conflict-handling strategies
|
|
155
|
+
|
|
156
|
+
Some workflow systems resolve duplicate IDs with a fixed, pre-declared policy, typically a static choice between rejecting the new execution, deferring to the existing one, or terminating it. Workflow has no policy enum. `hook.getConflict()` hands the duplicate run the conflicting `Run` itself, and the policy is ordinary code, including policies that inspect state before deciding, which static configuration can't express.
|
|
157
|
+
|
|
158
|
+
The example above implements **reject the duplicate**: return the owner's `runId` and let the caller decide. Other common strategies:
|
|
159
|
+
|
|
160
|
+
**Adopt the owner's result.** Wait for the active run to finish and return its result, so callers cannot tell which run did the work:
|
|
161
|
+
|
|
162
|
+
```typescript lineNumbers
|
|
163
|
+
import { createHook } from "workflow";
|
|
164
|
+
|
|
165
|
+
type OrderRequest = { confirmed: boolean };
|
|
166
|
+
declare function processOwnedOrder(orderId: string): Promise<{ status: string }>; // @setup
|
|
167
|
+
|
|
168
|
+
export async function processOrder(orderId: string) {
|
|
169
|
+
"use workflow";
|
|
170
|
+
|
|
171
|
+
using request = createHook<OrderRequest>({
|
|
172
|
+
token: `order:${orderId}`,
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
const conflict = await request.getConflict();
|
|
176
|
+
if (conflict) {
|
|
177
|
+
// Callers get the same result regardless of which run did the work.
|
|
178
|
+
return await conflict.returnValue; // [!code highlight]
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return await processOwnedOrder(orderId);
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
**Inspect the owner before deciding.** Reuse a completed owner's result, but reject other duplicates:
|
|
186
|
+
|
|
187
|
+
```typescript lineNumbers
|
|
188
|
+
import { createHook } from "workflow";
|
|
189
|
+
|
|
190
|
+
type OrderRequest = { confirmed: boolean };
|
|
191
|
+
declare function processOwnedOrder(orderId: string): Promise<{ status: string }>; // @setup
|
|
192
|
+
|
|
193
|
+
export async function processOrder(orderId: string) {
|
|
194
|
+
"use workflow";
|
|
195
|
+
|
|
196
|
+
using request = createHook<OrderRequest>({
|
|
197
|
+
token: `order:${orderId}`,
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
const conflict = await request.getConflict();
|
|
201
|
+
if (conflict) {
|
|
202
|
+
const status = await conflict.status; // [!code highlight]
|
|
203
|
+
if (status === "completed") {
|
|
204
|
+
return await conflict.returnValue;
|
|
205
|
+
}
|
|
206
|
+
return { status: "duplicate" as const, runId: conflict.runId };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return await processOwnedOrder(orderId);
|
|
210
|
+
}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
**Signal the owner instead of doing the work.** A conflict can refer to a finished run when `experimental_minRetention` is set, so check its status before sending data to its Hook:
|
|
214
|
+
|
|
215
|
+
```typescript lineNumbers
|
|
216
|
+
import { createHook } from "workflow";
|
|
217
|
+
import { resumeHook } from "workflow/api";
|
|
218
|
+
|
|
219
|
+
type OrderRequest = { confirmed: boolean };
|
|
220
|
+
|
|
221
|
+
async function forwardToOwner(token: string, payload: OrderRequest) {
|
|
222
|
+
"use step";
|
|
223
|
+
await resumeHook(token, payload); // [!code highlight]
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export async function processOrder(orderId: string, confirmed: boolean) {
|
|
227
|
+
"use workflow";
|
|
228
|
+
|
|
229
|
+
const token = `order:${orderId}`;
|
|
230
|
+
using request = createHook<OrderRequest>({ token });
|
|
231
|
+
|
|
232
|
+
const conflict = await request.getConflict();
|
|
233
|
+
if (conflict && ["pending", "running"].includes(await conflict.status)) {
|
|
234
|
+
await forwardToOwner(token, { confirmed }); // [!code highlight]
|
|
235
|
+
return { status: "forwarded" as const, runId: conflict.runId };
|
|
236
|
+
}
|
|
237
|
+
if (conflict) {
|
|
238
|
+
return { status: "duplicate" as const, runId: conflict.runId };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// ... own the token and do the work
|
|
242
|
+
}
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
**Supersede the owner.** Without minimum retention, cancel the active run, then claim the released token. The retry loop covers the window where cancellation cleanup has not propagated yet:
|
|
246
|
+
|
|
247
|
+
```typescript lineNumbers
|
|
248
|
+
import { createHook } from "workflow";
|
|
249
|
+
|
|
250
|
+
type OrderRequest = { confirmed: boolean };
|
|
251
|
+
declare function chargeOrder(orderId: string): Promise<void>; // @setup
|
|
252
|
+
|
|
253
|
+
export async function processOrderNewestWins(orderId: string) {
|
|
254
|
+
"use workflow";
|
|
255
|
+
|
|
256
|
+
const token = `order:${orderId}`;
|
|
257
|
+
|
|
258
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
259
|
+
using request = createHook<OrderRequest>({ token });
|
|
260
|
+
|
|
261
|
+
const conflict = await request.getConflict();
|
|
262
|
+
if (!conflict) {
|
|
263
|
+
// Token claimed: this run is now the owner.
|
|
264
|
+
const { confirmed } = await request;
|
|
265
|
+
if (confirmed) {
|
|
266
|
+
await chargeOrder(orderId);
|
|
267
|
+
}
|
|
268
|
+
return { status: "processed" as const };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
await conflict.cancel(); // [!code highlight]
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
throw new Error(`Could not claim ${token} after canceling the owner`);
|
|
275
|
+
}
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
<Callout type="warn">
|
|
279
|
+
This pattern does not work with `experimental_minRetention`: canceling the old run does not make its token available early.
|
|
280
|
+
</Callout>
|
|
281
|
+
|
|
282
|
+
If duplicate requests should only reuse the active run without sending data, use [`getHookByToken()`](/docs/api-reference/workflow-api/get-hook-by-token) as an advisory pre-check before calling `start()`. The workflow should still check `hook.getConflict()`, because the lookup and `start()` are not atomic.
|
|
53
283
|
|
|
54
|
-
|
|
55
|
-
- **Prefer `stepId` as your key**; it is stable across retries and unique per step.
|
|
56
|
-
- **Keep keys deterministic**; avoid including timestamps or attempt counters.
|
|
57
|
-
- **Handle 409/conflict responses** gracefully; treat them as success if the prior attempt completed.
|
|
284
|
+
Because this pattern uses hooks for idempotency, duplicate requests can also inject additional data and steer the existing run. The route example above uses `resumeHook()` for that: if the hook already exists, the duplicate request resumes the active workflow; if the hook is not registered yet, the route starts the workflow and retries `resumeHook()` so the payload is not dropped.
|
|
58
285
|
|
|
59
286
|
## Related docs
|
|
60
287
|
|
|
61
288
|
- Learn about retries in [Errors & Retrying](/docs/foundations/errors-and-retries)
|
|
62
289
|
- API reference: [`getStepMetadata`](/docs/api-reference/workflow/get-step-metadata)
|
|
290
|
+
- API reference: [`createHook()`](/docs/api-reference/workflow/create-hook)
|
|
291
|
+
- API reference: [`getHookByToken()`](/docs/api-reference/workflow-api/get-hook-by-token)
|
|
292
|
+
- API reference: [`resumeHook()`](/docs/api-reference/workflow-api/resume-hook)
|
|
293
|
+
- API reference: [`start()`](/docs/api-reference/workflow-api/start)
|
|
294
|
+
- Learn about deterministic hook tokens in [Hooks](/docs/foundations/hooks)
|
|
@@ -10,26 +10,4 @@ related:
|
|
|
10
10
|
|
|
11
11
|
Workflow programming can be a slight shift from how you traditionally write real-world applications. Learning the foundations now will go a long way toward helping you use workflows effectively.
|
|
12
12
|
|
|
13
|
-
<
|
|
14
|
-
<Card href="/docs/foundations/workflows-and-steps" title="Workflows and Steps">
|
|
15
|
-
Learn about the building blocks of durability
|
|
16
|
-
</Card>
|
|
17
|
-
<Card href="/docs/foundations/starting-workflows" title="Starting Workflows">
|
|
18
|
-
Trigger workflows and track their execution using the `start()` function.
|
|
19
|
-
</Card>
|
|
20
|
-
<Card href="/docs/foundations/errors-and-retries" title="Errors & Retrying">
|
|
21
|
-
Types of errors and how retrying work in workflows.
|
|
22
|
-
</Card>
|
|
23
|
-
<Card href="/docs/foundations/hooks" title="Webhooks (and hooks)">
|
|
24
|
-
Respond to external events in your workflow using hooks and webhooks.
|
|
25
|
-
</Card>
|
|
26
|
-
<Card href="/docs/foundations/streaming" title="Streaming">
|
|
27
|
-
Stream data in real-time to clients without waiting for the workflow to complete.
|
|
28
|
-
</Card>
|
|
29
|
-
<Card href="/docs/foundations/serialization" title="Serialization">
|
|
30
|
-
Understand which types can be passed between workflow and step functions.
|
|
31
|
-
</Card>
|
|
32
|
-
<Card href="/docs/foundations/idempotency" title="Idempotency">
|
|
33
|
-
Prevent duplicate side effects when retrying operations.
|
|
34
|
-
</Card>
|
|
35
|
-
</Cards>
|
|
13
|
+
<AutoCards />
|