workflow 5.0.0-beta.35 → 5.0.0-beta.37
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/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 +1 -1
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +1 -1
- package/docs/ai/index.mdx +1 -1
- package/docs/api-reference/workflow/create-hook.mdx +43 -2
- package/docs/api-reference/workflow/define-hook.mdx +26 -24
- package/docs/api-reference/workflow/fatal-error.mdx +29 -7
- package/docs/api-reference/workflow/fetch.mdx +3 -4
- package/docs/api-reference/workflow/set-attributes.mdx +0 -4
- package/docs/api-reference/workflow/sleep.mdx +1 -1
- package/docs/api-reference/workflow-api/get-hook-by-token.mdx +2 -0
- package/docs/api-reference/workflow-api/resume-hook.mdx +2 -0
- package/docs/api-reference/workflow-api/resume-webhook.mdx +6 -4
- package/docs/api-reference/workflow-api/start.mdx +1 -1
- package/docs/api-reference/workflow-globals.mdx +4 -1
- package/docs/api-reference/workflow-nest/nest-local-builder.mdx +1 -1
- package/docs/api-reference/workflow-runtime/health-check.mdx +4 -4
- package/docs/api-reference/workflow-runtime/world/queue.mdx +4 -4
- 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 +103 -0
- package/docs/configuration/build-and-diagnostics.mdx +19 -0
- package/docs/configuration/runtime-tuning.mdx +30 -6
- package/docs/configuration/worlds.mdx +22 -8
- package/docs/cookbook/advanced/publishing-libraries.mdx +17 -13
- package/docs/cookbook/common-patterns/idempotency.mdx +1 -1
- package/docs/{deploying/index.mdx → deploying.mdx} +6 -8
- package/docs/foundations/hooks.mdx +1 -1
- package/docs/foundations/idempotency.mdx +16 -9
- package/docs/getting-started/astro.mdx +2 -2
- package/docs/getting-started/express.mdx +2 -2
- package/docs/getting-started/fastify.mdx +2 -2
- package/docs/getting-started/hono.mdx +2 -2
- package/docs/getting-started/index.mdx +3 -3
- package/docs/getting-started/meta.json +2 -1
- package/docs/getting-started/nestjs.mdx +64 -3
- package/docs/getting-started/next.mdx +3 -3
- package/docs/getting-started/nitro.mdx +2 -2
- package/docs/getting-started/nuxt.mdx +2 -2
- package/docs/getting-started/sveltekit.mdx +2 -2
- package/docs/getting-started/tanstack-start.mdx +1 -1
- package/docs/getting-started/vite.mdx +2 -2
- package/docs/how-it-works/cancellation.mdx +2 -2
- package/docs/how-it-works/code-transform.mdx +19 -15
- package/docs/how-it-works/encryption.mdx +3 -3
- package/docs/how-it-works/event-sourcing.mdx +6 -6
- package/docs/how-it-works/framework-integrations.mdx +96 -342
- package/docs/meta.json +1 -1
- package/package.json +14 -12
- package/docs/deploying/building-a-world.mdx +0 -251
- package/docs/deploying/meta.json +0 -4
- package/docs/deploying/world/local-world.mdx +0 -105
- package/docs/deploying/world/meta.json +0 -4
- package/docs/deploying/world/postgres-world.mdx +0 -288
- package/docs/deploying/world/vercel-world.mdx +0 -270
- 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 -362
- package/docs/migration-guides/migrating-from-inngest.mdx +0 -308
- package/docs/migration-guides/migrating-from-temporal.mdx +0 -317
- package/docs/migration-guides/migrating-from-trigger-dev.mdx +0 -332
|
@@ -4,9 +4,9 @@ description: Configure the Workflow backend that stores runs and delivers queue
|
|
|
4
4
|
type: reference
|
|
5
5
|
summary: Select and configure Local, Postgres, Vercel, or custom Worlds.
|
|
6
6
|
related:
|
|
7
|
-
- /
|
|
8
|
-
- /
|
|
9
|
-
- /
|
|
7
|
+
- /worlds/local
|
|
8
|
+
- /worlds/postgres
|
|
9
|
+
- /worlds/vercel
|
|
10
10
|
---
|
|
11
11
|
|
|
12
12
|
A [World](/docs/deploying) stores workflow state and delivers queue messages.
|
|
@@ -21,6 +21,12 @@ A [World](/docs/deploying) stores workflow state and delivers queue messages.
|
|
|
21
21
|
|
|
22
22
|
Outside Vercel, Workflow defaults to the Local World. On Vercel, leave `WORKFLOW_TARGET_WORLD` unset for the normal case; Workflow detects the Vercel deployment and selects the Vercel World automatically.
|
|
23
23
|
|
|
24
|
+
The World is selected when your app is **built**, not when it runs, and compiled into the server bundles. Detection therefore reads the build environment: `VERCEL_DEPLOYMENT_ID` when it is available, otherwise `VERCEL=1` for builds where no deployment exists yet. A dev server is never affected, because `VERCEL_ENV=development` and `NODE_ENV=development` both keep the Local World selected — including when `vercel env pull` has written `VERCEL=1` into `.env.local`.
|
|
25
|
+
|
|
26
|
+
If your build environment does not expose Vercel's system environment variables — for example a build that runs in a container of your own before uploading, or one where [system environment variables](https://vercel.com/docs/environment-variables/system-environment-variables) are disabled for the project — set `WORKFLOW_TARGET_WORLD=vercel` in the build environment. Without it, the deployment is built against the Local World and every run fails writing to a read-only filesystem. Switching this value requires a rebuild; changing it at runtime has no effect.
|
|
27
|
+
|
|
28
|
+
The mirror image applies when a build that looks like Vercel is served somewhere else: a production server started locally or in CI with an env file from `vercel env pull` has `VERCEL=1`, so it is built against the Vercel World and then has no `VERCEL_DEPLOYMENT_ID` at runtime. Starting a run in that server fails with an error saying so. To run the same code against your filesystem, set `WORKFLOW_TARGET_WORLD=local` and rebuild.
|
|
29
|
+
|
|
24
30
|
Set `WORKFLOW_TARGET_WORLD` only when you want to use a custom or self-hosted World:
|
|
25
31
|
|
|
26
32
|
- `local` - alias for `@workflow/world-local`.
|
|
@@ -95,8 +101,8 @@ The Local World is the default outside Vercel and is intended for development.
|
|
|
95
101
|
### `streamFlushIntervalMs`
|
|
96
102
|
|
|
97
103
|
- Environment variable fallback: `WORKFLOW_STREAM_FLUSH_INTERVAL_MS`
|
|
98
|
-
- Default: `
|
|
99
|
-
-
|
|
104
|
+
- Default: `0` (dispatch the leading chunk of an idle stream immediately)
|
|
105
|
+
- Group-commit window for the leading chunk of an idle stream; a positive value trades first-chunk latency for larger groups. The `WORKFLOW_STREAM_FLUSH_INTERVAL_MS` environment variable, when set, overrides this option; otherwise the World option governs, including the very first chunk.
|
|
100
106
|
|
|
101
107
|
## Postgres World
|
|
102
108
|
|
|
@@ -128,6 +134,14 @@ The Postgres World is a self-hosted durable backend for long-running server proc
|
|
|
128
134
|
- Number of concurrent workers polling for jobs.
|
|
129
135
|
- Also bounds concurrent parent-to-child workflow return-value polls.
|
|
130
136
|
|
|
137
|
+
### `applicationManagedShutdown`
|
|
138
|
+
|
|
139
|
+
- Environment variable: `WORKFLOW_POSTGRES_APPLICATION_MANAGED_SHUTDOWN` (`1` enables)
|
|
140
|
+
- Default: `false`
|
|
141
|
+
- Whether the application coordinates shutdown instead of Graphile Worker responding automatically.
|
|
142
|
+
- Set to `true` only when the application awaits `world.close()` before closing its workflow HTTP server and caller-owned pool.
|
|
143
|
+
- Prevents Graphile Worker's default handler from terminating the process before the application's remaining cleanup finishes.
|
|
144
|
+
|
|
131
145
|
### `maxPoolSize`
|
|
132
146
|
|
|
133
147
|
- Environment variable: `WORKFLOW_POSTGRES_MAX_POOL_SIZE`
|
|
@@ -143,8 +157,8 @@ The Postgres World is a self-hosted durable backend for long-running server proc
|
|
|
143
157
|
### `streamFlushIntervalMs`
|
|
144
158
|
|
|
145
159
|
- Environment variable fallback: `WORKFLOW_STREAM_FLUSH_INTERVAL_MS`
|
|
146
|
-
- Default: `
|
|
147
|
-
-
|
|
160
|
+
- Default: `0` (dispatch the leading chunk of an idle stream immediately)
|
|
161
|
+
- Group-commit window for the leading chunk of an idle stream; a positive value trades first-chunk latency for larger groups. The `WORKFLOW_STREAM_FLUSH_INTERVAL_MS` environment variable, when set, overrides this option; otherwise the World option governs, including the very first chunk.
|
|
148
162
|
|
|
149
163
|
## Vercel World
|
|
150
164
|
|
|
@@ -201,7 +215,7 @@ Platform-provided values such as `VERCEL_DEPLOYMENT_ID`, `VERCEL_PROJECT_ID`, an
|
|
|
201
215
|
- Default: disabled
|
|
202
216
|
- Set `1` to serialize orchestrator (flow) invocations per run: each run's replays get their own queue topic and the flow trigger is generated with `maxConcurrency: 1`. Inline step executions get per-step topics and keep full parallelism.
|
|
203
217
|
- Read at **both build time and runtime** — set it as a project-level environment variable so the generated trigger and the runtime queue routing agree.
|
|
204
|
-
- Routing each run through a dedicated `maxConcurrency: 1` topic might lead to higher queue performance overhead. See [Vercel World](/
|
|
218
|
+
- Routing each run through a dedicated `maxConcurrency: 1` topic might lead to higher queue performance overhead. See [Vercel World](/worlds/vercel#workflow_sequential_replays) for details.
|
|
205
219
|
|
|
206
220
|
### `VERCEL_WORKFLOW_SERVER_URL`
|
|
207
221
|
|
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
title: Publishing Libraries
|
|
3
3
|
description: Structure and publish npm packages that export workflow functions for consumers to use with Workflow SDK.
|
|
4
4
|
type: guide
|
|
5
|
-
summary: Learn how to build, export, and test npm packages that ship workflow and step functions — including package.json exports, re-exporting
|
|
5
|
+
summary: Learn how to build, export, and test npm packages that ship workflow and step functions — including package.json exports, re-exporting so the consumer's compiler discovers your workflows, keeping step I/O clean, and integration testing.
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
<CopyPrompt
|
|
9
|
-
text="Package these workflow functions as a publishable npm library. Give the package a dedicated workflows entry point (for example `exports["./workflows"]`) that ships the workflow and step source for the consumer's compiler to process. Keep every workflow and step input and output serializable, and read credentials from environment variables inside steps instead of accepting client instances. Document the consumer re-export requirement: consumers create a file in their `workflows/` directory containing `export * from "<pkg>/workflows"` so their build
|
|
9
|
+
text="Package these workflow functions as a publishable npm library. Give the package a dedicated workflows entry point (for example `exports["./workflows"]`) that ships the workflow and step source for the consumer's compiler to process. Keep every workflow and step input and output serializable, and read credentials from environment variables inside steps instead of accepting client instances. Document the consumer re-export requirement: consumers create a file in their `workflows/` directory containing `export * from "<pkg>/workflows"` so their build discovers and compiles the library's workflow and step files and replay can resolve them after cold starts. Add an integration test that runs a library workflow end to end from a consumer-style setup. Verify the build, that the library's workflows and steps show up as compiled entries, and replay safety."
|
|
10
10
|
/>
|
|
11
11
|
|
|
12
12
|
import { File, Folder, Files } from "fumadocs-ui/components/files";
|
|
@@ -114,31 +114,35 @@ export default defineConfig({
|
|
|
114
114
|
});
|
|
115
115
|
```
|
|
116
116
|
|
|
117
|
-
## Re-Exporting for
|
|
117
|
+
## Re-Exporting for Compiler Discovery
|
|
118
118
|
|
|
119
|
-
|
|
119
|
+
The workflow compiler only transforms files it discovers, and discovery starts from the consumer's `workflows/` directory and follows imports out from there. A library's workflow functions are not on that graph by default, so nothing compiles them and the runtime has no definition to run.
|
|
120
120
|
|
|
121
|
-
The fix is a **re-export file**. The consumer creates a file in their `workflows/` directory that re-exports the library's workflows
|
|
121
|
+
The fix is a **re-export file**. The consumer creates a file in their `workflows/` directory that re-exports the library's workflows, which pulls the library's source onto the discovery graph and gives its entry point an address the runtime can resolve.
|
|
122
122
|
|
|
123
123
|
### Consumer Setup
|
|
124
124
|
|
|
125
125
|
```typescript lineNumbers
|
|
126
126
|
// workflows/media.ts (in the consumer's project)
|
|
127
|
-
// Re-export library workflows so the
|
|
127
|
+
// Re-export library workflows so the compiler discovers and transforms them
|
|
128
128
|
export * from "@acme/media/workflows"; // [!code highlight]
|
|
129
129
|
```
|
|
130
130
|
|
|
131
|
-
This one-line file is all that's needed. The
|
|
131
|
+
This one-line file is all that's needed. The compiler follows the re-export into the package, transforms the workflow and step functions it finds, and registers them under IDs the runtime can resolve.
|
|
132
132
|
|
|
133
133
|
### Why This Is Necessary
|
|
134
134
|
|
|
135
|
-
Without re-exporting, the workflow runtime cannot match a running workflow to its function definition. When a
|
|
135
|
+
Without re-exporting, the workflow runtime cannot match a running workflow to its function definition. When a run is replayed after a cold start, the runtime looks up functions by their compiler-assigned IDs. If those functions were never compiled, the IDs don't exist and replay fails.
|
|
136
136
|
|
|
137
|
-
|
|
137
|
+
Point the re-export at the package's dedicated workflows entry point rather than a deep path into `dist/`. Files reachable through the package's `exports` map get an ID of the form `name/subpath@version`; a deep, non-exported file falls back to a path-based ID instead.
|
|
138
138
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
139
|
+
<Callout type="info">
|
|
140
|
+
**IDs for code you ship embed your package version.** A step in `@acme/media` version `1.4.0` gets the ID `step//@acme/media/workflows@1.4.0//transcode`, so publishing `1.5.0` renames every workflow and step the package ships.
|
|
141
|
+
|
|
142
|
+
That is safe on worlds with deployment pinning, such as Vercel, because runs are pinned to the deployment that started them. A run records its deployment ID and every resume targets that same deployment, so after a consumer upgrades, new runs execute the new version while runs already in flight keep replaying against the old one. Consumers do not need to drain anything before upgrading.
|
|
143
|
+
|
|
144
|
+
The re-export file does not change any of this. An ID is derived from where the file lives, not from how it was imported, so a package file keeps its `name@version` ID whether or not a consumer re-exports it.
|
|
145
|
+
</Callout>
|
|
142
146
|
|
|
143
147
|
## Keeping Step I/O Clean
|
|
144
148
|
|
|
@@ -252,7 +256,7 @@ Some libraries want to be useful to consumers who *aren't* using Workflow SDK at
|
|
|
252
256
|
Two rules for isomorphic packages:
|
|
253
257
|
|
|
254
258
|
1. **Any runtime reference to the `workflow` package must be loaded via dynamic `import("workflow")` inside a try/catch.** A static top-level import makes the module fail to load for consumers who haven't installed workflow.
|
|
255
|
-
2. **The `"use workflow"` and `"use step"` directives are safe to keep in your library source.** When a consumer compiles your code with the Workflow SDK toolchain (via the [re-export pattern](#re-exporting-for-
|
|
259
|
+
2. **The `"use workflow"` and `"use step"` directives are safe to keep in your library source.** When a consumer compiles your code with the Workflow SDK toolchain (via the [re-export pattern](#re-exporting-for-compiler-discovery) above), the SWC plugin transforms them into durable-execution glue. When they're not compiled — plain Node, plain tests, a consumer without the runtime — they are just string expression statements and run as no-ops.
|
|
256
260
|
</Callout>
|
|
257
261
|
|
|
258
262
|
### Optional peer dependency
|
|
@@ -90,6 +90,6 @@ The workflow should create the deterministic hook and check `await hook.getConfl
|
|
|
90
90
|
- [`"use step"`](/docs/foundations/workflows-and-steps#step-functions) -- declares step functions with full Node.js access
|
|
91
91
|
- [`getStepMetadata()`](/docs/api-reference/workflow/get-step-metadata) -- provides the deterministic `stepId` for idempotency keys
|
|
92
92
|
- [`createHook()`](/docs/api-reference/workflow/create-hook) -- creates a hook with an optional deterministic token
|
|
93
|
-
- [`getHookByToken()`](/docs/api-reference/workflow-api/get-hook-by-token) -- finds the
|
|
93
|
+
- [`getHookByToken()`](/docs/api-reference/workflow-api/get-hook-by-token) -- finds the Hook that owns a token
|
|
94
94
|
- [`resumeHook()`](/docs/api-reference/workflow-api/resume-hook) -- resumes the active hook when the duplicate request carries data
|
|
95
95
|
- [`start()`](/docs/api-reference/workflow-api/start) -- starts a new workflow run
|
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Deploying
|
|
3
|
-
icon: Rocket
|
|
4
3
|
description: Deploy workflows locally, on Vercel, or anywhere using pluggable World adapters.
|
|
5
4
|
type: overview
|
|
6
5
|
summary: Learn how to deploy workflows to different environments using World adapters.
|
|
7
|
-
manualCards: true
|
|
8
6
|
related:
|
|
9
|
-
- /
|
|
10
|
-
- /
|
|
11
|
-
- /
|
|
12
|
-
- /
|
|
7
|
+
- /worlds/local
|
|
8
|
+
- /worlds/postgres
|
|
9
|
+
- /worlds/vercel
|
|
10
|
+
- /worlds/building-a-world
|
|
13
11
|
---
|
|
14
12
|
|
|
15
13
|
Workflows are designed to be highly portable. The same workflow code can run locally during development, on Vercel with zero configuration, or on any infrastructure using **Worlds** — pluggable adapters that handle storage, queuing, and communication.
|
|
@@ -54,7 +52,7 @@ vercel deploy
|
|
|
54
52
|
<FluidComputeCallout />
|
|
55
53
|
|
|
56
54
|
<Callout>
|
|
57
|
-
Learn more about the [Vercel World](/
|
|
55
|
+
Learn more about the [Vercel World](/worlds/vercel) and its capabilities, including [multi-region](/worlds/vercel#multi-region).
|
|
58
56
|
</Callout>
|
|
59
57
|
|
|
60
58
|
## Self-Hosting & Other Providers
|
|
@@ -65,7 +63,7 @@ For self-hosting or deploying to other cloud providers, you can use community-ma
|
|
|
65
63
|
<Card title="Explore Worlds" href="/worlds">
|
|
66
64
|
Browse official and community World implementations with compatibility status and performance benchmarks.
|
|
67
65
|
</Card>
|
|
68
|
-
<Card title="Build Your Own" href="/
|
|
66
|
+
<Card title="Build Your Own" href="/worlds/building-a-world">
|
|
69
67
|
Learn how to implement a custom World for your infrastructure.
|
|
70
68
|
</Card>
|
|
71
69
|
</Cards>
|
|
@@ -112,7 +112,7 @@ export async function orderWorkflow(orderId: string) {
|
|
|
112
112
|
}
|
|
113
113
|
```
|
|
114
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
|
|
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
116
|
|
|
117
117
|
### Custom Tokens for Deterministic Hooks
|
|
118
118
|
|
|
@@ -96,7 +96,7 @@ export async function processOrder(orderId: string): Promise<OrderResult> {
|
|
|
96
96
|
}
|
|
97
97
|
```
|
|
98
98
|
|
|
99
|
-
The runtime creates the hook atomically. At most one
|
|
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
100
|
|
|
101
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
102
|
|
|
@@ -149,7 +149,7 @@ export async function POST(request: Request) {
|
|
|
149
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
150
|
</Callout>
|
|
151
151
|
|
|
152
|
-
This
|
|
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
153
|
|
|
154
154
|
### Conflict-handling strategies
|
|
155
155
|
|
|
@@ -182,7 +182,7 @@ export async function processOrder(orderId: string) {
|
|
|
182
182
|
}
|
|
183
183
|
```
|
|
184
184
|
|
|
185
|
-
**Inspect the owner before deciding.**
|
|
185
|
+
**Inspect the owner before deciding.** Reuse a completed owner's result, but reject other duplicates:
|
|
186
186
|
|
|
187
187
|
```typescript lineNumbers
|
|
188
188
|
import { createHook } from "workflow";
|
|
@@ -200,17 +200,17 @@ export async function processOrder(orderId: string) {
|
|
|
200
200
|
const conflict = await request.getConflict();
|
|
201
201
|
if (conflict) {
|
|
202
202
|
const status = await conflict.status; // [!code highlight]
|
|
203
|
-
if (status === "
|
|
204
|
-
return
|
|
203
|
+
if (status === "completed") {
|
|
204
|
+
return await conflict.returnValue;
|
|
205
205
|
}
|
|
206
|
-
|
|
206
|
+
return { status: "duplicate" as const, runId: conflict.runId };
|
|
207
207
|
}
|
|
208
208
|
|
|
209
209
|
return await processOwnedOrder(orderId);
|
|
210
210
|
}
|
|
211
211
|
```
|
|
212
212
|
|
|
213
|
-
**Signal the owner instead of doing the work.**
|
|
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
214
|
|
|
215
215
|
```typescript lineNumbers
|
|
216
216
|
import { createHook } from "workflow";
|
|
@@ -230,16 +230,19 @@ export async function processOrder(orderId: string, confirmed: boolean) {
|
|
|
230
230
|
using request = createHook<OrderRequest>({ token });
|
|
231
231
|
|
|
232
232
|
const conflict = await request.getConflict();
|
|
233
|
-
if (conflict) {
|
|
233
|
+
if (conflict && ["pending", "running"].includes(await conflict.status)) {
|
|
234
234
|
await forwardToOwner(token, { confirmed }); // [!code highlight]
|
|
235
235
|
return { status: "forwarded" as const, runId: conflict.runId };
|
|
236
236
|
}
|
|
237
|
+
if (conflict) {
|
|
238
|
+
return { status: "duplicate" as const, runId: conflict.runId };
|
|
239
|
+
}
|
|
237
240
|
|
|
238
241
|
// ... own the token and do the work
|
|
239
242
|
}
|
|
240
243
|
```
|
|
241
244
|
|
|
242
|
-
**Supersede the owner.**
|
|
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:
|
|
243
246
|
|
|
244
247
|
```typescript lineNumbers
|
|
245
248
|
import { createHook } from "workflow";
|
|
@@ -272,6 +275,10 @@ export async function processOrderNewestWins(orderId: string) {
|
|
|
272
275
|
}
|
|
273
276
|
```
|
|
274
277
|
|
|
278
|
+
<Callout type="warn">
|
|
279
|
+
This pattern does not work with `experimental_minRetention`: cancelling the old run does not make its token available early.
|
|
280
|
+
</Callout>
|
|
281
|
+
|
|
275
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.
|
|
276
283
|
|
|
277
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.
|
|
@@ -160,7 +160,7 @@ async function sendOnboardingEmail(user: { id: string; email: string}) {
|
|
|
160
160
|
|
|
161
161
|
Taking a look at this code:
|
|
162
162
|
|
|
163
|
-
* Business logic lives inside **steps**. When a
|
|
163
|
+
* Business logic lives inside **steps**. When a workflow invokes a step, the workflow suspends while the step runs with full Node.js access. The combined handler may execute it inline; queued retries and continuations return through the same flow route.
|
|
164
164
|
* If a step throws an error, like in `sendWelcomeEmail`, the step will automatically be retried until it succeeds (or hits the step's max retry count).
|
|
165
165
|
* Steps can throw a `FatalError` if an error is intentional and should not be retried.
|
|
166
166
|
|
|
@@ -252,7 +252,7 @@ Additionally, check the [Deploying](/docs/deploying) section to learn how your w
|
|
|
252
252
|
If you see this error:
|
|
253
253
|
|
|
254
254
|
```
|
|
255
|
-
'start' received an invalid workflow function. Ensure the Workflow
|
|
255
|
+
'start' received an invalid workflow function. Ensure the Workflow SDK is configured correctly and the function includes a 'use workflow' directive.
|
|
256
256
|
```
|
|
257
257
|
|
|
258
258
|
Check both of these first:
|
|
@@ -185,7 +185,7 @@ async function sendOnboardingEmail(user: { id: string; email: string }) {
|
|
|
185
185
|
|
|
186
186
|
Taking a look at this code:
|
|
187
187
|
|
|
188
|
-
- Business logic lives inside **steps**. When a
|
|
188
|
+
- Business logic lives inside **steps**. When a workflow invokes a step, the workflow suspends while the step runs with full Node.js access. The combined handler may execute it inline; queued retries and continuations return through the same flow route.
|
|
189
189
|
- If a step throws an error, like in `sendWelcomeEmail`, the step will automatically be retried until it succeeds (or hits the step's max retry count).
|
|
190
190
|
- Steps can throw a `FatalError` if an error is intentional and should not be retried.
|
|
191
191
|
|
|
@@ -273,7 +273,7 @@ Check the [Deploying](/docs/deploying) section to learn how your workflows can b
|
|
|
273
273
|
If you see this error:
|
|
274
274
|
|
|
275
275
|
```
|
|
276
|
-
'start' received an invalid workflow function. Ensure the Workflow
|
|
276
|
+
'start' received an invalid workflow function. Ensure the Workflow SDK is configured correctly and the function includes a 'use workflow' directive.
|
|
277
277
|
```
|
|
278
278
|
|
|
279
279
|
Check both of these first:
|
|
@@ -174,7 +174,7 @@ async function sendOnboardingEmail(user: { id: string; email: string }) {
|
|
|
174
174
|
```
|
|
175
175
|
Taking a look at this code:
|
|
176
176
|
|
|
177
|
-
- Business logic lives inside **steps**. When a
|
|
177
|
+
- Business logic lives inside **steps**. When a workflow invokes a step, the workflow suspends while the step runs with full Node.js access. The combined handler may execute it inline; queued retries and continuations return through the same flow route.
|
|
178
178
|
- If a step throws an error, like in `sendWelcomeEmail`, the step will automatically be retried until it succeeds (or hits the step's max retry count).
|
|
179
179
|
- Steps can throw a `FatalError` if an error is intentional and should not be retried.
|
|
180
180
|
|
|
@@ -260,7 +260,7 @@ Check the [Deploying](/docs/deploying) section to learn how your workflows can b
|
|
|
260
260
|
If you see this error:
|
|
261
261
|
|
|
262
262
|
```
|
|
263
|
-
'start' received an invalid workflow function. Ensure the Workflow
|
|
263
|
+
'start' received an invalid workflow function. Ensure the Workflow SDK is configured correctly and the function includes a 'use workflow' directive.
|
|
264
264
|
```
|
|
265
265
|
|
|
266
266
|
Check both of these first:
|
|
@@ -170,7 +170,7 @@ async function sendOnboardingEmail(user: { id: string; email: string }) {
|
|
|
170
170
|
|
|
171
171
|
Taking a look at this code:
|
|
172
172
|
|
|
173
|
-
- Business logic lives inside **steps**. When a
|
|
173
|
+
- Business logic lives inside **steps**. When a workflow invokes a step, the workflow suspends while the step runs with full Node.js access. The combined handler may execute it inline; queued retries and continuations return through the same flow route.
|
|
174
174
|
- If a step throws an error, like in `sendWelcomeEmail`, the step will automatically be retried until it succeeds (or hits the step's max retry count).
|
|
175
175
|
- Steps can throw a `FatalError` if an error is intentional and should not be retried.
|
|
176
176
|
|
|
@@ -255,7 +255,7 @@ Check the [Deploying](/docs/deploying) section to learn how your workflows can b
|
|
|
255
255
|
If you see this error:
|
|
256
256
|
|
|
257
257
|
```
|
|
258
|
-
'start' received an invalid workflow function. Ensure the Workflow
|
|
258
|
+
'start' received an invalid workflow function. Ensure the Workflow SDK is configured correctly and the function includes a 'use workflow' directive.
|
|
259
259
|
```
|
|
260
260
|
|
|
261
261
|
Check both of these first:
|
|
@@ -83,11 +83,11 @@ import { SiReactrouter } from "@icons-pack/react-simple-icons";
|
|
|
83
83
|
<Badge variant="secondary">Beta</Badge>
|
|
84
84
|
</div>
|
|
85
85
|
</Card>
|
|
86
|
-
<Card
|
|
86
|
+
<Card href="/docs/getting-started/nestjs">
|
|
87
87
|
<div className="flex flex-col items-center justify-center gap-2">
|
|
88
|
-
<Nest className="size-16 dark:invert
|
|
88
|
+
<Nest className="size-16 dark:invert" />
|
|
89
89
|
<span className="font-medium">NestJS</span>
|
|
90
|
-
<Badge variant="secondary">
|
|
90
|
+
<Badge variant="secondary">Experimental</Badge>
|
|
91
91
|
</div>
|
|
92
92
|
</Card>
|
|
93
93
|
</Cards>
|
|
@@ -16,7 +16,8 @@ related:
|
|
|
16
16
|
This guide will walk through setting up your first workflow in a NestJS app. Along the way, you'll learn more about the concepts that are fundamental to using the Workflow SDK in your own projects.
|
|
17
17
|
|
|
18
18
|
<Callout>
|
|
19
|
-
NestJS integration is experimental
|
|
19
|
+
NestJS integration is experimental. Deployment to Vercel is supported via the
|
|
20
|
+
`workflow-nest build --vercel` command — see [Deploy to Vercel](#deploy-to-vercel) below.
|
|
20
21
|
</Callout>
|
|
21
22
|
|
|
22
23
|
---
|
|
@@ -288,7 +289,7 @@ async function sendOnboardingEmail(user: { id: string; email: string }) {
|
|
|
288
289
|
|
|
289
290
|
Taking a look at this code:
|
|
290
291
|
|
|
291
|
-
- Business logic lives inside **steps**. When a
|
|
292
|
+
- Business logic lives inside **steps**. When a workflow invokes a step, the workflow suspends while the step runs with full Node.js access. The combined handler may execute it inline; queued retries and continuations return through the same flow route.
|
|
292
293
|
- If a step throws an error, like in `sendWelcomeEmail`, the step will automatically be retried until it succeeds (or hits the step's max retry count).
|
|
293
294
|
- Steps can throw a `FatalError` if an error is intentional and should not be retried.
|
|
294
295
|
|
|
@@ -361,6 +362,66 @@ npx workflow inspect runs
|
|
|
361
362
|
|
|
362
363
|
</Step>
|
|
363
364
|
|
|
365
|
+
<Step>
|
|
366
|
+
|
|
367
|
+
## Deploy to Vercel
|
|
368
|
+
|
|
369
|
+
Because NestJS is not a Vercel-native framework, the Workflow SDK produces a
|
|
370
|
+
[Build Output API](https://vercel.com/docs/build-output-api) directory for you. This emits the
|
|
371
|
+
workflow queue-consumer function (registered with `experimentalTriggers` so Vercel's queue can
|
|
372
|
+
discover it) alongside your NestJS app as a catch-all function. Without it, workflow runs stay
|
|
373
|
+
`pending` because nothing consumes the queue.
|
|
374
|
+
|
|
375
|
+
Add a Vercel-specific entry module that default-exports your Nest app's underlying Node handler:
|
|
376
|
+
|
|
377
|
+
```typescript title="_vercel/entry.ts" lineNumbers
|
|
378
|
+
import 'reflect-metadata';
|
|
379
|
+
import { NestFactory } from '@nestjs/core';
|
|
380
|
+
import type { NestExpressApplication } from '@nestjs/platform-express';
|
|
381
|
+
import express from 'express';
|
|
382
|
+
import { AppModule } from '../dist/app.module.js';
|
|
383
|
+
|
|
384
|
+
let ready: Promise<express.Express> | undefined;
|
|
385
|
+
|
|
386
|
+
async function createHandler() {
|
|
387
|
+
const app = await NestFactory.create<NestExpressApplication>(AppModule, {
|
|
388
|
+
bodyParser: false,
|
|
389
|
+
});
|
|
390
|
+
app.use(express.json());
|
|
391
|
+
await app.init();
|
|
392
|
+
return app.getHttpAdapter().getInstance();
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
export default async function handler(req: express.Request, res: express.Response) {
|
|
396
|
+
ready ??= createHandler();
|
|
397
|
+
return (await ready)(req, res);
|
|
398
|
+
}
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
Skip the in-process build on Vercel (the bundles are pre-built) by passing `skipBuild` when
|
|
402
|
+
`VERCEL` is set:
|
|
403
|
+
|
|
404
|
+
{/* @skip-typecheck - config snippet, WorkflowModule imported above */}
|
|
405
|
+
```typescript title="src/app.module.ts"
|
|
406
|
+
WorkflowModule.forRoot({ skipBuild: Boolean(process.env.VERCEL) });
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
Then set your build command so the Build Output is produced after `nest build`:
|
|
410
|
+
|
|
411
|
+
```json title="package.json" lineNumbers
|
|
412
|
+
{
|
|
413
|
+
"scripts": {
|
|
414
|
+
"vercel-build": "workflow-nest init --force && nest build && workflow-nest build --vercel --dirs src/workflows --entry _vercel/entry.ts"
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
Deploy as usual. `workflow-nest build --vercel` runs automatically on Vercel (it also detects the
|
|
420
|
+
`VERCEL` environment variable), writing `.vercel/output` with the consumer function so your runs
|
|
421
|
+
execute instead of staying `pending`.
|
|
422
|
+
|
|
423
|
+
</Step>
|
|
424
|
+
|
|
364
425
|
</Steps>
|
|
365
426
|
|
|
366
427
|
---
|
|
@@ -409,7 +470,7 @@ WorkflowModule.forRoot({
|
|
|
409
470
|
If you see this error:
|
|
410
471
|
|
|
411
472
|
```
|
|
412
|
-
'start' received an invalid workflow function. Ensure the Workflow
|
|
473
|
+
'start' received an invalid workflow function. Ensure the Workflow SDK is configured correctly and the function includes a 'use workflow' directive.
|
|
413
474
|
```
|
|
414
475
|
|
|
415
476
|
Check both of these first:
|
|
@@ -7,7 +7,7 @@ prerequisites:
|
|
|
7
7
|
- /docs/getting-started
|
|
8
8
|
related:
|
|
9
9
|
- /docs/api-reference/workflow-next
|
|
10
|
-
- /
|
|
10
|
+
- /worlds/vercel
|
|
11
11
|
---
|
|
12
12
|
|
|
13
13
|
<CopyPrompt
|
|
@@ -192,7 +192,7 @@ async function sendOnboardingEmail(user: { id: string; email: string}) {
|
|
|
192
192
|
|
|
193
193
|
Taking a look at this code:
|
|
194
194
|
|
|
195
|
-
* Business logic lives inside **steps**. When a
|
|
195
|
+
* Business logic lives inside **steps**. When a workflow invokes a step, the workflow suspends while the step runs with full Node.js access. The combined handler may execute it inline; queued retries and continuations return through the same flow route.
|
|
196
196
|
* If a step throws an error, like in `sendWelcomeEmail`, the step will automatically be retried until it succeeds (or hits the step's max retry count).
|
|
197
197
|
* Steps can throw a `FatalError` if an error is intentional and should not be retried.
|
|
198
198
|
|
|
@@ -316,7 +316,7 @@ Without this configuration, you may experience intermittent issues where workflo
|
|
|
316
316
|
If you see this error:
|
|
317
317
|
|
|
318
318
|
```
|
|
319
|
-
'start' received an invalid workflow function. Ensure the Workflow
|
|
319
|
+
'start' received an invalid workflow function. Ensure the Workflow SDK is configured correctly and the function includes a 'use workflow' directive.
|
|
320
320
|
```
|
|
321
321
|
|
|
322
322
|
Check both of these first:
|
|
@@ -172,7 +172,7 @@ async function sendOnboardingEmail(user: { id: string; email: string }) {
|
|
|
172
172
|
|
|
173
173
|
Taking a look at this code:
|
|
174
174
|
|
|
175
|
-
- Business logic lives inside **steps**. When a
|
|
175
|
+
- Business logic lives inside **steps**. When a workflow invokes a step, the workflow suspends while the step runs with full Node.js access. The combined handler may execute it inline; queued retries and continuations return through the same flow route.
|
|
176
176
|
- If a step throws an error, like in `sendWelcomeEmail`, the step will automatically be retried until it succeeds (or hits the step's max retry count).
|
|
177
177
|
- Steps can throw a `FatalError` if an error is intentional and should not be retried.
|
|
178
178
|
|
|
@@ -261,7 +261,7 @@ Check the [Deploying](/docs/deploying) section to learn how your workflows can b
|
|
|
261
261
|
If you see this error:
|
|
262
262
|
|
|
263
263
|
```
|
|
264
|
-
'start' received an invalid workflow function. Ensure the Workflow
|
|
264
|
+
'start' received an invalid workflow function. Ensure the Workflow SDK is configured correctly and the function includes a 'use workflow' directive.
|
|
265
265
|
```
|
|
266
266
|
|
|
267
267
|
Check both of these first:
|
|
@@ -149,7 +149,7 @@ async function sendOnboardingEmail(user: { id: string; email: string }) {
|
|
|
149
149
|
|
|
150
150
|
Taking a look at this code:
|
|
151
151
|
|
|
152
|
-
- Business logic lives inside **steps**. When a
|
|
152
|
+
- Business logic lives inside **steps**. When a workflow invokes a step, the workflow suspends while the step runs with full Node.js access. The combined handler may execute it inline; queued retries and continuations return through the same flow route.
|
|
153
153
|
- If a step throws an error, like in `sendWelcomeEmail`, the step will automatically be retried until it succeeds (or hits the step's max retry count).
|
|
154
154
|
- Steps can throw a `FatalError` if an error is intentional and should not be retried.
|
|
155
155
|
|
|
@@ -240,7 +240,7 @@ Check the [Deploying](/docs/deploying) section to learn how your workflows can b
|
|
|
240
240
|
If you see this error:
|
|
241
241
|
|
|
242
242
|
```
|
|
243
|
-
'start' received an invalid workflow function. Ensure the Workflow
|
|
243
|
+
'start' received an invalid workflow function. Ensure the Workflow SDK is configured correctly and the function includes a 'use workflow' directive.
|
|
244
244
|
```
|
|
245
245
|
|
|
246
246
|
Check both of these first:
|
|
@@ -157,7 +157,7 @@ async function sendOnboardingEmail(user: { id: string; email: string}) {
|
|
|
157
157
|
|
|
158
158
|
Taking a look at this code:
|
|
159
159
|
|
|
160
|
-
* Business logic lives inside **steps**. When a
|
|
160
|
+
* Business logic lives inside **steps**. When a workflow invokes a step, the workflow suspends while the step runs with full Node.js access. The combined handler may execute it inline; queued retries and continuations return through the same flow route.
|
|
161
161
|
* If a step throws an error, like in `sendWelcomeEmail`, the step will automatically be retried until it succeeds (or hits the step's max retry count).
|
|
162
162
|
* Steps can throw a `FatalError` if an error is intentional and should not be retried.
|
|
163
163
|
|
|
@@ -245,7 +245,7 @@ Check the [Deploying](/docs/deploying) section to learn how your workflows can b
|
|
|
245
245
|
If you see this error:
|
|
246
246
|
|
|
247
247
|
```
|
|
248
|
-
'start' received an invalid workflow function. Ensure the Workflow
|
|
248
|
+
'start' received an invalid workflow function. Ensure the Workflow SDK is configured correctly and the function includes a 'use workflow' directive.
|
|
249
249
|
```
|
|
250
250
|
|
|
251
251
|
Check both of these first:
|
|
@@ -155,7 +155,7 @@ async function sendOnboardingEmail(user: { id: string; email: string}) {
|
|
|
155
155
|
|
|
156
156
|
Taking a look at this code:
|
|
157
157
|
|
|
158
|
-
* Business logic lives inside **steps**. When a
|
|
158
|
+
* Business logic lives inside **steps**. When a workflow invokes a step, the workflow suspends while the step runs with full Node.js access. The combined handler may execute it inline; queued retries and continuations return through the same flow route.
|
|
159
159
|
* If a step throws an error, like in `sendWelcomeEmail`, the step will automatically be retried until it succeeds (or hits the step's max retry count).
|
|
160
160
|
* Steps can throw a `FatalError` if an error is intentional and should not be retried.
|
|
161
161
|
|
|
@@ -160,7 +160,7 @@ async function sendOnboardingEmail(user: { id: string; email: string}) {
|
|
|
160
160
|
|
|
161
161
|
Taking a look at this code:
|
|
162
162
|
|
|
163
|
-
* Business logic lives inside **steps**. When a
|
|
163
|
+
* Business logic lives inside **steps**. When a workflow invokes a step, the workflow suspends while the step runs with full Node.js access. The combined handler may execute it inline; queued retries and continuations return through the same flow route.
|
|
164
164
|
* If a step throws an error, like in `sendWelcomeEmail`, the step will automatically be retried until it succeeds (or hits the step's max retry count).
|
|
165
165
|
* Steps can throw a `FatalError` if an error is intentional and should not be retried.
|
|
166
166
|
|
|
@@ -245,7 +245,7 @@ Check the [Deploying](/docs/deploying) section to learn how your workflows can b
|
|
|
245
245
|
If you see this error:
|
|
246
246
|
|
|
247
247
|
```
|
|
248
|
-
'start' received an invalid workflow function. Ensure the Workflow
|
|
248
|
+
'start' received an invalid workflow function. Ensure the Workflow SDK is configured correctly and the function includes a 'use workflow' directive.
|
|
249
249
|
```
|
|
250
250
|
|
|
251
251
|
Check both of these first:
|