workflow 5.0.0-beta.53 → 5.0.0-beta.55
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/api-workflow.d.ts +2 -0
- package/dist/api-workflow.d.ts.map +1 -1
- package/dist/api-workflow.js +2 -1
- package/dist/api.d.ts +1 -0
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +2 -1
- package/docs/api-reference/vitest/index.mdx +59 -0
- package/docs/api-reference/workflow/set-attributes.mdx +3 -1
- package/docs/api-reference/workflow-api/index.mdx +3 -0
- package/docs/api-reference/workflow-api/register-lifecycle-hooks.mdx +85 -0
- package/docs/api-reference/workflow-globals.mdx +1 -1
- package/docs/api-reference/workflow-nest/configure-workflow-controller.mdx +5 -1
- package/docs/api-reference/workflow-nest/workflow-controller.mdx +7 -1
- package/docs/api-reference/workflow-nest/workflow-module.mdx +64 -4
- package/docs/api-reference/workflow-runtime/world/storage.mdx +4 -1
- package/docs/configuration/build-and-diagnostics.mdx +9 -0
- package/docs/configuration/worlds.mdx +3 -1
- package/docs/deploying.mdx +11 -0
- package/docs/getting-started/nestjs.mdx +185 -9
- package/docs/observability/attributes.mdx +2 -0
- package/docs/observability/lifecycle-hooks.mdx +93 -0
- package/docs/observability/meta.json +1 -1
- package/docs/testing/index.mdx +84 -2
- package/package.json +11 -11
package/dist/api-workflow.d.ts
CHANGED
|
@@ -6,4 +6,6 @@ export declare const getHookByToken: () => never;
|
|
|
6
6
|
export declare const resumeHook: () => never;
|
|
7
7
|
export declare const resumeWebhook: () => never;
|
|
8
8
|
export declare const runStep: () => never;
|
|
9
|
+
export declare const registerLifecycleHooks: () => never;
|
|
10
|
+
export type { RunCompletedHookParams, RunFailedHookParams, WorkflowLifecycleHooks, } from '@workflow/core/runtime/lifecycle-hooks';
|
|
9
11
|
//# sourceMappingURL=api-workflow.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api-workflow.d.ts","sourceRoot":"","sources":["../src/api-workflow.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,gBAAgB,EAChB,KAAK,EACL,YAAY,EACZ,gBAAgB,EAChB,eAAe,EACf,6BAA6B,EAC7B,WAAW,EACX,gCAAgC,GACjC,MAAM,wBAAwB,CAAC;AAEhC,OAAO,EAAE,GAAG,EAAE,MAAM,4BAA4B,CAAC;AACjD,OAAO,EAAE,KAAK,EAAE,MAAM,8BAA8B,CAAC;AAQrD,eAAO,MAAM,MAAM,aAA+B,CAAC;AACnD,eAAO,MAAM,cAAc,aAAuC,CAAC;AACnE,eAAO,MAAM,UAAU,aAAmC,CAAC;AAC3D,eAAO,MAAM,aAAa,aAAsC,CAAC;AACjE,eAAO,MAAM,OAAO,aAAgC,CAAC"}
|
|
1
|
+
{"version":3,"file":"api-workflow.d.ts","sourceRoot":"","sources":["../src/api-workflow.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,gBAAgB,EAChB,KAAK,EACL,YAAY,EACZ,gBAAgB,EAChB,eAAe,EACf,6BAA6B,EAC7B,WAAW,EACX,gCAAgC,GACjC,MAAM,wBAAwB,CAAC;AAEhC,OAAO,EAAE,GAAG,EAAE,MAAM,4BAA4B,CAAC;AACjD,OAAO,EAAE,KAAK,EAAE,MAAM,8BAA8B,CAAC;AAQrD,eAAO,MAAM,MAAM,aAA+B,CAAC;AACnD,eAAO,MAAM,cAAc,aAAuC,CAAC;AACnE,eAAO,MAAM,UAAU,aAAmC,CAAC;AAC3D,eAAO,MAAM,aAAa,aAAsC,CAAC;AACjE,eAAO,MAAM,OAAO,aAAgC,CAAC;AACrD,eAAO,MAAM,sBAAsB,aACK,CAAC;AACzC,YAAY,EACV,sBAAsB,EACtB,mBAAmB,EACnB,sBAAsB,GACvB,MAAM,wCAAwC,CAAC"}
|
package/dist/api-workflow.js
CHANGED
|
@@ -8,4 +8,5 @@ export const getHookByToken = () => workflowStub('getHookByToken');
|
|
|
8
8
|
export const resumeHook = () => workflowStub('resumeHook');
|
|
9
9
|
export const resumeWebhook = () => workflowStub('resumeWebhook');
|
|
10
10
|
export const runStep = () => workflowStub('runStep');
|
|
11
|
-
|
|
11
|
+
export const registerLifecycleHooks = () => workflowStub('registerLifecycleHooks');
|
|
12
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYXBpLXdvcmtmbG93LmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vc3JjL2FwaS13b3JrZmxvdy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFXQSxPQUFPLEVBQUUsR0FBRyxFQUFFLE1BQU0sNEJBQTRCLENBQUM7QUFDakQsT0FBTyxFQUFFLEtBQUssRUFBRSxNQUFNLDhCQUE4QixDQUFDO0FBRXJELE1BQU0sWUFBWSxHQUFHLENBQUMsSUFBWSxFQUFFLEVBQUU7SUFDcEMsTUFBTSxJQUFJLEtBQUssQ0FDYixnRUFBZ0UsSUFBSSwyRkFBMkYsQ0FDaEssQ0FBQztBQUNKLENBQUMsQ0FBQztBQUVGLE1BQU0sQ0FBQyxNQUFNLE1BQU0sR0FBRyxHQUFHLEVBQUUsQ0FBQyxZQUFZLENBQUMsUUFBUSxDQUFDLENBQUM7QUFDbkQsTUFBTSxDQUFDLE1BQU0sY0FBYyxHQUFHLEdBQUcsRUFBRSxDQUFDLFlBQVksQ0FBQyxnQkFBZ0IsQ0FBQyxDQUFDO0FBQ25FLE1BQU0sQ0FBQyxNQUFNLFVBQVUsR0FBRyxHQUFHLEVBQUUsQ0FBQyxZQUFZLENBQUMsWUFBWSxDQUFDLENBQUM7QUFDM0QsTUFBTSxDQUFDLE1BQU0sYUFBYSxHQUFHLEdBQUcsRUFBRSxDQUFDLFlBQVksQ0FBQyxlQUFlLENBQUMsQ0FBQztBQUNqRSxNQUFNLENBQUMsTUFBTSxPQUFPLEdBQUcsR0FBRyxFQUFFLENBQUMsWUFBWSxDQUFDLFNBQVMsQ0FBQyxDQUFDO0FBQ3JELE1BQU0sQ0FBQyxNQUFNLHNCQUFzQixHQUFHLEdBQUcsRUFBRSxDQUN6QyxZQUFZLENBQUMsd0JBQXdCLENBQUMsQ0FBQyIsInNvdXJjZXNDb250ZW50IjpbImV4cG9ydCB0eXBlIHtcbiAgQ2FuY2VsUnVuT3B0aW9ucyxcbiAgRXZlbnQsXG4gIFN0YXJ0T3B0aW9ucyxcbiAgU3RvcFNsZWVwT3B0aW9ucyxcbiAgU3RvcFNsZWVwUmVzdWx0LFxuICBXb3JrZmxvd1JlYWRhYmxlU3RyZWFtT3B0aW9ucyxcbiAgV29ya2Zsb3dSdW4sXG4gIFdvcmtmbG93UnVuV3JpdGFibGVTdHJlYW1PcHRpb25zLFxufSBmcm9tICdAd29ya2Zsb3cvY29yZS9ydW50aW1lJztcblxuZXhwb3J0IHsgUnVuIH0gZnJvbSAnQHdvcmtmbG93L2NvcmUvcnVudGltZS9ydW4nO1xuZXhwb3J0IHsgc3RhcnQgfSBmcm9tICdAd29ya2Zsb3cvY29yZS9ydW50aW1lL3N0YXJ0JztcblxuY29uc3Qgd29ya2Zsb3dTdHViID0gKGl0ZW06IHN0cmluZykgPT4ge1xuICB0aHJvdyBuZXcgRXJyb3IoXG4gICAgYFRoZSB3b3JrZmxvdyBlbnZpcm9ubWVudCBkb2Vzbid0IGFsbG93IHRoaXMgcnVudGltZSB1c2FnZSBvZiAke2l0ZW19LiBNb3ZlIHRoaXMgY2FsbCB0byBhIHN0ZXAgZnVuY3Rpb24gKFwidXNlIHN0ZXBcIikgb3IgY2FsbCBpdCBvdXRzaWRlIHRoZSB3b3JrZmxvdyBjb250ZXh0LmBcbiAgKTtcbn07XG5cbmV4cG9ydCBjb25zdCBnZXRSdW4gPSAoKSA9PiB3b3JrZmxvd1N0dWIoJ2dldFJ1bicpO1xuZXhwb3J0IGNvbnN0IGdldEhvb2tCeVRva2VuID0gKCkgPT4gd29ya2Zsb3dTdHViKCdnZXRIb29rQnlUb2tlbicpO1xuZXhwb3J0IGNvbnN0IHJlc3VtZUhvb2sgPSAoKSA9PiB3b3JrZmxvd1N0dWIoJ3Jlc3VtZUhvb2snKTtcbmV4cG9ydCBjb25zdCByZXN1bWVXZWJob29rID0gKCkgPT4gd29ya2Zsb3dTdHViKCdyZXN1bWVXZWJob29rJyk7XG5leHBvcnQgY29uc3QgcnVuU3RlcCA9ICgpID0+IHdvcmtmbG93U3R1YigncnVuU3RlcCcpO1xuZXhwb3J0IGNvbnN0IHJlZ2lzdGVyTGlmZWN5Y2xlSG9va3MgPSAoKSA9PlxuICB3b3JrZmxvd1N0dWIoJ3JlZ2lzdGVyTGlmZWN5Y2xlSG9va3MnKTtcbmV4cG9ydCB0eXBlIHtcbiAgUnVuQ29tcGxldGVkSG9va1BhcmFtcyxcbiAgUnVuRmFpbGVkSG9va1BhcmFtcyxcbiAgV29ya2Zsb3dMaWZlY3ljbGVIb29rcyxcbn0gZnJvbSAnQHdvcmtmbG93L2NvcmUvcnVudGltZS9saWZlY3ljbGUtaG9va3MnO1xuIl19
|
package/dist/api.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import '@workflow/core/runtime/world-init';
|
|
2
2
|
export type { CancelRunOptions, Event, StopSleepOptions, StopSleepResult, WorkflowRun, } from '@workflow/core/runtime';
|
|
3
|
+
export { type RunCompletedHookParams, type RunFailedHookParams, registerLifecycleHooks, type WorkflowLifecycleHooks, } from '@workflow/core/runtime/lifecycle-hooks';
|
|
3
4
|
export { getHookByToken, type Hook, type ResumedHook, resumeHook, resumeWebhook, } from '@workflow/core/runtime/resume-hook';
|
|
4
5
|
export { getRun, Run, type WorkflowReadableStream, type WorkflowReadableStreamOptions, type WorkflowRunWritableStreamOptions, } from '@workflow/core/runtime/run';
|
|
5
6
|
export { type StartOptions, start, } from '@workflow/core/runtime/start';
|
package/dist/api.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"AAOA,OAAO,mCAAmC,CAAC;AAE3C,YAAY,EACV,gBAAgB,EAChB,KAAK,EACL,gBAAgB,EAChB,eAAe,EACf,WAAW,GACZ,MAAM,wBAAwB,CAAC;AAChC,OAAO,EACL,cAAc,EACd,KAAK,IAAI,EACT,KAAK,WAAW,EAChB,UAAU,EACV,aAAa,GACd,MAAM,oCAAoC,CAAC;AAC5C,OAAO,EACL,MAAM,EACN,GAAG,EACH,KAAK,sBAAsB,EAC3B,KAAK,6BAA6B,EAClC,KAAK,gCAAgC,GACtC,MAAM,4BAA4B,CAAC;AACpC,OAAO,EACL,KAAK,YAAY,EACjB,KAAK,GACN,MAAM,8BAA8B,CAAC"}
|
|
1
|
+
{"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"AAOA,OAAO,mCAAmC,CAAC;AAE3C,YAAY,EACV,gBAAgB,EAChB,KAAK,EACL,gBAAgB,EAChB,eAAe,EACf,WAAW,GACZ,MAAM,wBAAwB,CAAC;AAChC,OAAO,EACL,KAAK,sBAAsB,EAC3B,KAAK,mBAAmB,EACxB,sBAAsB,EACtB,KAAK,sBAAsB,GAC5B,MAAM,wCAAwC,CAAC;AAChD,OAAO,EACL,cAAc,EACd,KAAK,IAAI,EACT,KAAK,WAAW,EAChB,UAAU,EACV,aAAa,GACd,MAAM,oCAAoC,CAAC;AAC5C,OAAO,EACL,MAAM,EACN,GAAG,EACH,KAAK,sBAAsB,EAC3B,KAAK,6BAA6B,EAClC,KAAK,gCAAgC,GACtC,MAAM,4BAA4B,CAAC;AACpC,OAAO,EACL,KAAK,YAAY,EACjB,KAAK,GACN,MAAM,8BAA8B,CAAC"}
|
package/dist/api.js
CHANGED
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
// stays host-only.
|
|
7
7
|
// See `@workflow/core/src/runtime/world-init.ts` for the full rationale.
|
|
8
8
|
import '@workflow/core/runtime/world-init';
|
|
9
|
+
export { registerLifecycleHooks, } from '@workflow/core/runtime/lifecycle-hooks';
|
|
9
10
|
export { getHookByToken, resumeHook, resumeWebhook, } from '@workflow/core/runtime/resume-hook';
|
|
10
11
|
export { getRun, Run, } from '@workflow/core/runtime/run';
|
|
11
12
|
export { start, } from '@workflow/core/runtime/start';
|
|
12
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
13
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYXBpLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vc3JjL2FwaS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxxRUFBcUU7QUFDckUseUVBQXlFO0FBQ3pFLDJFQUEyRTtBQUMzRSw0RUFBNEU7QUFDNUUsNkVBQTZFO0FBQzdFLG1CQUFtQjtBQUNuQix5RUFBeUU7QUFDekUsT0FBTyxtQ0FBbUMsQ0FBQztBQVMzQyxPQUFPLEVBR0wsc0JBQXNCLEdBRXZCLE1BQU0sd0NBQXdDLENBQUM7QUFDaEQsT0FBTyxFQUNMLGNBQWMsRUFHZCxVQUFVLEVBQ1YsYUFBYSxHQUNkLE1BQU0sb0NBQW9DLENBQUM7QUFDNUMsT0FBTyxFQUNMLE1BQU0sRUFDTixHQUFHLEdBSUosTUFBTSw0QkFBNEIsQ0FBQztBQUNwQyxPQUFPLEVBRUwsS0FBSyxHQUNOLE1BQU0sOEJBQThCLENBQUMiLCJzb3VyY2VzQ29udGVudCI6WyIvLyBTaWRlLWVmZmVjdCBpbXBvcnQ6IGVuc3VyZSBgd29ybGQudHNgIGlzIGxvYWRlZCBzbyBpdHMgbW9kdWxlLWxvYWRcbi8vIGBnbG9iYWxUaGlzW0dldFdvcmxkRm5LZXldID8/PSBnZXRXb3JsZGAgcmVnaXN0cmF0aW9uIGZpcmVzIGJlZm9yZSBhbnlcbi8vIGhvc3Qgcm91dGUgcmVhY2hlcyBgZ2V0V29ybGRMYXp5KClgLiBXaXRob3V0IHRoaXMsIHdlYnBhY2svdHVyYm9wYWNrIGNhblxuLy8gdHJlZS1zaGFrZSBgd29ybGQudHNgIG91dCBvZiByb3V0ZXMgdGhhdCBvbmx5IHVzZSBgc3RhcnRgLiBSZXNvbHZlZCB0byBhblxuLy8gZW1wdHkgc3R1YiB2aWEgdGhlIGB3b3JrZmxvd2AgZXhwb3J0IGNvbmRpdGlvbiBpbiBWTS9zdGVwIGJ1bmRsZXMsIHNvIHRoaXNcbi8vIHN0YXlzIGhvc3Qtb25seS5cbi8vIFNlZSBgQHdvcmtmbG93L2NvcmUvc3JjL3J1bnRpbWUvd29ybGQtaW5pdC50c2AgZm9yIHRoZSBmdWxsIHJhdGlvbmFsZS5cbmltcG9ydCAnQHdvcmtmbG93L2NvcmUvcnVudGltZS93b3JsZC1pbml0JztcblxuZXhwb3J0IHR5cGUge1xuICBDYW5jZWxSdW5PcHRpb25zLFxuICBFdmVudCxcbiAgU3RvcFNsZWVwT3B0aW9ucyxcbiAgU3RvcFNsZWVwUmVzdWx0LFxuICBXb3JrZmxvd1J1bixcbn0gZnJvbSAnQHdvcmtmbG93L2NvcmUvcnVudGltZSc7XG5leHBvcnQge1xuICB0eXBlIFJ1bkNvbXBsZXRlZEhvb2tQYXJhbXMsXG4gIHR5cGUgUnVuRmFpbGVkSG9va1BhcmFtcyxcbiAgcmVnaXN0ZXJMaWZlY3ljbGVIb29rcyxcbiAgdHlwZSBXb3JrZmxvd0xpZmVjeWNsZUhvb2tzLFxufSBmcm9tICdAd29ya2Zsb3cvY29yZS9ydW50aW1lL2xpZmVjeWNsZS1ob29rcyc7XG5leHBvcnQge1xuICBnZXRIb29rQnlUb2tlbixcbiAgdHlwZSBIb29rLFxuICB0eXBlIFJlc3VtZWRIb29rLFxuICByZXN1bWVIb29rLFxuICByZXN1bWVXZWJob29rLFxufSBmcm9tICdAd29ya2Zsb3cvY29yZS9ydW50aW1lL3Jlc3VtZS1ob29rJztcbmV4cG9ydCB7XG4gIGdldFJ1bixcbiAgUnVuLFxuICB0eXBlIFdvcmtmbG93UmVhZGFibGVTdHJlYW0sXG4gIHR5cGUgV29ya2Zsb3dSZWFkYWJsZVN0cmVhbU9wdGlvbnMsXG4gIHR5cGUgV29ya2Zsb3dSdW5Xcml0YWJsZVN0cmVhbU9wdGlvbnMsXG59IGZyb20gJ0B3b3JrZmxvdy9jb3JlL3J1bnRpbWUvcnVuJztcbmV4cG9ydCB7XG4gIHR5cGUgU3RhcnRPcHRpb25zLFxuICBzdGFydCxcbn0gZnJvbSAnQHdvcmtmbG93L2NvcmUvcnVudGltZS9zdGFydCc7XG4iXX0=
|
|
@@ -5,6 +5,18 @@ description: Vitest plugin and test helpers for integration testing workflows in
|
|
|
5
5
|
|
|
6
6
|
The `@workflow/vitest` package provides a Vitest plugin and test helpers for running full workflow integration tests in-process, no server required.
|
|
7
7
|
|
|
8
|
+
## Installation
|
|
9
|
+
|
|
10
|
+
```package-install
|
|
11
|
+
npm i -D @workflow/vitest@beta
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
<Callout type="warn">
|
|
15
|
+
`@workflow/vitest@latest` is still the 4.x line, so a Workflow 5 app has to install the `beta` tag (or pin the matching beta, for example `@workflow/vitest@5.0.0-beta.53`). The package carries its own copy of `@workflow/core` and runs your workflows against it, so it has to move with `workflow`.
|
|
16
|
+
|
|
17
|
+
`globalSetup` compares the two copies once per run: a different major fails the run with the install command that fixes it, and any other difference logs a warning. Set [`WORKFLOW_VITEST_VERSION_CHECK=off`](/docs/configuration/build-and-diagnostics#workflow_vitest_version_check) to skip the check.
|
|
18
|
+
</Callout>
|
|
19
|
+
|
|
8
20
|
## Plugin
|
|
9
21
|
|
|
10
22
|
### `workflow()`
|
|
@@ -108,6 +120,53 @@ Tears down the workflow test world. Clears the global world and closes the Local
|
|
|
108
120
|
| `dataDir` | `string` | `<rootDir>/.workflow-data` | Directory for workflow runtime data written by the test world. Relative paths resolve against `cwd`. |
|
|
109
121
|
| `outDir` | `string` | `<rootDir>/.workflow-vitest` | Directory for generated workflow and step bundles. Relative paths resolve against `cwd`. |
|
|
110
122
|
|
|
123
|
+
## Workflow references
|
|
124
|
+
|
|
125
|
+
The test build writes the same `manifest.json` the other Workflow builders write into `outDir`. These helpers read it, so a test can name a workflow instead of hand-writing the generated `workflow//...` id. Importing the workflow function is still preferable when a test can do it, because it keeps the argument and return types.
|
|
126
|
+
|
|
127
|
+
### `getWorkflowRef()`
|
|
128
|
+
|
|
129
|
+
Looks up one workflow in the test build and returns it in the shape [`start()`](/docs/api-reference/workflow-api/start) accepts.
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
import { getWorkflowRef } from "@workflow/vitest"; // [!code highlight]
|
|
133
|
+
import { start } from "workflow/api";
|
|
134
|
+
|
|
135
|
+
const run = await start(getWorkflowRef("approvalWorkflow"), ["doc-1"]); // [!code highlight]
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
`query` is an exported workflow name, or a file-qualified name when the same name appears in more than one file: `getWorkflowRef("workflows/approval.ts#approvalWorkflow")`. The file part also matches by path suffix, so `"approval.ts#approvalWorkflow"` resolves the same entry.
|
|
139
|
+
|
|
140
|
+
**Parameters:**
|
|
141
|
+
|
|
142
|
+
| Parameter | Type | Description |
|
|
143
|
+
| --- | --- | --- |
|
|
144
|
+
| `query` | `string` | Workflow name, or `<file>#<name>` |
|
|
145
|
+
|
|
146
|
+
**Returns:** [`WorkflowRef`](#workflowref).
|
|
147
|
+
|
|
148
|
+
**Throws** when the manifest is missing (the plugin or `buildWorkflowTests()` has not run), when nothing matches, or when the name is ambiguous. The message lists the workflows the build contains.
|
|
149
|
+
|
|
150
|
+
### `listWorkflowRefs()`
|
|
151
|
+
|
|
152
|
+
Returns every workflow in the test build, sorted by file and then name.
|
|
153
|
+
|
|
154
|
+
```typescript
|
|
155
|
+
import { listWorkflowRefs } from "@workflow/vitest"; // [!code highlight]
|
|
156
|
+
|
|
157
|
+
const names = listWorkflowRefs().map((ref) => ref.name); // [!code highlight]
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
**Returns:** [`WorkflowRef`](#workflowref)`[]`.
|
|
161
|
+
|
|
162
|
+
### `WorkflowRef`
|
|
163
|
+
|
|
164
|
+
| Property | Type | Description |
|
|
165
|
+
| --- | --- | --- |
|
|
166
|
+
| `name` | `string` | Exported name of the workflow function |
|
|
167
|
+
| `file` | `string` | Project-relative path of the file it was compiled from |
|
|
168
|
+
| `workflowId` | `string` | Generated workflow id, the field `start()` reads |
|
|
169
|
+
|
|
111
170
|
## Test helpers
|
|
112
171
|
|
|
113
172
|
### `waitForSleep()`
|
|
@@ -54,7 +54,9 @@ export async function cleanupAttributes() {
|
|
|
54
54
|
|
|
55
55
|
Attribute keys must be 1-256 characters, values must be strings up to 256 bytes, and each run can have up to 64 attributes. Keys that start with `$` are reserved for framework and library code.
|
|
56
56
|
|
|
57
|
-
|
|
57
|
+
Each call's complete `attr_set` event data must also fit in **8192 UTF-8 JSON bytes (8KiB)**. This includes the change keys and values, JSON escaping and structure, writer metadata (including the step ID and attempt for step calls), and the reserved-key option when enabled. It is not a limit on values alone. Split large updates into smaller calls; updates across multiple calls are not atomic and still share the 64-attribute per-run limit.
|
|
58
|
+
|
|
59
|
+
Validation errors reject `setAttributes` with [`FatalError`](/docs/api-reference/workflow/fatal-error) before a new attribute write is attempted. Catch the error if the metadata is best-effort; an uncaught error fails the workflow or step. Previously persisted attribute events remain replayable.
|
|
58
60
|
|
|
59
61
|
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
62
|
|
|
@@ -25,6 +25,9 @@ The `workflow/api` package provides runtime functions to inspect runs, start new
|
|
|
25
25
|
<Card href="/docs/api-reference/workflow-api/get-run" title="getRun()">
|
|
26
26
|
Get workflow run status and metadata without waiting for completion.
|
|
27
27
|
</Card>
|
|
28
|
+
<Card href="/docs/api-reference/workflow-api/register-lifecycle-hooks" title="registerLifecycleHooks()">
|
|
29
|
+
Observe run completions and failures with global handlers.
|
|
30
|
+
</Card>
|
|
28
31
|
</Cards>
|
|
29
32
|
|
|
30
33
|
<Callout type="info">
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: registerLifecycleHooks
|
|
3
|
+
description: Register global handlers that observe workflow runs completing or failing.
|
|
4
|
+
type: reference
|
|
5
|
+
summary: Use registerLifecycleHooks to observe run completions and failures from one central place.
|
|
6
|
+
prerequisites:
|
|
7
|
+
- /docs/foundations/workflows-and-steps
|
|
8
|
+
related:
|
|
9
|
+
- /docs/observability/lifecycle-hooks
|
|
10
|
+
- /docs/api-reference/workflow-api/get-run
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
Registers global workflow lifecycle handlers, invoked by the runtime on the compute that records a run's terminal transition. Use it for best-effort centralized reporting, such as forwarding failed runs to Sentry, without wrapping each workflow body.
|
|
14
|
+
|
|
15
|
+
Register early in the process lifecycle (in Next.js, `instrumentation.ts`) so handlers exist before the first run finishes. See the [lifecycle hooks guide](/docs/observability/lifecycle-hooks) for semantics and a full Sentry example.
|
|
16
|
+
|
|
17
|
+
```typescript title="instrumentation.ts" lineNumbers
|
|
18
|
+
export async function register() {
|
|
19
|
+
if (process.env.NEXT_RUNTIME === "nodejs") {
|
|
20
|
+
const { registerLifecycleHooks } = await import("workflow/api");
|
|
21
|
+
|
|
22
|
+
registerLifecycleHooks({
|
|
23
|
+
async onRunCompleted({ run, workflowName }) {
|
|
24
|
+
console.log(`Run ${run.runId} (${workflowName}) completed`);
|
|
25
|
+
},
|
|
26
|
+
async onRunFailed({ run, workflowName, error }) {
|
|
27
|
+
console.error(
|
|
28
|
+
`Run ${run.runId} (${workflowName}) failed (${error.errorCode})`,
|
|
29
|
+
error.cause
|
|
30
|
+
);
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Keep the dynamic import inside the `NEXT_RUNTIME === "nodejs"` guard. Next.js also compiles `instrumentation.ts` for the Edge runtime. A top-level static import of `workflow/api` pulls Node.js-only dependencies into that compilation and breaks webpack Edge builds, even if the registration call is guarded.
|
|
38
|
+
|
|
39
|
+
## API Signature
|
|
40
|
+
|
|
41
|
+
### Parameters
|
|
42
|
+
|
|
43
|
+
<TSDoc
|
|
44
|
+
definition={`
|
|
45
|
+
import { registerLifecycleHooks } from "workflow/api";
|
|
46
|
+
export default registerLifecycleHooks;`}
|
|
47
|
+
showSections={["parameters"]}
|
|
48
|
+
/>
|
|
49
|
+
|
|
50
|
+
### Returns
|
|
51
|
+
|
|
52
|
+
Returns a function that unregisters these hooks.
|
|
53
|
+
|
|
54
|
+
## Handlers
|
|
55
|
+
|
|
56
|
+
Both handlers receive a `workflowName` string and a lazily hydrated [`Run`](/docs/api-reference/workflow-api/get-run) instance. Use the `workflowName` parameter to filter without a backend read; `run.runId` also requires no read. Accessors such as `run.workflowName`, `run.status`, and `run.returnValue` still fetch from the backend when used. Lazy access defers those reads rather than eliminating them.
|
|
57
|
+
|
|
58
|
+
### `onRunCompleted`
|
|
59
|
+
|
|
60
|
+
Invoked when a workflow run completes successfully.
|
|
61
|
+
|
|
62
|
+
| Parameter | Type | Description |
|
|
63
|
+
| --- | --- | --- |
|
|
64
|
+
| `params.run` | `Run` | The completed run. |
|
|
65
|
+
| `params.workflowName` | `string` | The machine-readable workflow identifier, such as `workflow//./src/workflows/order//processOrder`. Available without a backend read. |
|
|
66
|
+
|
|
67
|
+
### `onRunFailed`
|
|
68
|
+
|
|
69
|
+
Invoked when a workflow run fails terminally (after any retries).
|
|
70
|
+
|
|
71
|
+
| Parameter | Type | Description |
|
|
72
|
+
| --- | --- | --- |
|
|
73
|
+
| `params.run` | `Run` | The failed run. |
|
|
74
|
+
| `params.workflowName` | `string` | The machine-readable workflow identifier, such as `workflow//./src/workflows/order//processOrder`. Available without a backend read. |
|
|
75
|
+
| `params.error` | `WorkflowRunFailedError` | The failure, in the same shape `run.returnValue` rejects with: `error.errorCode` carries the classification (e.g. `USER_ERROR`) and `error.cause` is the hydrated thrown value. |
|
|
76
|
+
|
|
77
|
+
`error.cause` is hydrated from the persisted error data, with streamed values loaded lazily when consumed. Abort signals reflect their persisted state without live subscriptions. If hydration fails, the cause is a generic `Error`, matching `run.returnValue`'s fallback. In `onRunFailed`, `run.returnValue` rejects because the run failed. Use `error.cause` to inspect or report the thrown value instead.
|
|
78
|
+
|
|
79
|
+
## Behavior
|
|
80
|
+
|
|
81
|
+
- Handlers run on the host (full Node.js), never inside the workflow VM. Calling `registerLifecycleHooks` from workflow code throws.
|
|
82
|
+
- Handlers are fire-and-forget. They cannot delay or change the run's outcome, and the runtime logs and swallows a throwing handler. On Vercel, `waitUntil` keeps the invocation alive while handlers finish, subject to the invocation's duration limit. On other hosts, handlers run as detached work and may not complete if the host freezes or terminates the process after the response.
|
|
83
|
+
- Delivery is best effort. Each registered handler is invoked at most once by the invocation that writes the terminal event. Callbacks are not retried if they throw or the process dies, so they may never run or may stop before completing. The [event log](/docs/how-it-works/event-sourcing) is the system of record.
|
|
84
|
+
- Handlers fire only on the invocation that wrote the terminal event. Transitions recorded outside your app's compute (e.g. a run cancelled from the CLI or dashboard) do not fire handlers.
|
|
85
|
+
- You can register multiple hook sets, and handlers run in registration order.
|
|
@@ -23,7 +23,7 @@ These APIs are available but are **seeded or fixed** to ensure deterministic beh
|
|
|
23
23
|
| API | Behavior |
|
|
24
24
|
|-----|----------|
|
|
25
25
|
| [`Math.random()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/random) | Seeded random number generator: same seed produces the same sequence every replay |
|
|
26
|
-
| [`Date`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date) / `Date.now()` / `new Date()` | Returns
|
|
26
|
+
| [`Date`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date) / `Date.now()` / `new Date()` | Returns the workflow's logical clock: the run's creation time until the first step result, hook payload, hook registration, wait completion or abort reaches the workflow, then the time that event was recorded, advancing as each later one is delivered. Identical on every replay of the same log |
|
|
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
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 |
|
|
@@ -9,7 +9,11 @@ prerequisites:
|
|
|
9
9
|
|
|
10
10
|
Configures the output directory that [`WorkflowController`](/docs/api-reference/workflow-nest/workflow-controller) loads the generated workflow bundles (`steps.mjs`, `workflows.mjs`, `webhook.mjs`, `manifest.json`) from.
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
<Callout type="warn">
|
|
13
|
+
Deprecated. [`WorkflowModule.forRoot()`](/docs/api-reference/workflow-nest/workflow-module) provides the output directory through dependency injection, which this function predates. It writes process-global state, so two applications in one process (the usual `Test.createTestingModule` setup) overwrite each other's configuration. It is kept only so existing callers keep working.
|
|
14
|
+
</Callout>
|
|
15
|
+
|
|
16
|
+
`WorkflowModule.forRoot()` still calls this for you, and injected options take precedence over it. Call it yourself only when registering `WorkflowController` without the module. A controller with no configuration at all answers `503` with an explanatory message.
|
|
13
17
|
|
|
14
18
|
## Usage
|
|
15
19
|
|
|
@@ -9,6 +9,10 @@ prerequisites:
|
|
|
9
9
|
|
|
10
10
|
NestJS controller that handles the well-known workflow endpoints under `.well-known/workflow/v1`. It dynamically imports the generated workflow bundles and converts between Express/Fastify requests and the Web API `Request`/`Response` objects the workflow runtime expects. Both the Express and Fastify HTTP adapters are supported.
|
|
11
11
|
|
|
12
|
+
The conversion preserves bytes in both directions: request bodies come from `req.rawBody` when the app is created with `{ rawBody: true }`, from a `Buffer`/string body left by a parser, or read directly from the request stream when no parser claimed the content type. Responses are written as bytes, and every `set-cookie` value is kept. See [Raw request bodies](/docs/getting-started/nestjs#raw-request-bodies).
|
|
13
|
+
|
|
14
|
+
Handlers take `@Res()`, so the application's interceptors and exception filters do not wrap these routes. That is deliberate: the queue and third-party webhook senders key off the exact status and body the workflow runtime produces.
|
|
15
|
+
|
|
12
16
|
[`WorkflowModule.forRoot()`](/docs/api-reference/workflow-nest/workflow-module) registers this controller automatically. You only register it yourself if you are not using `WorkflowModule`.
|
|
13
17
|
|
|
14
18
|
## Usage
|
|
@@ -35,6 +39,8 @@ export class AppModule {}
|
|
|
35
39
|
|
|
36
40
|
| Route | Method | Description |
|
|
37
41
|
| --- | --- | --- |
|
|
38
|
-
| `/.well-known/workflow/v1/flow` | `POST` | Executes workflow and step work items via the combined handler in `workflows.mjs` (step registrations are imported from `steps.mjs` first). |
|
|
42
|
+
| `/.well-known/workflow/v1/flow` | `POST`, `GET`, `HEAD`, `OPTIONS` | Executes workflow and step work items via the combined handler in `workflows.mjs` (step registrations are imported from `steps.mjs` first). `HEAD` is what local port detection probes to identify a workflow server, so all four methods are served. |
|
|
39
43
|
| `/.well-known/workflow/v1/webhook/:token` | Any | Forwards webhook requests to the handler in `webhook.mjs`. |
|
|
40
44
|
| `/.well-known/workflow/v1/manifest.json` | `GET` | Serves the workflow manifest. Responds with `404` unless the `WORKFLOW_PUBLIC_MANIFEST=1` environment variable is set. |
|
|
45
|
+
|
|
46
|
+
A route whose bundle cannot be loaded answers `503` with a message naming the missing file and the build command that produces it, rather than surfacing a raw `ERR_MODULE_NOT_FOUND`.
|
|
@@ -46,7 +46,29 @@ export class AppModule {}
|
|
|
46
46
|
|
|
47
47
|
#### `forRoot(options?)`
|
|
48
48
|
|
|
49
|
-
Configures the module and returns a NestJS `DynamicModule` registered as `global`. It
|
|
49
|
+
Configures the module and returns a NestJS `DynamicModule` registered as `global`. It provides the resolved options under the `WORKFLOW_MODULE_OPTIONS` token, and (unless `skipBuild` is set) creates a [`NestLocalBuilder`](/docs/api-reference/workflow-nest/nest-local-builder) that builds the workflow bundles when the module initializes.
|
|
50
|
+
|
|
51
|
+
#### `forRootAsync(options)`
|
|
52
|
+
|
|
53
|
+
Same as `forRoot`, with the options produced by a factory so they can come from other providers.
|
|
54
|
+
|
|
55
|
+
{/* @skip-typecheck - config snippet, WorkflowModule imported above */}
|
|
56
|
+
|
|
57
|
+
```typescript title="src/app.module.ts" lineNumbers
|
|
58
|
+
WorkflowModule.forRootAsync({
|
|
59
|
+
imports: [ConfigModule],
|
|
60
|
+
inject: [ConfigService],
|
|
61
|
+
useFactory: (config: ConfigService) => ({
|
|
62
|
+
basePath: config.get("API_PREFIX"),
|
|
63
|
+
}),
|
|
64
|
+
});
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
| Parameter | Type | Description |
|
|
68
|
+
| --- | --- | --- |
|
|
69
|
+
| `imports` | `unknown[]` | Optional. Modules whose exported providers the factory injects. |
|
|
70
|
+
| `inject` | `unknown[]` | Optional. Providers passed to `useFactory`, in order. |
|
|
71
|
+
| `useFactory` | `(...args) => WorkflowModuleOptions \| Promise<WorkflowModuleOptions>` | Returns the module options. |
|
|
50
72
|
|
|
51
73
|
### Parameters
|
|
52
74
|
|
|
@@ -60,15 +82,53 @@ Extends [`NestBuilderOptions`](/docs/api-reference/workflow-nest/nest-local-buil
|
|
|
60
82
|
|
|
61
83
|
| Option | Type | Default | Description |
|
|
62
84
|
| --- | --- | --- | --- |
|
|
63
|
-
| `skipBuild` | `boolean` | `false` | Skip building workflow bundles on startup.
|
|
85
|
+
| `skipBuild` | `boolean` | `true` when `VERCEL` is set, else `false` | Skip building workflow bundles on startup. The bundles must already exist; startup fails with an explicit error if they do not. |
|
|
86
|
+
| `basePath` | `string` | adopted from `app.setGlobalPrefix()` | Route prefix the workflow endpoints are served under, applied to generated callback and webhook URLs. Set it when a reverse proxy mounts the app on a sub-path NestJS cannot see. |
|
|
87
|
+
| `manageWorldLifecycle` | `boolean` | `false` | Start the target World's background workers with the app and close them on shutdown. Required for self-hosted Worlds, which otherwise never pick up runs. |
|
|
88
|
+
| `preloadBundles` | `boolean` | `false` when `VERCEL` is set, else `true` | Load the generated bundles during startup instead of on the first request. On Vercel, dedicated functions serve the bundles, so there is nothing to preload. |
|
|
64
89
|
| `workingDir` | `string` | `process.cwd()` | Working directory for the NestJS application. |
|
|
65
90
|
| `dirs` | `string[]` | `['src']` | Directories to scan for workflow files. |
|
|
66
91
|
| `outDir` | `string` | `'.nestjs/workflow'` (relative to `workingDir`) | Output directory for generated workflow bundles. |
|
|
67
|
-
| `watch` | `boolean` | `false` |
|
|
92
|
+
| `watch` | `boolean` | `false` | Deprecated and ignored. Watch mode is not implemented for this builder. Use `nest start --watch`, which re-runs the startup build. |
|
|
68
93
|
| `moduleType` | `'es6' \| 'commonjs'` | `'es6'` | SWC module compilation type. Set to `'commonjs'` if your NestJS project compiles to CommonJS (CJS) through SWC. |
|
|
69
94
|
| `distDir` | `string` | `'dist'` | Directory where NestJS compiles `.ts` source files to `.js` (relative to `workingDir`). Used when `moduleType` is `'commonjs'`. Should match the `outDir` in your `tsconfig.json`. |
|
|
70
95
|
| `sourcemap` | `boolean \| 'inline' \| 'linked' \| 'external' \| 'both'` | `'inline'` (dev) / `false` (prod) | Controls source maps on generated workflow bundles. Defaults to `'inline'` in development and `false` in production. Can also be set via the `WORKFLOW_SOURCEMAP` environment variable. |
|
|
71
96
|
|
|
72
97
|
### Returns
|
|
73
98
|
|
|
74
|
-
`forRoot()`
|
|
99
|
+
`forRoot()` and `forRootAsync()` return a `DynamicModule` to include in the `imports` array of your root module.
|
|
100
|
+
|
|
101
|
+
## Injecting the resolved options
|
|
102
|
+
|
|
103
|
+
Both factories export the resolved options under `WORKFLOW_MODULE_OPTIONS`:
|
|
104
|
+
|
|
105
|
+
{/* @skip-typecheck - NestJS decorators require special TypeScript config */}
|
|
106
|
+
|
|
107
|
+
```typescript title="src/some.service.ts" lineNumbers
|
|
108
|
+
import { Inject, Injectable } from "@nestjs/common";
|
|
109
|
+
import {
|
|
110
|
+
WORKFLOW_MODULE_OPTIONS,
|
|
111
|
+
type WorkflowModuleOptions,
|
|
112
|
+
} from "workflow/nest";
|
|
113
|
+
|
|
114
|
+
@Injectable()
|
|
115
|
+
export class SomeService {
|
|
116
|
+
constructor(
|
|
117
|
+
@Inject(WORKFLOW_MODULE_OPTIONS)
|
|
118
|
+
private readonly options: WorkflowModuleOptions
|
|
119
|
+
) {}
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
The older `WORKFLOW_OPTIONS` token resolves to the same value and is kept for compatibility.
|
|
124
|
+
|
|
125
|
+
## Lifecycle
|
|
126
|
+
|
|
127
|
+
| Hook | Behaviour |
|
|
128
|
+
| --- | --- |
|
|
129
|
+
| `onModuleInit` | Reconciles the base path against `app.setGlobalPrefix()`, builds the bundles (or verifies they exist when `skipBuild` is set), optionally starts the World, and preloads the bundles. |
|
|
130
|
+
| `onApplicationShutdown` | Closes the World when `manageWorldLifecycle` is set. Call `app.enableShutdownHooks()` so this runs on a signal. |
|
|
131
|
+
|
|
132
|
+
<Callout type="warn">
|
|
133
|
+
Workflows and steps run outside the NestJS injector, so providers cannot be injected into `"use workflow"` or `"use step"` code. See [NestJS dependency injection is not available in workflows and steps](/docs/getting-started/nestjs#nestjs-dependency-injection-is-not-available-in-workflows-and-steps).
|
|
134
|
+
</Callout>
|
|
@@ -189,12 +189,15 @@ a normal return, not an error, and a missing run throws
|
|
|
189
189
|
|
|
190
190
|
```typescript lineNumbers
|
|
191
191
|
const result = await world.runs.list({ // [!code highlight]
|
|
192
|
+
status: "running", // [!code highlight]
|
|
192
193
|
pagination: { cursor },
|
|
193
194
|
}); // [!code highlight]
|
|
194
195
|
```
|
|
195
196
|
|
|
196
197
|
| Parameter | Type | Description |
|
|
197
198
|
|-----------|------|-------------|
|
|
199
|
+
| `params.workflowName` | `string` | Only return runs of this workflow |
|
|
200
|
+
| `params.status` | `WorkflowRunStatus \| WorkflowRunStatus[]` | Only return runs in this status. Pass an array to match any of the listed statuses; an empty array matches no runs. `@workflow/world-vercel` accepts a single status only and throws `WorkflowWorldError` (`INVALID_ARGUMENT`) for an array |
|
|
198
201
|
| `params.pagination.cursor` | `string` | Cursor for the next page |
|
|
199
202
|
| `params.resolveData` | `'all' \| 'none'` | Whether to include input/output data |
|
|
200
203
|
|
|
@@ -219,7 +222,7 @@ To cancel a batch in one call, a world may implement the optional `runs.cancelMa
|
|
|
219
222
|
| Field | Type | Description |
|
|
220
223
|
|-------|------|-------------|
|
|
221
224
|
| `runId` | `string` | Unique run identifier |
|
|
222
|
-
| `status` | `string` | `'running'`, `'completed'`, `'failed'`, `'cancelled'` |
|
|
225
|
+
| `status` | `string` | `'pending'`, `'running'`, `'completed'`, `'failed'`, `'cancelled'` |
|
|
223
226
|
| `workflowName` | `string` | Machine-readable workflow identifier |
|
|
224
227
|
| `input` | `any` | Workflow input data (when `resolveData: 'all'`) |
|
|
225
228
|
| `output` | `any` | Workflow output data (when `resolveData: 'all'`) |
|
|
@@ -53,6 +53,15 @@ Accepted values:
|
|
|
53
53
|
- The SDK's own runtime serde classes (for example, `Run`) stay registered because they are reached through a seeded entry point, and imports *within* `node_modules` are still followed.
|
|
54
54
|
- Explicit framework config wins over this environment variable.
|
|
55
55
|
|
|
56
|
+
## Testing
|
|
57
|
+
|
|
58
|
+
### `WORKFLOW_VITEST_VERSION_CHECK`
|
|
59
|
+
|
|
60
|
+
- Default: enabled
|
|
61
|
+
- Read by [`@workflow/vitest`](/docs/api-reference/vitest) in `globalSetup`, once per test run.
|
|
62
|
+
- The plugin builds and runs your workflows against the copy of `@workflow/core` it was installed with. Before building, it compares that copy with the one your app resolves: a different major fails the run with the install command that fixes it, and any other difference logs a warning.
|
|
63
|
+
- Set `off` (or `0` / `false`) to skip the check, for example in a setup that resolves two copies on purpose.
|
|
64
|
+
|
|
56
65
|
## Development diagnostics
|
|
57
66
|
|
|
58
67
|
### `WORKFLOW_DEV_HMR_LOGS`
|
|
@@ -27,6 +27,8 @@ Broader signals are deliberately ignored. `vercel env pull` writes `VERCEL=1` in
|
|
|
27
27
|
|
|
28
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
29
|
|
|
30
|
+
A deployment can land on the Local World without pinning anything, and without that warning, if the project has cleared **Enable access to System Environment Variables** under **Settings**, then **Environment Variables**. That checkbox is what makes Vercel expose `VERCEL_DEPLOYMENT_ID` to your build and your functions; with it off, there is no signal to detect, so detection and the warning both see an ordinary non-Vercel process. See [System environment variables](/worlds/vercel#system-environment-variables) for how to confirm and fix it.
|
|
31
|
+
|
|
30
32
|
Set `WORKFLOW_TARGET_WORLD` only when you want to use a custom or self-hosted World:
|
|
31
33
|
|
|
32
34
|
- `local`: Alias for `@workflow/world-local`.
|
|
@@ -197,7 +199,7 @@ The Vercel World is configured automatically inside Vercel deployments. The plat
|
|
|
197
199
|
|
|
198
200
|
Most applications should not set `WORKFLOW_VERCEL_*` variables on Vercel. They configure tooling that talks to a Vercel Workflow project from outside a deployment, such as the Workflow CLI, the web user interface (UI), continuous integration (CI), or tests. The runtime warns if these variables are set in a deployed Vercel Function because they do not control runtime configuration there.
|
|
199
201
|
|
|
200
|
-
Platform-provided values such as `VERCEL_DEPLOYMENT_ID`, `VERCEL_PROJECT_ID`, and `VERCEL_DEPLOYMENT_KEY` are read by the runtime inside Vercel deployments. Do not set them yourself.
|
|
202
|
+
Platform-provided values such as `VERCEL_DEPLOYMENT_ID`, `VERCEL_PROJECT_ID`, and `VERCEL_DEPLOYMENT_KEY` are read by the runtime inside Vercel deployments. Do not set them yourself; keep [system environment variables](/worlds/vercel#system-environment-variables) enabled for the project so that Vercel provides them.
|
|
201
203
|
|
|
202
204
|
### `token`
|
|
203
205
|
|
package/docs/deploying.mdx
CHANGED
|
@@ -51,6 +51,17 @@ vercel deploy
|
|
|
51
51
|
|
|
52
52
|
<FluidComputeCallout />
|
|
53
53
|
|
|
54
|
+
<Callout type="warn">
|
|
55
|
+
**Enable access to System Environment Variables before deploying.** Workflow
|
|
56
|
+
recognizes a Vercel deployment by `VERCEL_DEPLOYMENT_ID`, which Vercel exposes
|
|
57
|
+
to your build and your functions only when the **Enable access to System
|
|
58
|
+
Environment Variables** checkbox is selected under **Settings**, then
|
|
59
|
+
**Environment Variables**, in your project. With it cleared, the deployment
|
|
60
|
+
falls back to the Local World and every run fails on the read-only
|
|
61
|
+
filesystem. See
|
|
62
|
+
[System environment variables](/worlds/vercel#system-environment-variables).
|
|
63
|
+
</Callout>
|
|
64
|
+
|
|
54
65
|
<Callout>
|
|
55
66
|
Learn more about the [Vercel World](/worlds/vercel) and its capabilities, including [multi-region](/worlds/vercel#multi-region).
|
|
56
67
|
</Callout>
|
|
@@ -398,15 +398,11 @@ export default async function handler(req: express.Request, res: express.Respons
|
|
|
398
398
|
}
|
|
399
399
|
```
|
|
400
400
|
|
|
401
|
-
|
|
402
|
-
`VERCEL` is set
|
|
401
|
+
No module change is needed for the in-process build: `skipBuild` defaults to `true` when the
|
|
402
|
+
`VERCEL` environment variable is set, because the Build Output already contains the bundles and
|
|
403
|
+
the deployed filesystem is read-only.
|
|
403
404
|
|
|
404
|
-
|
|
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`:
|
|
405
|
+
Set your build command so the Build Output is produced after `nest build`:
|
|
410
406
|
|
|
411
407
|
```json title="package.json" lineNumbers
|
|
412
408
|
{
|
|
@@ -440,7 +436,9 @@ WorkflowModule.forRoot({
|
|
|
440
436
|
// Output directory for generated bundles (default: '.nestjs/workflow')
|
|
441
437
|
outDir: '.nestjs/workflow',
|
|
442
438
|
|
|
443
|
-
// Skip building
|
|
439
|
+
// Skip building when bundles are pre-built with `workflow-nest build`
|
|
440
|
+
// (default: true when VERCEL is set, false otherwise). Startup fails if the
|
|
441
|
+
// bundles are missing.
|
|
444
442
|
skipBuild: false,
|
|
445
443
|
|
|
446
444
|
// SWC module type: 'es6' (default) or 'commonjs'
|
|
@@ -460,9 +458,187 @@ WorkflowModule.forRoot({
|
|
|
460
458
|
// size limit) at the cost of stack traces pointing at generated code.
|
|
461
459
|
// Can also be set via the WORKFLOW_SOURCEMAP environment variable.
|
|
462
460
|
sourcemap: 'inline',
|
|
461
|
+
|
|
462
|
+
// Route prefix the workflow endpoints are served under. Leave unset to adopt
|
|
463
|
+
// app.setGlobalPrefix() automatically; set it when a reverse proxy mounts the
|
|
464
|
+
// app on a sub-path NestJS does not know about. See "Global prefixes" below.
|
|
465
|
+
basePath: '/api',
|
|
466
|
+
|
|
467
|
+
// Start the target World's background workers with the app and close them on
|
|
468
|
+
// shutdown. Self-hosted Worlds (for example @workflow/world-postgres) need
|
|
469
|
+
// this or runs are created and never picked up. Leave off on Vercel.
|
|
470
|
+
manageWorldLifecycle: false,
|
|
471
|
+
|
|
472
|
+
// Load the generated bundles during startup instead of on the first request
|
|
473
|
+
// (default: true, or false when VERCEL is set because dedicated functions
|
|
474
|
+
// serve the bundles there).
|
|
475
|
+
preloadBundles: true,
|
|
463
476
|
});
|
|
464
477
|
```
|
|
465
478
|
|
|
479
|
+
Options can also come from other providers with `forRootAsync`:
|
|
480
|
+
|
|
481
|
+
{/*@skip-typecheck - Configuration snippet, imports shown above*/}
|
|
482
|
+
|
|
483
|
+
```typescript
|
|
484
|
+
WorkflowModule.forRootAsync({
|
|
485
|
+
imports: [ConfigModule],
|
|
486
|
+
inject: [ConfigService],
|
|
487
|
+
useFactory: (config: ConfigService) => ({
|
|
488
|
+
basePath: config.get('API_PREFIX'),
|
|
489
|
+
}),
|
|
490
|
+
});
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
---
|
|
494
|
+
|
|
495
|
+
## Global prefixes and sub-paths
|
|
496
|
+
|
|
497
|
+
`app.setGlobalPrefix()` moves the workflow routes. The SDK has to generate its
|
|
498
|
+
queue callback and webhook URLs under the same prefix, or every delivery 404s and
|
|
499
|
+
runs stay `pending`.
|
|
500
|
+
|
|
501
|
+
`WorkflowModule` reads the global prefix during startup and adopts it, so this
|
|
502
|
+
works with no configuration:
|
|
503
|
+
|
|
504
|
+
{/*@skip-typecheck - Bootstrap snippet*/}
|
|
505
|
+
|
|
506
|
+
```typescript
|
|
507
|
+
const app = await NestFactory.create(AppModule);
|
|
508
|
+
app.setGlobalPrefix('api'); // workflow URLs become /api/.well-known/workflow/v1/...
|
|
509
|
+
await app.listen(3000);
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
Set `basePath` explicitly when the prefix is applied outside NestJS, for example
|
|
513
|
+
by a reverse proxy that strips it before the request reaches your app. An
|
|
514
|
+
explicit `basePath` wins over the global prefix, and a disagreement between the
|
|
515
|
+
two is logged at startup.
|
|
516
|
+
|
|
517
|
+
For Vercel, pass the same value to the build so the deployed queue-consumer
|
|
518
|
+
function generates matching URLs:
|
|
519
|
+
|
|
520
|
+
```bash
|
|
521
|
+
workflow-nest build --vercel --base-path /api
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
<Callout type="warn">
|
|
525
|
+
`app.enableVersioning()` moves the workflow routes the same way, and is not
|
|
526
|
+
handled automatically. Exclude the workflow controller from versioning, or mount
|
|
527
|
+
it where the SDK expects it.
|
|
528
|
+
</Callout>
|
|
529
|
+
|
|
530
|
+
---
|
|
531
|
+
|
|
532
|
+
## Raw request bodies
|
|
533
|
+
|
|
534
|
+
The workflow routes are a byte pipe. A webhook that verifies a signature over its
|
|
535
|
+
raw body (Stripe, GitHub, Shopify, Slack) only works if the bytes the sender
|
|
536
|
+
signed reach the workflow unchanged, and a body parser that has already turned
|
|
537
|
+
the request into an object destroys them.
|
|
538
|
+
|
|
539
|
+
Create the app with `rawBody` so the original bytes stay available:
|
|
540
|
+
|
|
541
|
+
{/*@skip-typecheck - Bootstrap snippet*/}
|
|
542
|
+
|
|
543
|
+
```typescript
|
|
544
|
+
const app = await NestFactory.create(AppModule, { rawBody: true });
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
Without it, `@workflow/nest` falls back to re-serializing the parsed body with
|
|
548
|
+
`JSON.stringify` and logs a warning once. That changes whitespace and key order,
|
|
549
|
+
so signature verification fails.
|
|
550
|
+
|
|
551
|
+
Content types no body parser claims (XML, `application/x-www-form-urlencoded`
|
|
552
|
+
without the parser registered, custom media types) are read straight from the
|
|
553
|
+
request stream and need no configuration.
|
|
554
|
+
|
|
555
|
+
<Callout type="info">
|
|
556
|
+
On Vercel the webhook route is served by its own function rather than by your
|
|
557
|
+
NestJS app, so this only affects self-hosted deployments.
|
|
558
|
+
</Callout>
|
|
559
|
+
|
|
560
|
+
---
|
|
561
|
+
|
|
562
|
+
## NestJS dependency injection is not available in workflows and steps
|
|
563
|
+
|
|
564
|
+
Workflows and steps do not run inside your NestJS application. They are compiled
|
|
565
|
+
into separate bundles, so **the Nest injector, your providers, and anything
|
|
566
|
+
built on the request context are out of reach from `"use workflow"` and
|
|
567
|
+
`"use step"` code.**
|
|
568
|
+
|
|
569
|
+
Concretely, none of these work inside a step:
|
|
570
|
+
|
|
571
|
+
- injecting a provider, or resolving one with `app.get(MyService)`
|
|
572
|
+
- `@Injectable()` classes reached through a module-level reference to the app
|
|
573
|
+
- request-scoped providers, and `AsyncLocalStorage` context such as `nestjs-cls`
|
|
574
|
+
- guards, interceptors, pipes, and filters
|
|
575
|
+
- the Nest `Logger`
|
|
576
|
+
|
|
577
|
+
Two mechanics cause this, and neither has a workaround:
|
|
578
|
+
|
|
579
|
+
1. A step's bundle gets **its own copy** of any application file it imports.
|
|
580
|
+
Module-level state is therefore duplicated, and a class imported into a step
|
|
581
|
+
is a different class object from the one your module registered. Nest uses the
|
|
582
|
+
class itself as the injection token, so `app.get(MyService)` from a step
|
|
583
|
+
raises `UnknownElementException` even when it can reach the app.
|
|
584
|
+
2. On Vercel, workflows and steps run in the queue-consumer function, a separate
|
|
585
|
+
function from the one serving your NestJS app. There is no shared process to
|
|
586
|
+
reach into.
|
|
587
|
+
|
|
588
|
+
Write steps as plain functions over their arguments, and keep the wiring in your
|
|
589
|
+
controllers and providers:
|
|
590
|
+
|
|
591
|
+
{/*@skip-typecheck - Illustrates the pattern, not a complete app*/}
|
|
592
|
+
|
|
593
|
+
```typescript
|
|
594
|
+
// A step takes what it needs as arguments and builds its own clients.
|
|
595
|
+
async function chargeCustomer(customerId: string, cents: number) {
|
|
596
|
+
'use step';
|
|
597
|
+
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
|
|
598
|
+
return await stripe.charges.create({ customer: customerId, amount: cents });
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// The controller stays a normal Nest controller with normal DI.
|
|
602
|
+
@Controller('billing')
|
|
603
|
+
export class BillingController {
|
|
604
|
+
constructor(private readonly customers: CustomerService) {}
|
|
605
|
+
|
|
606
|
+
@Post('charge')
|
|
607
|
+
async charge(@Body() body: { id: string }) {
|
|
608
|
+
const customer = await this.customers.find(body.id);
|
|
609
|
+
// Pass plain, serializable values into the workflow.
|
|
610
|
+
await start(billingWorkflow, [customer.id, customer.planCents]);
|
|
611
|
+
return { started: true };
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
```
|
|
615
|
+
|
|
616
|
+
Configuration a step needs should come from the environment rather than
|
|
617
|
+
`ConfigService`, and shared logic should live in plain modules that a step can
|
|
618
|
+
import without pulling in `@nestjs/common`.
|
|
619
|
+
|
|
620
|
+
<Callout type="info">
|
|
621
|
+
Importing a file that uses `@nestjs/common` into a step is supported and builds
|
|
622
|
+
correctly, it simply gives you a class with no injected dependencies. Prefer
|
|
623
|
+
plain functions so the intent is clear.
|
|
624
|
+
</Callout>
|
|
625
|
+
|
|
626
|
+
---
|
|
627
|
+
|
|
628
|
+
## Production checklist
|
|
629
|
+
|
|
630
|
+
- Run `workflow-nest build` in your build step and set `skipBuild: true`, or
|
|
631
|
+
leave `skipBuild` unset so the bundles are built during startup. With
|
|
632
|
+
`skipBuild` set and no bundles present, startup fails with an explicit error
|
|
633
|
+
rather than serving broken workflow routes.
|
|
634
|
+
- Create the app with `{ rawBody: true }` if you receive signed webhooks.
|
|
635
|
+
- Set `basePath` (or rely on the adopted global prefix) so generated URLs match
|
|
636
|
+
the routes NestJS serves.
|
|
637
|
+
- Set `manageWorldLifecycle: true` for a self-hosted World, and call
|
|
638
|
+
`app.enableShutdownHooks()` so the World is closed on a signal.
|
|
639
|
+
- `skipBuild` defaults to `true` when the `VERCEL` environment variable is set,
|
|
640
|
+
so no Vercel-specific branch is needed in your module configuration.
|
|
641
|
+
|
|
466
642
|
## Troubleshooting
|
|
467
643
|
|
|
468
644
|
### `start()` says it received an invalid workflow function
|
|
@@ -57,6 +57,8 @@ export async function cleanupAttributes() {
|
|
|
57
57
|
|
|
58
58
|
Attribute keys must be 1-256 characters, values must be strings up to 256 bytes, and each run can have up to 64 attributes. Keys that start with `$` are reserved for framework and library code.
|
|
59
59
|
|
|
60
|
+
Each `setAttributes` call also writes one `attr_set` event, and that event's complete data (keys, values, and writer metadata) must fit in 8192 UTF-8 JSON bytes (8KiB). Split large updates across several calls; the calls still share the 64-attribute limit. A call that exceeds a limit rejects with [`FatalError`](/docs/api-reference/workflow/fatal-error) before anything is written, so catch it when the metadata is best-effort.
|
|
61
|
+
|
|
60
62
|
## Reserved keys
|
|
61
63
|
|
|
62
64
|
When `start()` is called from inside a running workflow or step, the new run is automatically tagged with two reserved attributes:
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Lifecycle Hooks
|
|
3
|
+
description: Register global handlers that observe workflow runs completing or failing, for centralized reporting to services like Sentry.
|
|
4
|
+
type: guide
|
|
5
|
+
summary: Observe run completions and failures from a single place with registerLifecycleHooks.
|
|
6
|
+
prerequisites:
|
|
7
|
+
- /docs/foundations/workflows-and-steps
|
|
8
|
+
related:
|
|
9
|
+
- /docs/observability
|
|
10
|
+
- /docs/observability/tracing
|
|
11
|
+
- /docs/errors
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
Lifecycle hooks let you register global handlers that observe workflow runs completing or failing on your app's compute. They can observe even the failures that never reach a `try/catch` in workflow code, such as a replay timing out or a run exhausting its queue deliveries. Use them for best-effort centralized error reporting, such as forwarding failed runs to Sentry without wrapping each workflow body.
|
|
15
|
+
|
|
16
|
+
## Registering hooks
|
|
17
|
+
|
|
18
|
+
Call `registerLifecycleHooks` from `workflow/api` early in your application's lifecycle, so the handlers exist before the first run finishes. In Next.js, [`instrumentation.ts`](https://nextjs.org/docs/app/building-your-application/optimizing/instrumentation) is the natural place; in any other app, any module that loads at startup works.
|
|
19
|
+
|
|
20
|
+
```typescript title="instrumentation.ts" lineNumbers
|
|
21
|
+
export async function register() {
|
|
22
|
+
if (process.env.NEXT_RUNTIME === "nodejs") {
|
|
23
|
+
const { registerLifecycleHooks } = await import("workflow/api")
|
|
24
|
+
|
|
25
|
+
registerLifecycleHooks({
|
|
26
|
+
async onRunCompleted({ run, workflowName }) {
|
|
27
|
+
console.log(`Run ${run.runId} (${workflowName}) completed`)
|
|
28
|
+
},
|
|
29
|
+
async onRunFailed({ run, workflowName, error }) {
|
|
30
|
+
console.error(
|
|
31
|
+
`Run ${run.runId} (${workflowName}) failed with ${error.errorCode}:`,
|
|
32
|
+
error.cause
|
|
33
|
+
)
|
|
34
|
+
},
|
|
35
|
+
})
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Keep the dynamic `workflow/api` import inside the `NEXT_RUNTIME === "nodejs"` guard. Next.js also compiles `instrumentation.ts` for the Edge runtime. A top-level static import pulls Node.js-only dependencies into that compilation and breaks webpack Edge builds, even if the registration call is guarded.
|
|
41
|
+
|
|
42
|
+
`registerLifecycleHooks` returns an unregister function. You can register multiple hook sets, and handlers run in registration order.
|
|
43
|
+
|
|
44
|
+
## Handler parameters
|
|
45
|
+
|
|
46
|
+
Both handlers receive a `workflowName` string and the [`Run`](/docs/api-reference/workflow-api/get-run) instance for the transitioned run. `workflowName` is the machine-readable workflow identifier, such as `workflow//./src/workflows/order//processOrder`. Use this parameter to filter runs without a backend read. `run.runId` is also available without a read.
|
|
47
|
+
|
|
48
|
+
The `Run` instance hydrates lazily. Accessors such as `run.workflowName`, `run.status`, and `run.returnValue` still fetch from the backend when used. Lazy access defers those reads; it does not make them free.
|
|
49
|
+
|
|
50
|
+
`onRunFailed` additionally receives the failure as a `WorkflowRunFailedError`, the same shape `run.returnValue` rejects with:
|
|
51
|
+
|
|
52
|
+
- `error.errorCode`: the failure classification (`USER_ERROR`, `RUNTIME_ERROR`, `MAX_DELIVERIES_EXCEEDED`, and more). See [error codes](/docs/errors) for the full list.
|
|
53
|
+
- `error.cause`: the thrown value hydrated from the persisted error data, with registered Error subclass identity, message, stack, and cause chain preserved. Streamed values load lazily when consumed, and abort signals reflect their persisted state without live subscriptions. If hydration fails, the cause is a generic `Error`, matching `run.returnValue`'s fallback. Any JavaScript value can be thrown, so this is typed `unknown`.
|
|
54
|
+
|
|
55
|
+
In `onRunFailed`, `run.returnValue` rejects because the run failed. Use `error.cause` to inspect or report the thrown value instead of awaiting `run.returnValue`.
|
|
56
|
+
|
|
57
|
+
## Reporting failed runs to Sentry
|
|
58
|
+
|
|
59
|
+
This example reports failures for workflows named `processOrder`. Remove the filter to report failures from all workflows.
|
|
60
|
+
|
|
61
|
+
```typescript title="instrumentation.ts" lineNumbers
|
|
62
|
+
export async function register() {
|
|
63
|
+
if (process.env.NEXT_RUNTIME === "nodejs") {
|
|
64
|
+
const { registerLifecycleHooks } = await import("workflow/api")
|
|
65
|
+
const Sentry = await import("@sentry/nextjs")
|
|
66
|
+
|
|
67
|
+
Sentry.init({ dsn: process.env.SENTRY_DSN })
|
|
68
|
+
|
|
69
|
+
registerLifecycleHooks({
|
|
70
|
+
async onRunFailed({ run, workflowName, error }) {
|
|
71
|
+
if (!workflowName.endsWith("//processOrder")) return
|
|
72
|
+
|
|
73
|
+
Sentry.captureException(error.cause ?? error, {
|
|
74
|
+
tags: {
|
|
75
|
+
workflowRunId: run.runId,
|
|
76
|
+
workflowName,
|
|
77
|
+
errorCode: error.errorCode,
|
|
78
|
+
},
|
|
79
|
+
})
|
|
80
|
+
await Sentry.flush(2000)
|
|
81
|
+
},
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## How handlers behave
|
|
88
|
+
|
|
89
|
+
- **Host-only.** Handlers run with full Node.js access, never inside the workflow's sandboxed VM. Calling `registerLifecycleHooks` from workflow code throws.
|
|
90
|
+
- **Fire-and-forget.** Handlers cannot delay or change the run's outcome. The runtime logs and swallows a throwing handler, and the remaining handlers still run. On Vercel, `waitUntil` keeps the invocation alive while handlers finish, subject to the invocation's duration limit. On other hosts, handlers run as detached work. If the host freezes or terminates the process after the response, handlers may not complete.
|
|
91
|
+
- **Best-effort delivery.** Each registered handler is invoked at most once by the invocation that writes the terminal event. For a failure, this happens after any workflow or step retries are exhausted. Callbacks are not retried if they throw or the process dies, so a callback may never run or may stop before completing. The [event log](/docs/how-it-works/event-sourcing) is the system of record, not lifecycle callbacks.
|
|
92
|
+
- **Fires where the transition is recorded.** Terminal transitions recorded outside your app's compute do **not** fire handlers. For example, when you cancel a run from the CLI or the Vercel dashboard, the backend writes that transition, so no handler runs.
|
|
93
|
+
- **Register everywhere your workflows run.** The terminal write can happen in any function invocation that processes the run's queue messages, so registration must run at startup in every instance of the app (which `instrumentation.ts` guarantees).
|
package/docs/testing/index.mdx
CHANGED
|
@@ -85,7 +85,19 @@ Unit testing works well for individual steps. A workflow that only calls steps c
|
|
|
85
85
|
For workflows that rely on runtime features like [hooks](/docs/foundations/hooks), [webhooks](/docs/foundations/hooks#understanding-webhooks), [`sleep()`](/docs/api-reference/workflow/sleep), or error retries, you need to test against a real workflow setup. The `@workflow/vitest` plugin handles everything automatically: it compiles your workflow directives, builds the runtime bundles, and executes workflows entirely in-process. No server required.
|
|
86
86
|
|
|
87
87
|
<Callout type="warn">
|
|
88
|
-
`vi.mock()`
|
|
88
|
+
`vi.mock()` cannot reach code inside workflow functions at all, and reaches step code only under specific conditions. Read [Mocking](#mocking) before reaching for it.
|
|
89
|
+
</Callout>
|
|
90
|
+
|
|
91
|
+
### Installation
|
|
92
|
+
|
|
93
|
+
```package-install
|
|
94
|
+
npm i -D @workflow/vitest@beta
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
<Callout type="warn">
|
|
98
|
+
**Workflow 5 needs the `beta` tag.** `@workflow/vitest@latest` is still the 4.x line, so a plain `npm i -D @workflow/vitest` installs a v4 plugin next to a v5 app. Install `@workflow/vitest@beta`, or pin the beta your app is on (for example `@workflow/vitest@5.0.0-beta.53`), and upgrade it together with `workflow`.
|
|
99
|
+
|
|
100
|
+
The plugin builds and runs your workflows against the copy of `@workflow/core` it was installed with, so it checks this for you. At the start of each run it compares that copy with the one your app resolves: a different major fails the run with the install command that fixes it, and any other difference logs a warning. Set [`WORKFLOW_VITEST_VERSION_CHECK=off`](/docs/configuration/build-and-diagnostics#workflow_vitest_version_check) to skip the check.
|
|
89
101
|
</Callout>
|
|
90
102
|
|
|
91
103
|
### Vitest configuration
|
|
@@ -303,6 +315,72 @@ describe("ingestWorkflow", () => {
|
|
|
303
315
|
});
|
|
304
316
|
```
|
|
305
317
|
|
|
318
|
+
### Referencing workflows by name
|
|
319
|
+
|
|
320
|
+
Importing the workflow function is the best way to start it: [`start()`](/docs/api-reference/workflow-api/start) keeps its argument and return types. When a test cannot import it, because the workflow lives in a module the test does not pull in or because the test drives runs by name, look it up with [`getWorkflowRef()`](/docs/api-reference/vitest#getworkflowref) instead of hand-writing the generated `workflow//...` id:
|
|
321
|
+
|
|
322
|
+
```typescript title="workflows/approval.integration.test.ts" lineNumbers
|
|
323
|
+
import { describe, it, expect } from "vitest";
|
|
324
|
+
import { start } from "workflow/api";
|
|
325
|
+
import { getWorkflowRef, listWorkflowRefs } from "@workflow/vitest"; // [!code highlight]
|
|
326
|
+
|
|
327
|
+
describe("approvalWorkflow", () => {
|
|
328
|
+
it("is part of the test build", () => {
|
|
329
|
+
expect(listWorkflowRefs().map((ref) => ref.name)).toContain( // [!code highlight]
|
|
330
|
+
"approvalWorkflow"
|
|
331
|
+
);
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it("can be started by name", async () => {
|
|
335
|
+
const run = await start(getWorkflowRef("approvalWorkflow"), ["doc-1"]); // [!code highlight]
|
|
336
|
+
expect(run.runId).toMatch(/^wrun_/);
|
|
337
|
+
});
|
|
338
|
+
});
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
The names come from the manifest the test build writes next to its bundles, so they follow your code when files move. When one exported name appears in several files, qualify it with the file, `getWorkflowRef("workflows/approval.ts#approvalWorkflow")`, which also matches by path suffix. An unknown or ambiguous name throws with the workflows the build does contain.
|
|
342
|
+
|
|
343
|
+
<Callout type="info">
|
|
344
|
+
A reference is matched by name, so the run is typed as `Run<unknown>`. Import the workflow function when you want the argument and return types checked.
|
|
345
|
+
</Callout>
|
|
346
|
+
|
|
347
|
+
### Mocking
|
|
348
|
+
|
|
349
|
+
Steps do not run from your test file's module graph. They run from the bundles the plugin builds, and workflow bodies run from a code string inside the sandboxed VM, so `vi.mock()` reaches less here than in an ordinary Vitest test:
|
|
350
|
+
|
|
351
|
+
| What you mock | Step code sees it | Workflow body sees it |
|
|
352
|
+
| --- | --- | --- |
|
|
353
|
+
| An npm package a step file imports | Only when the generated bundles load through Vitest's module runner (see below) | No |
|
|
354
|
+
| An npm package imported by a local module that a step file imports | Same as above | No |
|
|
355
|
+
| A project-local module a step file imports | No: it is bundled into the step bundle, so there is no module left to replace | No |
|
|
356
|
+
| A step function imported and called directly, with no `workflow()` plugin | Yes, ordinary Vitest rules | n/a |
|
|
357
|
+
|
|
358
|
+
Workflow bodies execute in a VM that has no module registry, so nothing can intercept their imports. That is the same reason side effects belong in steps: if a dependency needs mocking, it belongs on the step side.
|
|
359
|
+
|
|
360
|
+
Whether step code sees an npm mock depends on how Vitest loaded the plugin. In a normal install `@workflow/vitest` is external to Vitest, Node loads the generated bundle directly, and steps get the real package. Ask for the other behavior explicitly:
|
|
361
|
+
|
|
362
|
+
```typescript title="vitest.integration.config.ts" lineNumbers
|
|
363
|
+
import { defineConfig } from "vitest/config";
|
|
364
|
+
import { workflow } from "@workflow/vitest";
|
|
365
|
+
|
|
366
|
+
export default defineConfig({
|
|
367
|
+
plugins: [workflow()],
|
|
368
|
+
test: {
|
|
369
|
+
// Route the generated bundles through Vitest's module runner, so vi.mock()
|
|
370
|
+
// applies to the npm packages your steps import.
|
|
371
|
+
server: { deps: { inline: [/@workflow\/vitest/] } }, // [!code highlight]
|
|
372
|
+
},
|
|
373
|
+
});
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
The cost is that the generated bundle goes through Vite's transform pipeline in every test worker.
|
|
377
|
+
|
|
378
|
+
Three approaches work regardless of how the plugin was loaded:
|
|
379
|
+
|
|
380
|
+
- **Unit test the step directly.** Without the plugin, `"use step"` is a no-op and the function is an ordinary async function, so `vi.mock()` behaves normally.
|
|
381
|
+
- **Pass the dependency in.** A step that takes its collaborator as an argument is controlled by the caller, with no module interception involved.
|
|
382
|
+
- **Feed values through hooks.** Resume a [hook](/docs/foundations/hooks) with the data you want instead of mocking whatever would have produced it.
|
|
383
|
+
|
|
306
384
|
### Manual setup
|
|
307
385
|
|
|
308
386
|
If you need more control over the test lifecycle, the plugin also exports the individual setup functions:
|
|
@@ -403,6 +481,10 @@ Workflows may take longer to execute than typical unit tests, especially when th
|
|
|
403
481
|
|
|
404
482
|
Integration tests are the right place to verify that your workflows handle errors correctly, including retryable errors, fatal errors, and timeout scenarios.
|
|
405
483
|
|
|
484
|
+
### Upgrade `@workflow/vitest` with the SDK
|
|
485
|
+
|
|
486
|
+
`@workflow/vitest` carries its own copy of the Workflow runtime, so treat it as part of the same upgrade as `workflow`. While Workflow 5 is in beta, that means the `beta` tag on both. See [Installation](#installation).
|
|
487
|
+
|
|
406
488
|
## Further reading
|
|
407
489
|
|
|
408
490
|
- [Hooks & Webhooks](/docs/foundations/hooks) - Pausing and resuming workflows with external data
|
|
@@ -410,7 +492,7 @@ Integration tests are the right place to verify that your workflows handle error
|
|
|
410
492
|
- [`resumeHook()` API Reference](/docs/api-reference/workflow-api/resume-hook) - Resume hooks with data
|
|
411
493
|
- [`resumeWebhook()` API Reference](/docs/api-reference/workflow-api/resume-webhook) - Resume webhooks with Request objects
|
|
412
494
|
- [`getRun()` API Reference](/docs/api-reference/workflow-api/get-run) - Check workflow run status and wake up sleeping runs
|
|
413
|
-
- [`@workflow/vitest` API Reference](/docs/api-reference/vitest) - Test helpers: `waitForSleep()`, `waitForHook()`, and plugin setup
|
|
495
|
+
- [`@workflow/vitest` API Reference](/docs/api-reference/vitest) - Test helpers: `waitForSleep()`, `waitForHook()`, `getWorkflowRef()`, and plugin setup
|
|
414
496
|
- [Vite Integration](/docs/getting-started/vite) - Set up the Vite plugin
|
|
415
497
|
- [Observability](/docs/observability) - Inspect and debug workflow runs with the CLI and Web UI
|
|
416
498
|
- [Server-based testing](/docs/testing/server-based) - Integration testing with a running server
|
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.55",
|
|
4
4
|
"description": "Workflow SDK - Build durable, resilient, and observable workflows",
|
|
5
5
|
"main": "dist/typescript-plugin.cjs",
|
|
6
6
|
"type": "module",
|
|
@@ -58,19 +58,19 @@
|
|
|
58
58
|
}
|
|
59
59
|
},
|
|
60
60
|
"dependencies": {
|
|
61
|
-
"@workflow/astro": "5.0.0-beta.
|
|
62
|
-
"@workflow/cli": "5.0.0-beta.
|
|
63
|
-
"@workflow/core": "5.0.0-beta.
|
|
64
|
-
"@workflow/errors": "5.0.0-beta.
|
|
61
|
+
"@workflow/astro": "5.0.0-beta.55",
|
|
62
|
+
"@workflow/cli": "5.0.0-beta.55",
|
|
63
|
+
"@workflow/core": "5.0.0-beta.55",
|
|
64
|
+
"@workflow/errors": "5.0.0-beta.22",
|
|
65
65
|
"@workflow/typescript-plugin": "5.0.0-beta.5",
|
|
66
66
|
"@workflow/utils": "5.0.0-beta.10",
|
|
67
67
|
"ms": "2.1.3",
|
|
68
|
-
"@workflow/next": "5.0.0-beta.
|
|
69
|
-
"@workflow/nest": "5.0.0-beta.
|
|
70
|
-
"@workflow/nitro": "5.0.0-beta.
|
|
71
|
-
"@workflow/nuxt": "5.0.0-beta.
|
|
72
|
-
"@workflow/sveltekit": "5.0.0-beta.
|
|
73
|
-
"@workflow/rollup": "5.0.0-beta.
|
|
68
|
+
"@workflow/next": "5.0.0-beta.55",
|
|
69
|
+
"@workflow/nest": "5.0.0-beta.55",
|
|
70
|
+
"@workflow/nitro": "5.0.0-beta.55",
|
|
71
|
+
"@workflow/nuxt": "5.0.0-beta.55",
|
|
72
|
+
"@workflow/sveltekit": "5.0.0-beta.55",
|
|
73
|
+
"@workflow/rollup": "5.0.0-beta.55"
|
|
74
74
|
},
|
|
75
75
|
"devDependencies": {
|
|
76
76
|
"@types/ms": "2.1.0",
|