workflow 5.0.0-beta.36 → 5.0.0-beta.38
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 +0 -16
- 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/set-attributes.mdx +0 -4
- package/docs/api-reference/workflow-globals.mdx +4 -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/configuration/build-and-diagnostics.mdx +19 -0
- package/docs/configuration/runtime-tuning.mdx +16 -3
- package/docs/configuration/worlds.mdx +18 -4
- package/docs/cookbook/advanced/publishing-libraries.mdx +17 -13
- package/docs/getting-started/astro.mdx +1 -1
- package/docs/getting-started/express.mdx +1 -1
- package/docs/getting-started/fastify.mdx +1 -1
- package/docs/getting-started/hono.mdx +1 -1
- package/docs/getting-started/nestjs.mdx +1 -1
- package/docs/getting-started/next.mdx +1 -1
- package/docs/getting-started/nitro.mdx +1 -1
- package/docs/getting-started/nuxt.mdx +1 -1
- package/docs/getting-started/sveltekit.mdx +1 -1
- package/docs/getting-started/tanstack-start.mdx +1 -1
- package/docs/getting-started/vite.mdx +1 -1
- package/docs/how-it-works/cancellation.mdx +2 -2
- package/docs/how-it-works/code-transform.mdx +19 -15
- package/docs/how-it-works/framework-integrations.mdx +95 -341
- package/package.json +12 -12
package/README.md
CHANGED
|
@@ -28,22 +28,6 @@ The Workflow SDK community can be found on [GitHub Discussions](https://github.c
|
|
|
28
28
|
|
|
29
29
|
Contributions to Workflow SDK are welcome and highly appreciated. Please use GitHub [issues](https://github.com/vercel/workflow/issues) and [discussions](https://github.com/vercel/workflow/discussions) to collaborate with the team and wider community.
|
|
30
30
|
|
|
31
|
-
## Author list
|
|
32
|
-
|
|
33
|
-
Workflow SDK was built by engineers at [Vercel](https://vercel.com) and the [Open Source Community](https://github.com/vercel/workflow/graphs/contributors).
|
|
34
|
-
|
|
35
|
-
The initial core contributing engineers are:
|
|
36
|
-
|
|
37
|
-
- Adrian Lam ([@adriandlam](https://github.com/adriandlam))
|
|
38
|
-
- Dillon Mulroy ([@dmmulroy](https://github.com/dmmulroy))
|
|
39
|
-
- Gal Schlezinger ([@Schniz](https://github.com/Schniz))
|
|
40
|
-
- JJ Kasper ([@ijjk](https://github.com/ijjk))
|
|
41
|
-
- Nathan Rajlich ([@TooTallNate](https://github.com/TooTallNate))
|
|
42
|
-
- Peter Wielander ([@VaguelySerious](https://github.com/VaguelySerious))
|
|
43
|
-
- Pranay Prakash ([@pranaygp](https://github.com/pranaygp))
|
|
44
|
-
|
|
45
|
-
The Workflow SDK logo was designed by Cecilio Ruiz [@ceciliorz](https://x.com/ceciliorz)
|
|
46
|
-
|
|
47
31
|
---
|
|
48
32
|
|
|
49
33
|
## Security
|
package/dist/runtime.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import '@workflow/core/runtime/world-init';
|
|
2
|
-
export { createWorld, getWorld, getWorldHandlers, type
|
|
2
|
+
export { createWorld, getWorld, getWorldHandlers, type HealthCheckOptions, type HealthCheckResult, healthCheck, setWorld, workflowEntrypoint, } from '@workflow/core/runtime';
|
|
3
3
|
//# sourceMappingURL=runtime.d.ts.map
|
package/dist/runtime.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"AAEA,OAAO,mCAAmC,CAAC;AAE3C,OAAO,EACL,WAAW,EACX,QAAQ,EACR,gBAAgB,EAChB,KAAK,
|
|
1
|
+
{"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"AAEA,OAAO,mCAAmC,CAAC;AAE3C,OAAO,EACL,WAAW,EACX,QAAQ,EACR,gBAAgB,EAChB,KAAK,kBAAkB,EACvB,KAAK,iBAAiB,EACtB,WAAW,EACX,QAAQ,EACR,kBAAkB,GACnB,MAAM,wBAAwB,CAAC"}
|
package/dist/runtime.js
CHANGED
|
@@ -2,4 +2,4 @@
|
|
|
2
2
|
// through stream helpers, so register getWorld before exporting runtime APIs.
|
|
3
3
|
import '@workflow/core/runtime/world-init';
|
|
4
4
|
export { createWorld, getWorld, getWorldHandlers, healthCheck, setWorld, workflowEntrypoint, } from '@workflow/core/runtime';
|
|
5
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
5
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicnVudGltZS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3NyYy9ydW50aW1lLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLDZFQUE2RTtBQUM3RSw4RUFBOEU7QUFDOUUsT0FBTyxtQ0FBbUMsQ0FBQztBQUUzQyxPQUFPLEVBQ0wsV0FBVyxFQUNYLFFBQVEsRUFDUixnQkFBZ0IsRUFHaEIsV0FBVyxFQUNYLFFBQVEsRUFDUixrQkFBa0IsR0FDbkIsTUFBTSx3QkFBd0IsQ0FBQyIsInNvdXJjZXNDb250ZW50IjpbIi8vIEhvc3Qtc2lkZSBzaWRlIGVmZmVjdDogcnVudGltZS1vbmx5IGltcG9ydHMgY2FuIHN0aWxsIHJlYWNoIGdldFdvcmxkTGF6eSgpXG4vLyB0aHJvdWdoIHN0cmVhbSBoZWxwZXJzLCBzbyByZWdpc3RlciBnZXRXb3JsZCBiZWZvcmUgZXhwb3J0aW5nIHJ1bnRpbWUgQVBJcy5cbmltcG9ydCAnQHdvcmtmbG93L2NvcmUvcnVudGltZS93b3JsZC1pbml0JztcblxuZXhwb3J0IHtcbiAgY3JlYXRlV29ybGQsXG4gIGdldFdvcmxkLFxuICBnZXRXb3JsZEhhbmRsZXJzLFxuICB0eXBlIEhlYWx0aENoZWNrT3B0aW9ucyxcbiAgdHlwZSBIZWFsdGhDaGVja1Jlc3VsdCxcbiAgaGVhbHRoQ2hlY2ssXG4gIHNldFdvcmxkLFxuICB3b3JrZmxvd0VudHJ5cG9pbnQsXG59IGZyb20gJ0B3b3JrZmxvdy9jb3JlL3J1bnRpbWUnO1xuIl19
|
package/docs/ai/index.mdx
CHANGED
|
@@ -382,7 +382,7 @@ export async function checkBaggageAllowance(
|
|
|
382
382
|
|
|
383
383
|
With `"use step"`:
|
|
384
384
|
|
|
385
|
-
- The tool execution runs
|
|
385
|
+
- The tool execution runs as a durable step with full Node.js access. The combined runtime may execute it inline in the flow request or resume it through the workflow queue, and can scale queued work with your workload.
|
|
386
386
|
- Failed tool calls are automatically retried (up to 3 times by default). See [Errors and Retries](/docs/foundations/errors-and-retries) for more details.
|
|
387
387
|
- Each tool execution appears as a discrete step in observability tools. See [Observability](/docs/observability) for more details.
|
|
388
388
|
</Step>
|
|
@@ -59,7 +59,3 @@ Validation errors throw [`FatalError`](/docs/api-reference/workflow/fatal-error)
|
|
|
59
59
|
Calls from both workflow and step bodies append a native `attr_set` event, which the World materializes onto `run.attributes`. Workflow-originated events record a workflow writer; step-originated events record the originating step ID and attempt.
|
|
60
60
|
|
|
61
61
|
Native attributes require spec version 4 or later. Step-body storage errors throw from `setAttributes`; catch them inside the step if the write should be best-effort. Workflow-body writes are committed when the workflow suspends: transient storage errors are retried with the suspension, while a write the World rejects as invalid — such as exceeding the per-run attribute cap across multiple calls — fails the run with the validation error.
|
|
62
|
-
|
|
63
|
-
<Callout>
|
|
64
|
-
This function was previously exported as `experimental_setAttributes`. The old name still works as a deprecated alias — update imports to `setAttributes`.
|
|
65
|
-
</Callout>
|
|
@@ -26,7 +26,7 @@ These APIs are available but are **seeded or fixed** to ensure deterministic beh
|
|
|
26
26
|
| [`Date`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date) / `Date.now()` / `new Date()` | Returns a fixed timestamp that advances with the workflow's logical clock |
|
|
27
27
|
| [`crypto.getRandomValues()`](https://developer.mozilla.org/en-US/docs/Web/API/Crypto/getRandomValues) | Seeded — produces deterministic output for a given workflow run |
|
|
28
28
|
| [`crypto.randomUUID()`](https://developer.mozilla.org/en-US/docs/Web/API/Crypto/randomUUID) | Seeded — produces deterministic UUIDs for a given workflow run |
|
|
29
|
-
| [`crypto.subtle.digest()`](https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest) |
|
|
29
|
+
| [`crypto.subtle.digest()`](https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest) | Computed synchronously via `node:crypto` (values are byte-identical to WebCrypto), so the promise settles at a deterministic point during replay |
|
|
30
30
|
|
|
31
31
|
<Callout type="info">
|
|
32
32
|
You can safely use `Math.random()`, `Date.now()`, and `crypto.randomUUID()` in workflow functions. The framework ensures these return the same values across replays.
|
|
@@ -100,3 +100,6 @@ The following are **not available** in workflow functions. Move this logic to [s
|
|
|
100
100
|
- **Global `fetch`**: Use [`import { fetch } from "workflow"`](/docs/api-reference/workflow/fetch) instead. See [fetch-in-workflow](/docs/errors/fetch-in-workflow).
|
|
101
101
|
- **Timers**: `setTimeout`, `setInterval`, `setImmediate`, and their `clear*` counterparts. Use [`sleep()`](/docs/api-reference/workflow/sleep) instead. See [timeout-in-workflow](/docs/errors/timeout-in-workflow).
|
|
102
102
|
- **`Buffer`**: Node.js-specific API. Use `Uint8Array` with `toBase64()` / `fromBase64()` / `toHex()` / `fromHex()` for binary data encoding, or `atob()` / `btoa()` for string-based base64.
|
|
103
|
+
- **`WeakRef` and `FinalizationRegistry`**: garbage-collection timing is not deterministic, so observing it would make workflow code impossible to replay. (`WeakMap` and `WeakSet` remain available — they do not expose GC state.)
|
|
104
|
+
- **`Atomics.waitAsync`**: a wall-clock timer, which cannot be replayed. Use [`sleep()`](/docs/api-reference/workflow/sleep) instead.
|
|
105
|
+
- **Async `WebAssembly` compilation** (`compile`, `instantiate`, `compileStreaming`, `instantiateStreaming`): resolves on compile-thread timing. The synchronous `new WebAssembly.Module()` and `new WebAssembly.Instance()` constructors remain available.
|
|
@@ -13,7 +13,7 @@ Performs an end-to-end health check of a deployment's workflow infrastructure by
|
|
|
13
13
|
import { getWorld, healthCheck } from "workflow/runtime";
|
|
14
14
|
|
|
15
15
|
const world = await getWorld();
|
|
16
|
-
const result = await healthCheck(world
|
|
16
|
+
const result = await healthCheck(world); // [!code highlight]
|
|
17
17
|
|
|
18
18
|
if (!result.healthy) {
|
|
19
19
|
console.error("Workflow infrastructure unhealthy:", result.error);
|
|
@@ -27,8 +27,7 @@ if (!result.healthy) {
|
|
|
27
27
|
| Parameter | Type | Description |
|
|
28
28
|
|-----------|------|-------------|
|
|
29
29
|
| `world` | `World` | The World instance to send the health check through |
|
|
30
|
-
| `
|
|
31
|
-
| `options` | `HealthCheckOptions & { namespace?: string }` | Optional configuration |
|
|
30
|
+
| `options` | `HealthCheckOptions` | Optional configuration |
|
|
32
31
|
|
|
33
32
|
Where `HealthCheckOptions` is:
|
|
34
33
|
|
|
@@ -36,6 +35,7 @@ Where `HealthCheckOptions` is:
|
|
|
36
35
|
|--------|------|-------------|
|
|
37
36
|
| `timeout` | `number` | Milliseconds to wait for the health check response. Default: `30000`. |
|
|
38
37
|
| `deploymentId` | `string` | Deployment to target. Falls back to `process.env.VERCEL_DEPLOYMENT_ID`. |
|
|
38
|
+
| `namespace` | `string` | Queue namespace of the target deployment. Falls back to `WORKFLOW_QUEUE_NAMESPACE`. |
|
|
39
39
|
|
|
40
40
|
### Returns
|
|
41
41
|
|
|
@@ -43,7 +43,7 @@ Returns a `Promise<HealthCheckResult>`:
|
|
|
43
43
|
|
|
44
44
|
| Property | Type | Description |
|
|
45
45
|
|----------|------|-------------|
|
|
46
|
-
| `healthy` | `boolean` | Whether the endpoint processed the health check message |
|
|
46
|
+
| `healthy` | `boolean` | Whether the combined workflow endpoint processed the health check message |
|
|
47
47
|
| `error` | `string \| undefined` | Error message when the check failed |
|
|
48
48
|
| `latencyMs` | `number \| undefined` | Round-trip latency when the check succeeded |
|
|
49
49
|
| `specVersion` | `number \| undefined` | Workflow spec version of the responding deployment |
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Queue
|
|
3
|
-
description: Low-level queue interface for dispatching workflow
|
|
3
|
+
description: Low-level queue interface for dispatching workflow invocations.
|
|
4
4
|
type: reference
|
|
5
5
|
summary: "Methods: getDeploymentId(), queue(), createQueueHandler(). Internal queue dispatch — normally handled by the SDK."
|
|
6
6
|
prerequisites:
|
|
@@ -17,10 +17,10 @@ keywords:
|
|
|
17
17
|
- queue dispatch
|
|
18
18
|
---
|
|
19
19
|
|
|
20
|
-
Queue methods live directly on the `world` object (not nested). They dispatch internal workflow
|
|
20
|
+
Queue methods live directly on the `world` object (not nested). They dispatch internal workflow invocations, including queued step work, to the queue backend.
|
|
21
21
|
|
|
22
22
|
<Callout type="warn">
|
|
23
|
-
These methods are used internally by the Workflow SDK to dispatch execution. You do not need to call them in normal operations — use [`start()`](/docs/api-reference/workflow-api/start) to trigger workflows instead. Direct queue access is only needed if you programmatically create a run via `world.events.create()` with a `run_created` event and need to kick off its initial execution, or for debugging
|
|
23
|
+
These methods are used internally by the Workflow SDK to dispatch execution. You do not need to call them in normal operations — use [`start()`](/docs/api-reference/workflow-api/start) to trigger workflows instead. Direct queue access is only needed if you programmatically create a run via `world.events.create()` with a `run_created` event and need to kick off its initial execution, or for debugging flow resumption.
|
|
24
24
|
</Callout>
|
|
25
25
|
|
|
26
26
|
## Import
|
|
@@ -46,7 +46,7 @@ const deploymentId = await world.getDeploymentId(); // [!code highlight]
|
|
|
46
46
|
|
|
47
47
|
### queue()
|
|
48
48
|
|
|
49
|
-
Dispatch a message to a named queue. The message payload is an internal SDK type (`WorkflowInvokePayload
|
|
49
|
+
Dispatch a message to a named queue. The message payload is an internal SDK type (`WorkflowInvokePayload` or `HealthCheckPayload`). Queued steps use `WorkflowInvokePayload` with `stepId` and `stepName`.
|
|
50
50
|
|
|
51
51
|
```typescript lineNumbers
|
|
52
52
|
const { messageId } = await world.queue(queueName, payload, opts); // [!code highlight]
|
|
@@ -42,6 +42,17 @@ Accepted values:
|
|
|
42
42
|
- Set `1` to expose the workflow manifest at `/.well-known/workflow/v1/manifest.json`.
|
|
43
43
|
- Useful for e2e tests and tools that need to discover workflows over HTTP.
|
|
44
44
|
|
|
45
|
+
## Discovery
|
|
46
|
+
|
|
47
|
+
### `WORKFLOW_DISCOVER_NODE_MODULES`
|
|
48
|
+
|
|
49
|
+
- Framework option: `discoverWorkflowsInNodeModules` where supported
|
|
50
|
+
- Default: enabled
|
|
51
|
+
- Controls whether workflow discovery descends into `node_modules`. By default, dependencies that declare a `workflow`/`@workflow/*` dependency can ship `"use workflow"`/`"use step"` files that are discovered and compiled into your app's bundles.
|
|
52
|
+
- Set `0` or `false` to opt out — imports from your application code that resolve into `node_modules` are not followed, so the build never reads, scans, or descends into dependency file graphs. This skips the cost of scanning `node_modules` and stops third-party workflow/step/serde code from being discovered. Useful when a dependency ships workflow code you don't want compiled into your app, or trips discovery with directive strings you don't intend to run.
|
|
53
|
+
- The SDK's own runtime serde classes (e.g. `Run`) stay registered — they are reached through a seeded entry point, and imports *within* `node_modules` are still followed.
|
|
54
|
+
- Explicit framework config wins over this environment variable.
|
|
55
|
+
|
|
45
56
|
## Development diagnostics
|
|
46
57
|
|
|
47
58
|
### `WORKFLOW_DEV_HMR_LOGS`
|
|
@@ -49,3 +60,11 @@ Accepted values:
|
|
|
49
60
|
- Default: disabled
|
|
50
61
|
- Set `1` to log workflow rebuild activity during `next dev`.
|
|
51
62
|
- Useful for diagnosing watch and HMR issues.
|
|
63
|
+
|
|
64
|
+
### `WORKFLOW_DEV_WATCH_IGNORED_PATHS`
|
|
65
|
+
|
|
66
|
+
- Default: unset
|
|
67
|
+
- Dev-mode only (`next dev`). Comma-separated list of path fragments the file watcher should never watch, in addition to the built-in ignores and your project's `.gitignore`.
|
|
68
|
+
- Each entry is matched as a substring of the absolute path (e.g. `/fixtures/,/generated/`).
|
|
69
|
+
- The watcher already respects `.gitignore` (walking from the app directory up to the workspace root). Use this variable only for large directories you cannot or do not want to add to `.gitignore`.
|
|
70
|
+
- Useful when a project has thousands of non-ignored directories and `next dev` fails with `EMFILE: too many open files, watch`.
|
|
@@ -115,6 +115,14 @@ For example, a workflow can run a 10-minute inline step even with `WORKFLOW_REPL
|
|
|
115
115
|
- Debug log filter with wildcards and negation.
|
|
116
116
|
- Examples: `workflow:*`, `workflow:*,-workflow:telemetry:*`.
|
|
117
117
|
|
|
118
|
+
## Transport
|
|
119
|
+
|
|
120
|
+
### `WORKFLOW_H2_MULTIPLEX`
|
|
121
|
+
|
|
122
|
+
- Default: enabled
|
|
123
|
+
- On the Vercel World, lets concurrent event-log requests share one HTTP/2 connection instead of one connection per in-flight request.
|
|
124
|
+
- Set `0` to send one event request per connection.
|
|
125
|
+
|
|
118
126
|
## Queue namespace
|
|
119
127
|
|
|
120
128
|
### `WORKFLOW_QUEUE_NAMESPACE`
|
|
@@ -130,9 +138,9 @@ These variables are primarily for tests, debugging, or unusual deployments.
|
|
|
130
138
|
|
|
131
139
|
### `WORKFLOW_STREAM_FLUSH_INTERVAL_MS`
|
|
132
140
|
|
|
133
|
-
- Default: `
|
|
134
|
-
-
|
|
135
|
-
- Also available as `streamFlushIntervalMs` on Worlds that expose it.
|
|
141
|
+
- Default: `0` (dispatch the first chunk of an idle stream immediately)
|
|
142
|
+
- Group-commit window for the *leading* chunk of an idle stream. `0` sends it at once; a positive value holds it up to that many milliseconds to collect a group — an opt-in trade of first-chunk latency for larger batches, useful for slow-but-steady producers. Chunks arriving while a request is already in flight always coalesce into the next group regardless of this setting.
|
|
143
|
+
- Also available as `streamFlushIntervalMs` on Worlds that expose it (the env var, when set, takes precedence over the World option).
|
|
136
144
|
|
|
137
145
|
### `WORKFLOW_STREAM_MAX_INFLIGHT_CHUNKS`
|
|
138
146
|
|
|
@@ -149,6 +157,11 @@ These variables are primarily for tests, debugging, or unusual deployments.
|
|
|
149
157
|
- Default: `1048576` (1 MiB)
|
|
150
158
|
- Wire limit: maximum cumulative bytes in a single coalesced multi-write, so large chunks don't produce a request body that platform limits reject. A single chunk larger than this is still sent on its own.
|
|
151
159
|
|
|
160
|
+
### `WORKFLOW_STREAM_MAX_BUFFERED_BYTES`
|
|
161
|
+
|
|
162
|
+
- Default: `8388608` (8 MiB)
|
|
163
|
+
- Flow-control bound: maximum cumulative bytes accepted into the stream writer's group-commit buffer before `write()` applies backpressure. The byte-denominated counterpart of `WORKFLOW_STREAM_MAX_INFLIGHT_CHUNKS`; distinct from `WORKFLOW_STREAM_MAX_BYTES_PER_BATCH`, which only splits how much goes out in one request.
|
|
164
|
+
|
|
152
165
|
### `WORKFLOW_FRAMED_STREAM_MAX_RECONNECTS`
|
|
153
166
|
|
|
154
167
|
- Default: `50`
|
|
@@ -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 **runs**, from the environment of the process serving it, so changing `WORKFLOW_TARGET_WORLD` takes effect on the next start without a rebuild. Detection keys off `VERCEL_DEPLOYMENT_ID`, which Vercel sets in every deployed function and nothing else sets: with it, the Vercel World; without it, the Local World.
|
|
25
|
+
|
|
26
|
+
Broader signals are deliberately ignored. `vercel env pull` writes `VERCEL=1` into `.env.local`, so a dev server or a production server started on your own machine sees it while running against a writable filesystem — where the Local World is the right choice. Set `WORKFLOW_TARGET_WORLD=vercel` explicitly if you want such a process to talk to the Vercel World; starting a run then fails with an error naming the missing `VERCEL_DEPLOYMENT_ID`.
|
|
27
|
+
|
|
28
|
+
A deployment that pins `WORKFLOW_TARGET_WORLD=local` warns at startup and fails on its first write, because a Vercel deployment's filesystem is read-only.
|
|
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
|
|
|
@@ -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
|
|
@@ -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
|
|
|
@@ -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
|
|
|
@@ -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
|
|
|
@@ -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
|
|
|
@@ -289,7 +289,7 @@ async function sendOnboardingEmail(user: { id: string; email: string }) {
|
|
|
289
289
|
|
|
290
290
|
Taking a look at this code:
|
|
291
291
|
|
|
292
|
-
- 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.
|
|
293
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).
|
|
294
294
|
- Steps can throw a `FatalError` if an error is intentional and should not be retried.
|
|
295
295
|
|
|
@@ -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
|
|
|
@@ -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
|
|
|
@@ -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
|
|
|
@@ -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
|
|
|
@@ -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
|
|
|
@@ -176,12 +176,12 @@ The step's `ops` array is awaited via `waitUntil(Promise.all(ops))` after the st
|
|
|
176
176
|
|
|
177
177
|
### Abort Errors Are Wrapped in FatalError
|
|
178
178
|
|
|
179
|
-
When a step throws due to an abort — whether from `fetch` throwing `AbortError`, `signal.throwIfAborted()`, or any other abort-induced error — the step
|
|
179
|
+
When a step throws due to an abort — whether from `fetch` throwing `AbortError`, `signal.throwIfAborted()`, or any other abort-induced error — the step executor wraps the error in `FatalError` before recording it in the event log. This ensures:
|
|
180
180
|
|
|
181
181
|
- **No retries**: An abort is intentional cancellation, not a transient failure. Retrying would just abort again.
|
|
182
182
|
- **Immediate propagation**: The error bubbles up to the workflow as a `FatalError`, which the workflow can catch with `FatalError.is(err)`.
|
|
183
183
|
|
|
184
|
-
The wrapping happens
|
|
184
|
+
The wrapping happens in `runtime/step-executor.ts` during error hydration. When the step's thrown error is an `AbortError` (checked via `err.name === 'AbortError'`), it is treated as fatal regardless of the step's `maxRetries` configuration.
|
|
185
185
|
|
|
186
186
|
### abort() in the Workflow
|
|
187
187
|
|
|
@@ -53,18 +53,20 @@ flowchart LR
|
|
|
53
53
|
A["Source Code<br/>with directives"] --> B["Step Mode"]
|
|
54
54
|
A --> C["Workflow Mode"]
|
|
55
55
|
A --> D["Client Mode"]
|
|
56
|
-
B --> E["
|
|
57
|
-
C --> F["
|
|
56
|
+
B --> E["Step registration bundle"]
|
|
57
|
+
C --> F["Workflow bundle"]
|
|
58
|
+
E --> H["Combined flow handler"]
|
|
59
|
+
F --> H
|
|
58
60
|
D --> G["Your App Code<br/>(Enables `start`)"]
|
|
59
61
|
```
|
|
60
62
|
|
|
61
63
|
### Comparison Table
|
|
62
64
|
|
|
63
|
-
| Mode | Used In | Purpose |
|
|
64
|
-
|
|
65
|
-
| Step | Build time |
|
|
66
|
-
| Workflow | Build time | Bundles workflow orchestrators | `.well-known/workflow/v1/flow`
|
|
67
|
-
| Client | Build/Runtime | Provides workflow IDs and types to `start`
|
|
65
|
+
| Mode | Used In | Purpose | Runtime role | Required? |
|
|
66
|
+
|----------|------------|--------------------------------|--------------|-----------|
|
|
67
|
+
| Step | Build time | Registers executable step functions | Imported by the combined flow handler | Yes |
|
|
68
|
+
| Workflow | Build time | Bundles workflow orchestrators | Executed by `.well-known/workflow/v1/flow` | Yes |
|
|
69
|
+
| Client | Build/Runtime | Provides workflow IDs and types to `start` | Your application code | Optional* |
|
|
68
70
|
|
|
69
71
|
\* Client mode is **recommended** for better developer experience—it provides automatic ID generation and type safety. Without it, you must manually construct workflow IDs or use the build manifest.
|
|
70
72
|
|
|
@@ -73,7 +75,7 @@ flowchart LR
|
|
|
73
75
|
<Tabs items={["Step Mode", "Workflow Mode", "Client Mode"]}>
|
|
74
76
|
<Tab value="Step Mode">
|
|
75
77
|
|
|
76
|
-
**Step Mode** creates the
|
|
78
|
+
**Step Mode** creates a registration bundle that the combined flow handler imports. It is not an HTTP route.
|
|
77
79
|
|
|
78
80
|
**Input:**
|
|
79
81
|
|
|
@@ -211,7 +213,7 @@ The IDs are generated exactly like in workflow mode to ensure they can be direct
|
|
|
211
213
|
|
|
212
214
|
## Generated Files
|
|
213
215
|
|
|
214
|
-
When you build your application, the Workflow SDK generates
|
|
216
|
+
When you build your application, the Workflow SDK generates a combined flow handler, an internal step registration bundle, and a webhook handler. Exact filenames vary by framework.
|
|
215
217
|
|
|
216
218
|
### `flow.js`
|
|
217
219
|
|
|
@@ -245,15 +247,17 @@ Most invalid patterns cause **build-time errors**, catching issues before deploy
|
|
|
245
247
|
**Why a VM?** Workflow functions must be deterministic to support replay. The VM sandbox prevents accidental use of non-deterministic APIs or side effects. All side effects should be performed in [step functions](/docs/foundations/workflows-and-steps#step-functions) instead.
|
|
246
248
|
</Callout>
|
|
247
249
|
|
|
248
|
-
### `
|
|
250
|
+
### `__step_registrations.js`
|
|
249
251
|
|
|
250
|
-
Contains all step functions transformed in **step mode**.
|
|
252
|
+
Contains all step functions transformed in **step mode**. The combined flow handler imports this module for its registration side effects.
|
|
251
253
|
|
|
252
254
|
**What it does:**
|
|
253
255
|
|
|
254
|
-
-
|
|
255
|
-
-
|
|
256
|
-
-
|
|
256
|
+
- Adds step functions to the runtime step registry
|
|
257
|
+
- Keeps step bodies in the full host runtime
|
|
258
|
+
- Makes registered steps available when a flow queue message includes `stepId` and `stepName`
|
|
259
|
+
|
|
260
|
+
This module must not be exposed as an HTTP endpoint.
|
|
257
261
|
|
|
258
262
|
### `webhook.js`
|
|
259
263
|
|
|
@@ -336,7 +340,7 @@ These transformations are framework-agnostic—they output standard JavaScript t
|
|
|
336
340
|
|
|
337
341
|
If you need to debug transformation issues, you can inspect the generated files:
|
|
338
342
|
|
|
339
|
-
1. **
|
|
343
|
+
1. **Inspect the generated output**: Check the combined flow handler, step registration bundle, webhook handler, and emitted debug files.
|
|
340
344
|
2. **Check build logs**: Most frameworks log transformation activity during builds
|
|
341
345
|
3. **Verify directives**: Ensure `"use workflow"` and `"use step"` are the first statements in functions
|
|
342
346
|
4. **Check file locations**: Transformations only apply to files in configured source directories
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
title: Framework Integrations
|
|
3
3
|
description: Guide for framework authors to integrate Workflow SDK with custom frameworks or runtimes.
|
|
4
4
|
type: guide
|
|
5
|
-
summary: Build a custom framework integration using the Workflow SDK compiler and runtime.
|
|
5
|
+
summary: Build a custom framework integration using the Workflow SDK compiler and combined runtime handler.
|
|
6
6
|
prerequisites:
|
|
7
7
|
- /docs/foundations/workflows-and-steps
|
|
8
8
|
related:
|
|
@@ -10,55 +10,45 @@ related:
|
|
|
10
10
|
---
|
|
11
11
|
|
|
12
12
|
<Callout>
|
|
13
|
-
**For users:** If you just want to use Workflow SDK with an existing framework,
|
|
13
|
+
**For users:** If you just want to use Workflow SDK with an existing framework, see [Getting Started](/docs/getting-started). This page is for framework authors.
|
|
14
14
|
</Callout>
|
|
15
15
|
|
|
16
|
-
This guide
|
|
16
|
+
This guide uses Bun as a concrete example, but the same build and routing model applies to other JavaScript frameworks and runtimes.
|
|
17
17
|
|
|
18
18
|
<Callout type="info">
|
|
19
|
-
|
|
19
|
+
Read [How the Directives Work](/docs/how-it-works/code-transform) first if you are not familiar with the three compiler modes.
|
|
20
20
|
</Callout>
|
|
21
21
|
|
|
22
|
-
##
|
|
22
|
+
## Integration shape
|
|
23
23
|
|
|
24
|
-
A framework integration has two
|
|
24
|
+
A framework integration has two responsibilities:
|
|
25
25
|
|
|
26
|
-
1. **Build
|
|
27
|
-
2. **Runtime
|
|
26
|
+
1. **Build time:** transform workflow and step code, register the steps, and generate the combined flow handler.
|
|
27
|
+
2. **Runtime:** expose the generated flow and webhook handlers as HTTP routes.
|
|
28
28
|
|
|
29
29
|
```mermaid
|
|
30
30
|
flowchart TD
|
|
31
|
-
A["Source
|
|
32
|
-
B --> C["
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
C -->
|
|
36
|
-
|
|
37
|
-
E -->
|
|
38
|
-
|
|
39
|
-
F --> I["Used by framework loader"]
|
|
40
|
-
G --> J["HTTP Server<br/>(Your Runtime)"]
|
|
41
|
-
H --> J
|
|
42
|
-
L --> J
|
|
31
|
+
A["Source code<br/>'use workflow' / 'use step'"] --> B["Workflow builder"]
|
|
32
|
+
B --> C["Workflow-mode bundle"]
|
|
33
|
+
B --> D["Step registration bundle"]
|
|
34
|
+
D --> E["Combined flow handler"]
|
|
35
|
+
C --> E
|
|
36
|
+
B --> F["Webhook handler"]
|
|
37
|
+
E --> G["POST /.well-known/workflow/v1/flow"]
|
|
38
|
+
F --> H["/.well-known/workflow/v1/webhook/:token"]
|
|
43
39
|
|
|
44
40
|
style B fill:#a78bfa,stroke:#8b5cf6,color:#000
|
|
45
|
-
style
|
|
46
|
-
style
|
|
41
|
+
style G fill:#a78bfa,stroke:#8b5cf6,color:#000
|
|
42
|
+
style H fill:#a78bfa,stroke:#8b5cf6,color:#000
|
|
47
43
|
```
|
|
48
44
|
|
|
49
|
-
|
|
45
|
+
Steps do not have their own HTTP route. A queued step invocation contains `stepId` and `stepName` and is delivered to the flow handler on the workflow queue. The handler executes the registered step in the full host runtime, then replays the workflow in its sandboxed VM.
|
|
50
46
|
|
|
51
|
-
## Example: Bun
|
|
47
|
+
## Example: Bun integration
|
|
52
48
|
|
|
53
|
-
|
|
49
|
+
### 1. Generate the bundles
|
|
54
50
|
|
|
55
|
-
|
|
56
|
-
A working example can be [found here](https://github.com/vercel/workflow-examples/tree/main/custom-adapter). For a production-ready reference, see the [Next.js integration](https://github.com/vercel/workflow/tree/main/packages/next).
|
|
57
|
-
</Callout>
|
|
58
|
-
|
|
59
|
-
### Step 1: Generate Handler Files
|
|
60
|
-
|
|
61
|
-
Use the `workflow` CLI to generate the handler bundles. The CLI scans your `workflows/` directory and creates `flow.js`, `step.js`, and `webhook.js`.
|
|
51
|
+
The standalone CLI scans `workflows/` and creates the combined flow handler, an internal step registration module, and the webhook handler.
|
|
62
52
|
|
|
63
53
|
```json title="package.json"
|
|
64
54
|
{
|
|
@@ -68,21 +58,19 @@ Use the `workflow` CLI to generate the handler bundles. The CLI scans your `work
|
|
|
68
58
|
}
|
|
69
59
|
```
|
|
70
60
|
|
|
71
|
-
|
|
72
|
-
**For production integrations:** Instead of using the CLI, extend the `BaseBuilder` class directly in your framework plugin. This gives you control over file watching, custom output paths, and framework-specific hooks. See the [Next.js plugin](https://github.com/vercel/workflow/tree/main/packages/next) for an example.
|
|
73
|
-
</Callout>
|
|
74
|
-
|
|
75
|
-
**What gets generated:**
|
|
61
|
+
The default output is:
|
|
76
62
|
|
|
77
|
-
- `/.well-known/workflow/v1/flow.
|
|
78
|
-
- `/.well-known/workflow/v1/
|
|
79
|
-
- `/.well-known/workflow/v1/webhook.
|
|
63
|
+
- `/.well-known/workflow/v1/flow.mjs` — the combined workflow and step queue consumer
|
|
64
|
+
- `/.well-known/workflow/v1/__step_registrations.mjs` — an internal module imported by `flow.mjs`; do not route to it
|
|
65
|
+
- `/.well-known/workflow/v1/webhook.mjs` — webhook delivery
|
|
80
66
|
|
|
81
|
-
|
|
67
|
+
<Callout>
|
|
68
|
+
Production integrations should extend `BaseBuilder` from `@workflow/builders` so they can participate in the framework's build, watch, and routing lifecycle.
|
|
69
|
+
</Callout>
|
|
82
70
|
|
|
83
|
-
###
|
|
71
|
+
### 2. Add the client transform
|
|
84
72
|
|
|
85
|
-
Client mode
|
|
73
|
+
Client mode gives application code the workflow IDs used by `start()` and prevents accidental direct workflow execution.
|
|
86
74
|
|
|
87
75
|
{/* @skip-typecheck: incomplete code sample */}
|
|
88
76
|
```typescript title="workflow-plugin.ts" lineNumbers
|
|
@@ -94,8 +82,6 @@ plugin({
|
|
|
94
82
|
setup(build) {
|
|
95
83
|
build.onLoad({ filter: /\.(ts|tsx|js|jsx)$/ }, async (args) => {
|
|
96
84
|
const source = await Bun.file(args.path).text();
|
|
97
|
-
|
|
98
|
-
// Optimization: Skip files that do not have any directives
|
|
99
85
|
if (!source.match(/(use step|use workflow)/)) {
|
|
100
86
|
return { contents: source };
|
|
101
87
|
}
|
|
@@ -105,7 +91,7 @@ plugin({
|
|
|
105
91
|
jsc: {
|
|
106
92
|
experimental: {
|
|
107
93
|
plugins: [
|
|
108
|
-
[require.resolve("@workflow/swc-plugin"), { mode: "client" }],
|
|
94
|
+
[require.resolve("@workflow/swc-plugin"), { mode: "client" }],
|
|
109
95
|
],
|
|
110
96
|
},
|
|
111
97
|
},
|
|
@@ -123,23 +109,14 @@ Activate the plugin in `bunfig.toml`:
|
|
|
123
109
|
preload = ["./workflow-plugin.ts"]
|
|
124
110
|
```
|
|
125
111
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
- Attaches workflow IDs to functions for use with `start()`
|
|
129
|
-
- Provides TypeScript type safety
|
|
130
|
-
- Prevents accidental direct execution of workflows
|
|
131
|
-
|
|
132
|
-
**Why optional?** Without client mode, you can still use workflows by manually constructing IDs or referencing the build manifest.
|
|
133
|
-
|
|
134
|
-
### Step 3: Expose HTTP Endpoints
|
|
112
|
+
### 3. Expose the HTTP routes
|
|
135
113
|
|
|
136
|
-
|
|
114
|
+
Only the combined flow handler and webhook handler are routable:
|
|
137
115
|
|
|
138
116
|
{/* @skip-typecheck: incomplete code sample */}
|
|
139
117
|
```typescript title="server.ts" lineNumbers
|
|
140
|
-
import flow from "./.well-known/workflow/v1/flow.
|
|
141
|
-
import
|
|
142
|
-
import * as webhook from "./.well-known/workflow/v1/webhook.js";
|
|
118
|
+
import * as flow from "./.well-known/workflow/v1/flow.mjs";
|
|
119
|
+
import * as webhook from "./.well-known/workflow/v1/webhook.mjs";
|
|
143
120
|
|
|
144
121
|
import { start } from "workflow/api";
|
|
145
122
|
import { handleUserSignup } from "./workflows/user-signup.js";
|
|
@@ -148,23 +125,14 @@ const server = Bun.serve({
|
|
|
148
125
|
port: process.env.PORT,
|
|
149
126
|
routes: {
|
|
150
127
|
"/.well-known/workflow/v1/flow": {
|
|
151
|
-
POST:
|
|
128
|
+
POST: flow.POST,
|
|
152
129
|
},
|
|
153
|
-
"/.well-known/workflow/v1/step": {
|
|
154
|
-
POST: (req) => step.POST(req),
|
|
155
|
-
},
|
|
156
|
-
// webhook exports handlers for GET, POST, DELETE, etc.
|
|
157
130
|
"/.well-known/workflow/v1/webhook/:token": webhook,
|
|
158
|
-
|
|
159
|
-
// Example: Start a workflow
|
|
160
131
|
"/": {
|
|
161
|
-
GET: async (
|
|
132
|
+
GET: async () => {
|
|
162
133
|
const email = `test-${crypto.randomUUID()}@test.com`;
|
|
163
134
|
const run = await start(handleUserSignup, [email]);
|
|
164
|
-
return Response.json({
|
|
165
|
-
message: "User signup workflow started",
|
|
166
|
-
runId: run.runId,
|
|
167
|
-
});
|
|
135
|
+
return Response.json({ runId: run.runId });
|
|
168
136
|
},
|
|
169
137
|
},
|
|
170
138
|
},
|
|
@@ -173,222 +141,90 @@ const server = Bun.serve({
|
|
|
173
141
|
console.log(`Server listening on http://localhost:${server.port}`);
|
|
174
142
|
```
|
|
175
143
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
## Understanding the Endpoints
|
|
179
|
-
|
|
180
|
-
Your integration must expose three HTTP endpoints. The generated handlers manage all protocol details—you just route requests.
|
|
144
|
+
## Runtime routes
|
|
181
145
|
|
|
182
|
-
###
|
|
146
|
+
### Combined flow endpoint
|
|
183
147
|
|
|
184
148
|
**Route:** `POST /.well-known/workflow/v1/flow`
|
|
185
149
|
|
|
186
|
-
|
|
150
|
+
The handler consumes every workflow queue message. Depending on the payload and event log, it can:
|
|
187
151
|
|
|
188
|
-
|
|
152
|
+
- start or replay workflow orchestration in the sandboxed VM;
|
|
153
|
+
- execute a queued step in the host runtime;
|
|
154
|
+
- continue replay inline after a step completes;
|
|
155
|
+
- resume a run after a hook, webhook, sleep, retry, or recovery event.
|
|
189
156
|
|
|
190
|
-
|
|
191
|
-
- Resuming after a step completes
|
|
192
|
-
- Resuming after a webhook or hook triggers
|
|
193
|
-
- Recovering from failures
|
|
157
|
+
### Webhook endpoint
|
|
194
158
|
|
|
195
|
-
|
|
159
|
+
**Route:** `/.well-known/workflow/v1/webhook/:token`
|
|
196
160
|
|
|
197
|
-
|
|
161
|
+
This handler delivers data to [`createWebhook()`](/docs/api-reference/workflow/create-webhook). Its generated file structure varies by framework; for example, Next.js uses `webhook/[token]/route.js`.
|
|
198
162
|
|
|
199
|
-
|
|
163
|
+
## Building with `BaseBuilder`
|
|
200
164
|
|
|
201
|
-
|
|
165
|
+
Use `createCombinedBundle()` so the flow bundle imports the generated step registrations. Calling `createWorkflowsBundle()` and `createStepsBundle()` independently does not create a complete runtime route.
|
|
202
166
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
<Callout type="info">
|
|
208
|
-
The webhook file structure varies by framework. Next.js generates `webhook/[token]/route.js` to leverage App Router's dynamic routing, while other frameworks generate a single `webhook.js` handler.
|
|
209
|
-
</Callout>
|
|
210
|
-
|
|
211
|
-
## Adapting to Other Frameworks
|
|
212
|
-
|
|
213
|
-
The Bun example demonstrates the core pattern. To adapt for your framework:
|
|
214
|
-
|
|
215
|
-
### Build-Time
|
|
216
|
-
|
|
217
|
-
**Option 1: Use the CLI** (simplest)
|
|
218
|
-
|
|
219
|
-
```bash
|
|
220
|
-
workflow build
|
|
221
|
-
```
|
|
222
|
-
|
|
223
|
-
This will default to scanning the `./workflows` top-level directory for workflow files, and will output bundled files directly into your working directory.
|
|
224
|
-
|
|
225
|
-
**Option 2: Extend `BaseBuilder`** (recommended)
|
|
226
|
-
|
|
227
|
-
{/* @skip-typecheck: @workflow/cli internal module */}
|
|
228
|
-
```typescript lineNumbers
|
|
229
|
-
import { BaseBuilder } from "@workflow/cli/dist/lib/builders/base-builder";
|
|
167
|
+
{/* @skip-typecheck: incomplete framework adapter */}
|
|
168
|
+
```typescript title="my-framework-builder.ts" lineNumbers
|
|
169
|
+
import { join } from "node:path";
|
|
170
|
+
import { BaseBuilder } from "@workflow/builders";
|
|
230
171
|
|
|
231
172
|
class MyFrameworkBuilder extends BaseBuilder {
|
|
232
|
-
constructor(options) {
|
|
233
|
-
super({
|
|
234
|
-
dirs: ["workflows"],
|
|
235
|
-
workingDir: options.rootDir,
|
|
236
|
-
watch: options.dev,
|
|
237
|
-
});
|
|
238
|
-
}
|
|
239
|
-
|
|
240
173
|
override async build(): Promise<void> {
|
|
241
174
|
const inputFiles = await this.getInputFiles();
|
|
175
|
+
const tsconfigPath = await this.findTsConfigPath();
|
|
176
|
+
const outputDir = join(this.config.workingDir, ".workflow");
|
|
242
177
|
|
|
243
|
-
await this.
|
|
244
|
-
outfile: "/path/to/.well-known/workflow/v1/flow.js",
|
|
245
|
-
format: "esm",
|
|
178
|
+
await this.createCombinedBundle({
|
|
246
179
|
inputFiles,
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
outfile: "/path/to/.well-known/workflow/v1/step.js",
|
|
180
|
+
stepsOutfile: join(outputDir, "__step_registrations.mjs"),
|
|
181
|
+
flowOutfile: join(outputDir, "flow.mjs"),
|
|
182
|
+
tsconfigPath,
|
|
251
183
|
format: "esm",
|
|
252
|
-
inputFiles,
|
|
253
184
|
});
|
|
254
185
|
|
|
255
186
|
await this.createWebhookBundle({
|
|
256
|
-
outfile: "
|
|
187
|
+
outfile: join(outputDir, "webhook.mjs"),
|
|
257
188
|
});
|
|
258
189
|
}
|
|
259
190
|
}
|
|
260
191
|
```
|
|
261
192
|
|
|
262
|
-
If
|
|
263
|
-
|
|
264
|
-
### Monorepos and Workspace Imports
|
|
265
|
-
|
|
266
|
-
If your framework integration lives in a subdirectory and your workflows import code from sibling workspace packages, pass `projectRoot` to `BaseBuilder`. Use the smallest directory that contains every workspace package imported by your workflows.
|
|
267
|
-
|
|
268
|
-
{/* @skip-typecheck: @workflow/cli internal module */}
|
|
269
|
-
```typescript title="my-framework-builder.ts" lineNumbers
|
|
270
|
-
import { BaseBuilder } from "@workflow/cli/dist/lib/builders/base-builder";
|
|
193
|
+
If workflows import sibling workspace packages, set `projectRoot` to the smallest directory containing all imported packages.
|
|
271
194
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
dirs: ["workflows"],
|
|
280
|
-
workingDir: options.rootDir,
|
|
281
|
-
projectRoot: options.workspaceRoot ?? options.rootDir, // [!code highlight]
|
|
282
|
-
watch: options.dev,
|
|
283
|
-
});
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
override async build(): Promise<void> {
|
|
287
|
-
const inputFiles = await this.getInputFiles();
|
|
288
|
-
// ...
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
```
|
|
292
|
-
|
|
293
|
-
Hook into your framework's build:
|
|
294
|
-
|
|
295
|
-
{/* @skip-typecheck: incomplete code sample */}
|
|
296
|
-
```typescript title="pseudocode.ts" lineNumbers
|
|
297
|
-
framework.hooks.hook("build:before", async () => {
|
|
298
|
-
await new MyFrameworkBuilder(framework).build();
|
|
195
|
+
{/* @skip-typecheck: partial constructor configuration */}
|
|
196
|
+
```typescript
|
|
197
|
+
super({
|
|
198
|
+
dirs: ["workflows"],
|
|
199
|
+
workingDir: options.rootDir,
|
|
200
|
+
projectRoot: options.workspaceRoot ?? options.rootDir,
|
|
201
|
+
watch: options.dev,
|
|
299
202
|
});
|
|
300
203
|
```
|
|
301
204
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
Add a loader/plugin for your bundler:
|
|
305
|
-
|
|
306
|
-
**Rollup/Vite:**
|
|
307
|
-
|
|
308
|
-
```typescript lineNumbers
|
|
309
|
-
export function workflowPlugin() {
|
|
310
|
-
return {
|
|
311
|
-
name: "workflow-client-transform",
|
|
312
|
-
async transform(code, id) {
|
|
313
|
-
if (!code.match(/(use step|use workflow)/)) return null;
|
|
205
|
+
Your integration can expose physical files, virtual modules, or framework-native routes. In every case, the step registration module is a dependency of the flow handler, not a route of its own.
|
|
314
206
|
|
|
315
|
-
|
|
316
|
-
filename: id,
|
|
317
|
-
jsc: {
|
|
318
|
-
experimental: {
|
|
319
|
-
plugins: [[require.resolve("@workflow/swc-plugin"), { mode: "client" }]], // [!code highlight]
|
|
320
|
-
},
|
|
321
|
-
},
|
|
322
|
-
});
|
|
207
|
+
## Vercel queue configuration
|
|
323
208
|
|
|
324
|
-
|
|
325
|
-
},
|
|
326
|
-
};
|
|
327
|
-
}
|
|
328
|
-
```
|
|
209
|
+
On Vercel, configure the flow function as the sole queue consumer. `getWorkflowQueueTrigger()` handles the optional queue namespace and `WORKFLOW_SEQUENTIAL_REPLAYS=1` behavior.
|
|
329
210
|
|
|
330
|
-
|
|
211
|
+
```typescript
|
|
212
|
+
import { getWorkflowQueueTrigger } from "@workflow/builders";
|
|
331
213
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
rules: [
|
|
336
|
-
{
|
|
337
|
-
test: /\.(ts|tsx|js|jsx)$/,
|
|
338
|
-
use: "workflow-client-loader", // Similar implementation
|
|
339
|
-
},
|
|
340
|
-
],
|
|
341
|
-
},
|
|
214
|
+
const flowConfig = {
|
|
215
|
+
maxDuration: "max",
|
|
216
|
+
experimentalTriggers: [getWorkflowQueueTrigger()],
|
|
342
217
|
};
|
|
343
218
|
```
|
|
344
219
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
Route the three endpoints to the generated handlers. The exact implementation depends on your framework's routing API.
|
|
348
|
-
|
|
349
|
-
In the bun example above, we left routing to the user. Essentially, the user has to serve routes like this:
|
|
220
|
+
The generated trigger listens to one topic family:
|
|
350
221
|
|
|
351
|
-
|
|
352
|
-
```typescript title="server.ts" lineNumbers
|
|
353
|
-
import flow from "./.well-known/workflow/v1/flow.js";
|
|
354
|
-
import step from "./.well-known/workflow/v1/step.js";
|
|
355
|
-
import * as webhook from "./.well-known/workflow/v1/webhook.js";
|
|
356
|
-
|
|
357
|
-
// Expose the 3 generated routes
|
|
358
|
-
const server = Bun.serve({
|
|
359
|
-
routes: {
|
|
360
|
-
"/.well-known/workflow/v1/flow": {
|
|
361
|
-
POST: (req) => flow.POST(req),
|
|
362
|
-
},
|
|
363
|
-
"/.well-known/workflow/v1/step": {
|
|
364
|
-
POST: (req) => step.POST(req),
|
|
365
|
-
},
|
|
366
|
-
// webhook exports handlers for GET, POST, DELETE, etc.
|
|
367
|
-
"/.well-known/workflow/v1/webhook/:token": webhook,
|
|
368
|
-
},
|
|
369
|
-
});
|
|
370
|
-
```
|
|
371
|
-
|
|
372
|
-
Production framework integrations should handle this routing in the plugin instead of leaving it to the user, and this depends on each framework's unique implementaiton.
|
|
373
|
-
Check the Workflow SDK source code for examples of production framework implementations.
|
|
374
|
-
In the future, the Workflow SDK will emit more routes under the `.well-known/workflow` namespace.
|
|
375
|
-
|
|
376
|
-
## Security
|
|
377
|
-
|
|
378
|
-
The workflow and step handler endpoints are invoked by the world's queuing infrastructure, not by end users. How they're secured depends on which world you're deploying to.
|
|
379
|
-
|
|
380
|
-
### Vercel (`@workflow/world-vercel`)
|
|
381
|
-
|
|
382
|
-
On Vercel, workflow handler functions are not accessible through public endpoints. Handlers use the same [consumer function security](https://vercel.com/docs/queues/concepts#consumer-function-security) mechanism that secures [Vercel Queues](https://vercel.com/docs/queues) consumers.
|
|
383
|
-
|
|
384
|
-
During the build step, the Workflow SDK automatically configures each handler as a queue consumer by writing `experimentalTriggers` to the function's `.vc-config.json`:
|
|
385
|
-
|
|
386
|
-
```json title=".vc-config.json (generated by Workflow SDK)"
|
|
222
|
+
```json title=".vc-config.json (excerpt)"
|
|
387
223
|
{
|
|
388
224
|
"experimentalTriggers": [
|
|
389
225
|
{
|
|
390
226
|
"type": "queue/v2beta",
|
|
391
|
-
"topic": "
|
|
227
|
+
"topic": "__wkf_workflow_*",
|
|
392
228
|
"consumer": "default",
|
|
393
229
|
"retryAfterSeconds": 5,
|
|
394
230
|
"initialDelaySeconds": 0
|
|
@@ -397,110 +233,28 @@ During the build step, the Workflow SDK automatically configures each handler as
|
|
|
397
233
|
}
|
|
398
234
|
```
|
|
399
235
|
|
|
236
|
+
Both orchestration messages and step messages use this topic family. A step message is distinguished by its payload, not by a second topic or function.
|
|
400
237
|
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
| Handler | Topic | Description |
|
|
404
|
-
| --- | --- | --- |
|
|
405
|
-
| `step.func` | `__wkf_step_*` | Step execution (long-running, `maxDuration: max`) |
|
|
406
|
-
| `flow.func` | `__wkf_workflow_*` | Workflow orchestration (`maxDuration: 60`) |
|
|
407
|
-
|
|
408
|
-
If you're building a framework integration that targets Vercel, you should write these triggers into the `.vc-config.json` for each generated function. Use `getWorkflowQueueTrigger()` for flow functions so `WORKFLOW_SEQUENTIAL_REPLAYS=1` is reflected in the generated trigger configuration (it also accepts a `namespace` option, matching `createWorkflowQueueTrigger`); `STEP_QUEUE_TRIGGER` is exported for step functions:
|
|
409
|
-
|
|
410
|
-
```typescript
|
|
411
|
-
import { getWorkflowQueueTrigger, STEP_QUEUE_TRIGGER } from "@workflow/builders";
|
|
412
|
-
|
|
413
|
-
const flowTriggers = [getWorkflowQueueTrigger()];
|
|
414
|
-
const stepTriggers = [STEP_QUEUE_TRIGGER];
|
|
415
|
-
```
|
|
416
|
-
|
|
417
|
-
If your integration constructs the flow trigger object itself instead of calling `getWorkflowQueueTrigger()`, it must add `maxConcurrency: 1` to that trigger when sequential replays are enabled at build time (`WORKFLOW_SEQUENTIAL_REPLAYS=1` — the exported `isSequentialReplaysEnabled()` helper implements this check). The runtime half of the feature (per-run queue topics) activates from the environment variable alone — without the trigger half, those per-run topics are not serialized and the setting only adds queue-topic cardinality.
|
|
418
|
-
|
|
238
|
+
If you construct the trigger yourself, add `maxConcurrency: 1` when `WORKFLOW_SEQUENTIAL_REPLAYS=1`. The exported `isSequentialReplaysEnabled()` helper implements that build-time check.
|
|
419
239
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
For self-hosted or non-Vercel deployments, you are responsible for securing the handler endpoints:
|
|
423
|
-
|
|
424
|
-
- **Framework middleware** — Add authentication (API keys, JWT, OIDC) in front of the `/.well-known/workflow/v1/*` routes
|
|
425
|
-
- **Network-level security** — Deploy handlers behind a VPC, private network, or firewall rules so only your queue infrastructure can reach them
|
|
426
|
-
- **Rate limiting** — Add request validation and rate limiting to prevent abuse
|
|
427
|
-
|
|
428
|
-
Learn more about [building custom Worlds](/worlds/building-a-world).
|
|
429
|
-
|
|
430
|
-
## Testing Your Integration
|
|
431
|
-
|
|
432
|
-
### 1. Test Build Output
|
|
433
|
-
|
|
434
|
-
Create a test workflow:
|
|
435
|
-
|
|
436
|
-
```typescript title="workflows/test.ts" lineNumbers
|
|
437
|
-
import { sleep, createWebhook } from "workflow";
|
|
438
|
-
|
|
439
|
-
export async function handleUserSignup(email: string) {
|
|
440
|
-
"use workflow";
|
|
441
|
-
|
|
442
|
-
const user = await createUser(email);
|
|
443
|
-
await sendWelcomeEmail(user);
|
|
444
|
-
|
|
445
|
-
await sleep("5s");
|
|
446
|
-
|
|
447
|
-
const webhook = createWebhook();
|
|
448
|
-
await sendOnboardingEmail(user, webhook.url);
|
|
449
|
-
|
|
450
|
-
await webhook;
|
|
451
|
-
console.log("Webhook Resolved");
|
|
452
|
-
|
|
453
|
-
return { userId: user.id, status: "onboarded" };
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
async function createUser(email: string) {
|
|
457
|
-
"use step";
|
|
458
|
-
|
|
459
|
-
console.log(`Creating a new user with email: ${email}`);
|
|
460
|
-
|
|
461
|
-
return { id: crypto.randomUUID(), email };
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
async function sendWelcomeEmail(user: { id: string; email: string }) {
|
|
465
|
-
"use step";
|
|
466
|
-
|
|
467
|
-
console.log(`Sending welcome email to user: ${user.id}`);
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
async function sendOnboardingEmail(user: { id: string; email: string }, callback: string) {
|
|
471
|
-
"use step";
|
|
472
|
-
|
|
473
|
-
console.log(`Sending onboarding email to user: ${user.id}`);
|
|
474
|
-
|
|
475
|
-
console.log(`Click this link to resolve the webhook: ${callback}`);
|
|
476
|
-
}
|
|
240
|
+
## Security
|
|
477
241
|
|
|
478
|
-
|
|
242
|
+
On Vercel, the flow handler uses [queue consumer security](https://vercel.com/docs/queues/concepts#consumer-function-security). For self-hosted worlds, protect the flow and webhook routes with the authentication, network controls, and rate limits appropriate to your queue transport.
|
|
479
243
|
|
|
480
|
-
|
|
244
|
+
## Testing an integration
|
|
481
245
|
|
|
482
|
-
|
|
483
|
-
- `.well-known/workflow/v1/step.js` exists
|
|
484
|
-
- `.well-known/workflow/v1/webhook.js` exists
|
|
246
|
+
After a build, verify that:
|
|
485
247
|
|
|
486
|
-
|
|
248
|
+
- the flow handler exists and imports or embeds the step registration bundle;
|
|
249
|
+
- the webhook handler exists;
|
|
250
|
+
- no step HTTP route or step queue trigger is generated;
|
|
251
|
+
- the flow function has the workflow queue trigger and the required maximum duration;
|
|
252
|
+
- starting a workflow executes a real step and resumes the workflow.
|
|
487
253
|
|
|
488
|
-
|
|
254
|
+
The flow route exposes a lightweight direct health mode:
|
|
489
255
|
|
|
490
256
|
```bash
|
|
491
|
-
curl -X POST http://localhost:3000/.well-known/workflow/v1/flow
|
|
492
|
-
curl -X POST http://localhost:3000/.well-known/workflow/v1/step
|
|
493
|
-
curl -X POST http://localhost:3000/.well-known/workflow/v1/webhook/test
|
|
257
|
+
curl -X POST "http://localhost:3000/.well-known/workflow/v1/flow?__health"
|
|
494
258
|
```
|
|
495
259
|
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
### 3. Run a Workflow End-to-End
|
|
499
|
-
|
|
500
|
-
```typescript
|
|
501
|
-
import { start } from "workflow/api";
|
|
502
|
-
import { handleUserSignup } from "./workflows/test";
|
|
503
|
-
|
|
504
|
-
const run = await start(handleUserSignup, ["test@example.com"]);
|
|
505
|
-
console.log("Workflow started:", run.runId);
|
|
506
|
-
```
|
|
260
|
+
For an end-to-end queue check, use [`healthCheck()`](/docs/api-reference/workflow-runtime/health-check).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "workflow",
|
|
3
|
-
"version": "5.0.0-beta.
|
|
3
|
+
"version": "5.0.0-beta.38",
|
|
4
4
|
"description": "Workflow SDK - Build durable, resilient, and observable workflows",
|
|
5
5
|
"main": "dist/typescript-plugin.cjs",
|
|
6
6
|
"type": "module",
|
|
@@ -59,18 +59,18 @@
|
|
|
59
59
|
},
|
|
60
60
|
"dependencies": {
|
|
61
61
|
"ms": "2.1.3",
|
|
62
|
-
"@workflow/astro": "5.0.0-beta.
|
|
63
|
-
"@workflow/cli": "5.0.0-beta.
|
|
64
|
-
"@workflow/core": "5.0.0-beta.
|
|
62
|
+
"@workflow/astro": "5.0.0-beta.38",
|
|
63
|
+
"@workflow/cli": "5.0.0-beta.38",
|
|
64
|
+
"@workflow/core": "5.0.0-beta.38",
|
|
65
|
+
"@workflow/errors": "5.0.0-beta.14",
|
|
65
66
|
"@workflow/typescript-plugin": "5.0.0-beta.5",
|
|
66
|
-
"@workflow/
|
|
67
|
-
"@workflow/
|
|
68
|
-
"@workflow/
|
|
69
|
-
"@workflow/
|
|
70
|
-
"@workflow/
|
|
71
|
-
"@workflow/
|
|
72
|
-
"@workflow/
|
|
73
|
-
"@workflow/rollup": "5.0.0-beta.36"
|
|
67
|
+
"@workflow/utils": "5.0.0-beta.8",
|
|
68
|
+
"@workflow/next": "5.0.0-beta.38",
|
|
69
|
+
"@workflow/nest": "5.0.0-beta.38",
|
|
70
|
+
"@workflow/nitro": "5.0.0-beta.38",
|
|
71
|
+
"@workflow/nuxt": "5.0.0-beta.38",
|
|
72
|
+
"@workflow/sveltekit": "5.0.0-beta.38",
|
|
73
|
+
"@workflow/rollup": "5.0.0-beta.38"
|
|
74
74
|
},
|
|
75
75
|
"devDependencies": {
|
|
76
76
|
"@types/ms": "2.1.0",
|