workflow 4.2.0-beta.76 → 4.2.0-beta.77

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.
Files changed (31) hide show
  1. package/docs/api-reference/workflow-ai/durable-agent.mdx +86 -1
  2. package/docs/api-reference/workflow-api/get-world.mdx +51 -0
  3. package/docs/api-reference/workflow-api/start.mdx +5 -0
  4. package/docs/api-reference/workflow-next/with-workflow.mdx +51 -0
  5. package/docs/changelog/meta.json +1 -1
  6. package/docs/changelog/resilient-start.mdx +327 -0
  7. package/docs/deploying/building-a-world.mdx +16 -6
  8. package/docs/deploying/index.mdx +2 -2
  9. package/docs/deploying/world/vercel-world.mdx +30 -1
  10. package/docs/errors/hook-conflict.mdx +1 -1
  11. package/docs/errors/node-js-module-in-workflow.mdx +1 -1
  12. package/docs/errors/start-invalid-workflow-function.mdx +83 -58
  13. package/docs/foundations/starting-workflows.mdx +1 -1
  14. package/docs/foundations/streaming.mdx +1 -1
  15. package/docs/getting-started/astro.mdx +17 -0
  16. package/docs/getting-started/express.mdx +17 -0
  17. package/docs/getting-started/fastify.mdx +17 -0
  18. package/docs/getting-started/hono.mdx +17 -0
  19. package/docs/getting-started/nestjs.mdx +83 -5
  20. package/docs/getting-started/next.mdx +16 -1
  21. package/docs/getting-started/nitro.mdx +17 -0
  22. package/docs/getting-started/nuxt.mdx +17 -0
  23. package/docs/getting-started/sveltekit.mdx +17 -0
  24. package/docs/getting-started/vite.mdx +17 -0
  25. package/docs/how-it-works/encryption.mdx +39 -2
  26. package/docs/how-it-works/event-sourcing.mdx +19 -2
  27. package/docs/how-it-works/framework-integrations.mdx +68 -11
  28. package/docs/observability/index.mdx +1 -1
  29. package/docs/testing/index.mdx +1 -1
  30. package/docs/testing/server-based.mdx +59 -16
  31. package/package.json +10 -10
@@ -82,12 +82,49 @@ Without `--decrypt`, encrypted fields display as `🔒 Encrypted` placeholders.
82
82
 
83
83
  ## Custom World Implementations
84
84
 
85
- The core runtime encrypts data automatically when the `World` implementation provides a `getEncryptionKeyForRun()` method. This method receives the run ID and returns the raw encryption key bytes.
85
+ The core runtime encrypts data automatically when the `World` implementation provides a `getEncryptionKeyForRun()` method. The core runtime can call this method in two forms:
86
+
87
+ {/* @skip-typecheck - interface signature, not runnable code */}
88
+ ```typescript
89
+ getEncryptionKeyForRun?(run: WorkflowRun): Promise<Uint8Array | undefined>;
90
+ getEncryptionKeyForRun?(
91
+ runId: string,
92
+ context?: Record<string, unknown>
93
+ ): Promise<Uint8Array | undefined>;
94
+ ```
95
+
96
+ Use `getEncryptionKeyForRun(run)` when the run entity already exists. Use `getEncryptionKeyForRun(runId, context?)` in runtime paths like `start()` where the run has not been created yet but the world may still need context such as `deploymentId`.
86
97
 
87
98
  To add encryption support to a custom `World`:
88
99
 
89
- 1. Implement `getEncryptionKeyForRun(runId: string)` on your `World` class
100
+ 1. Implement `getEncryptionKeyForRun()` on your `World` class, handling both call shapes
90
101
  2. Return the raw 32-byte key as a `Uint8Array` — the core runtime uses it for AES-256-GCM operations
91
102
  3. Ensure the same key is returned for the same run ID across invocations (for decryption during replay)
92
103
 
104
+ ```typescript
105
+ import type { WorkflowRun, World } from "@workflow/world";
106
+
107
+ export const getEncryptionKeyForRun: World["getEncryptionKeyForRun"] = async (
108
+ run,
109
+ context
110
+ ) => {
111
+ const runId = typeof run === "string" ? run : run.runId;
112
+ const deploymentId =
113
+ typeof run === "string"
114
+ ? (context?.deploymentId as string | undefined)
115
+ : run.deploymentId;
116
+
117
+ return await lookupRunKey(runId, deploymentId);
118
+ };
119
+
120
+ async function lookupRunKey(
121
+ runId: string,
122
+ deploymentId?: string
123
+ ): Promise<Uint8Array | undefined> {
124
+ // Look up or derive the encryption key for this run
125
+ // Return undefined to skip encryption
126
+ return new Uint8Array(32);
127
+ }
128
+ ```
129
+
93
130
  The [Vercel World](/docs/deploying/world/vercel-world) implementation uses HKDF derivation from a deployment-scoped key, but any consistent key management scheme will work.
@@ -127,7 +127,7 @@ flowchart TD
127
127
 
128
128
  Unlike other entities, hooks don't have a `status` field—the states above are conceptual. An "active" hook is one that exists in storage, while "disposed" means the hook has been deleted. When a `hook_disposed` event is created, the hook record is removed rather than updated.
129
129
 
130
- While a hook is active, its token is reserved and cannot be used by other workflows. If a workflow attempts to create a hook with a token that is already in use by another active hook, a `hook_conflict` event is recorded instead of `hook_created`. This causes the hook's promise to reject with a `WorkflowRuntimeError`, failing the workflow gracefully. See the [hook-conflict error](/docs/errors/hook-conflict) documentation for more details.
130
+ While a hook is active, its token is reserved and cannot be used by other workflows. If a workflow attempts to create a hook with a token that is already in use by another active hook, a `hook_conflict` event is recorded instead of `hook_created`. This causes the hook's promise to reject with a `HookConflictError`, which you can detect with `HookConflictError.is(error)`. See the [hook-conflict error](/docs/errors/hook-conflict) documentation for more details.
131
131
 
132
132
  When a hook is disposed (either explicitly or when its workflow completes), the token is released and can be claimed by future workflows. Hooks are automatically disposed when a workflow reaches a terminal state (`completed`, `failed`, or `cancelled`). The `hook_disposed` event is only needed for explicit disposal before workflow completion.
133
133
 
@@ -188,7 +188,7 @@ Events are categorized by the entity type they affect. Each event contains metad
188
188
  | Event | Description |
189
189
  |-------|-------------|
190
190
  | `hook_created` | Creates a new hook in `active` state. Contains the hook token and optional metadata. |
191
- | `hook_conflict` | Records that hook creation failed because the token is already in use by another active hook. The hook is not created, and the workflow will fail with a `WorkflowRuntimeError` when the hook is awaited. |
191
+ | `hook_conflict` | Records that hook creation failed because the token is already in use by another active hook. The hook is not created, and awaiting the hook will reject with a `HookConflictError`. |
192
192
  | `hook_received` | Records that a payload was delivered to the hook. The hook remains `active` and can receive more payloads. |
193
193
  | `hook_disposed` | Deletes the hook from storage (conceptually transitioning to `disposed` state). The token is released for reuse by future workflows. |
194
194
 
@@ -241,6 +241,23 @@ This correlation enables:
241
241
  - Building timelines of entity lifecycle transitions
242
242
  - Debugging by tracing the complete history of any entity
243
243
 
244
+ ### Request ID Correlation
245
+
246
+ Some `World` implementations also attach a `requestId` to events for platform-log correlation. This is different from `correlationId`:
247
+
248
+ - `correlationId` links together events for the same entity lifecycle
249
+ - `requestId` tells you which inbound platform request created or updated the event
250
+
251
+ On Vercel, `requestId` is the platform request ID when available. Other worlds are not expected to provide a `requestId`.
252
+
253
+ ```json
254
+ {
255
+ "eventType": "step_started",
256
+ "correlationId": "step_01JQEXAMPLE1234567890",
257
+ "requestId": "iad1::abc123-1712345678901-xyz987"
258
+ }
259
+ ```
260
+
244
261
  ## Entity IDs
245
262
 
246
263
  All entities in the Workflow SDK use a consistent ID format: a 4-character prefix followed by an underscore and a [ULID](https://github.com/ulid/spec) (Universally Unique Lexicographically Sortable Identifier).
@@ -261,6 +261,35 @@ class MyFrameworkBuilder extends BaseBuilder {
261
261
 
262
262
  If your framework supports virtual server routes and dev mode watching, make sure to adapt accordingly. Please open a PR to the Workflow SDK if the base builder class is missing necessary functionality.
263
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";
271
+
272
+ class MyFrameworkBuilder extends BaseBuilder {
273
+ constructor(options: {
274
+ rootDir: string;
275
+ workspaceRoot?: string;
276
+ dev: boolean;
277
+ }) {
278
+ super({
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
+
264
293
  Hook into your framework's build:
265
294
 
266
295
  {/* @skip-typecheck: incomplete code sample */}
@@ -346,22 +375,50 @@ In the future, the Workflow SDK will emit more routes under the `.well-known/wor
346
375
 
347
376
  ## Security
348
377
 
349
- **How are these HTTP endpoints secured?**
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)"
387
+ {
388
+ "experimentalTriggers": [
389
+ {
390
+ "type": "queue/v2beta",
391
+ "topic": "__wkf_step_*",
392
+ "consumer": "default",
393
+ "retryAfterSeconds": 5,
394
+ "initialDelaySeconds": 0
395
+ }
396
+ ]
397
+ }
398
+ ```
399
+
400
+
401
+ Two queue topics are created per deployment:
350
402
 
351
- Security is handled by the **world abstraction** you're using:
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. The `STEP_QUEUE_TRIGGER` and `WORKFLOW_QUEUE_TRIGGER` constants are exported from `@workflow/builders` for this purpose:
409
+
410
+ ```typescript
411
+ import { STEP_QUEUE_TRIGGER, WORKFLOW_QUEUE_TRIGGER } from "@workflow/builders";
412
+ ```
352
413
 
353
- **Vercel (`@workflow/world-vercel`):**
354
414
 
355
- - Vercel Queue will support private invoke, making routes inaccessible from the public internet
356
- - Handlers receive only a message ID that must be retrieved from Vercel's backend
357
- - Impossible to craft custom payloads without valid queue-issued message IDs
415
+ ### Custom implementations
358
416
 
359
- **Custom implementations:**
417
+ For self-hosted or non-Vercel deployments, you are responsible for securing the handler endpoints:
360
418
 
361
- - Implement authentication via framework middleware
362
- - Use API keys, JWT validation, or other auth schemes
363
- - Network-level security (VPCs, private networks, firewall rules)
364
- - Rate limiting and request validation
419
+ - **Framework middleware** — Add authentication (API keys, JWT, OIDC) in front of the `/.well-known/workflow/v1/*` routes
420
+ - **Network-level security** — Deploy handlers behind a VPC, private network, or firewall rules so only your queue infrastructure can reach them
421
+ - **Rate limiting** Add request validation and rate limiting to prevent abuse
365
422
 
366
423
  Learn more about [building custom Worlds](/docs/deploying/building-a-world).
367
424
 
@@ -18,7 +18,7 @@ Workflow SDK provides powerful tools to inspect, monitor, and debug your workflo
18
18
  npx workflow
19
19
  ```
20
20
 
21
- The CLI comes pre-installed with the Workflow SDK and registers the `workflow` command. If the `workflow` package is not already installed, `npx workflow` will install it globally, or use the local installed version if available.
21
+ The CLI comes pre-installed with the Workflow SDK and registers the `workflow` command. If the `workflow` package is not already installed, `npx workflow` will download and run the CLI temporarily, or use the local installed version if available.
22
22
 
23
23
  Get started inspecting your local workflows:
24
24
 
@@ -371,7 +371,7 @@ Or use the CLI to inspect runs in the terminal:
371
371
  npx workflow inspect runs
372
372
 
373
373
  # Inspect a specific run
374
- npx workflow inspect runs <run-id>
374
+ npx workflow inspect run <run-id>
375
375
  ```
376
376
 
377
377
  The Web UI shows each step, its inputs and outputs, retry attempts, hook state, and timing. This is especially useful for diagnosing issues with hooks that were not resumed, steps that failed unexpectedly, or workflows that timed out.
@@ -48,8 +48,24 @@ import type { ChildProcess } from "node:child_process";
48
48
  let server: ChildProcess | null = null;
49
49
  const PORT = "4000";
50
50
 
51
+ function emitSetupLog(event: string, fields: Record<string, unknown> = {}) {
52
+ console.log(
53
+ JSON.stringify({
54
+ scope: "workflow-server-test",
55
+ event,
56
+ port: PORT,
57
+ ...fields,
58
+ })
59
+ );
60
+ }
61
+
51
62
  export async function setup() { // [!code highlight]
52
- console.log("Starting server for workflow execution...");
63
+ const stdout: string[] = [];
64
+ const stderr: string[] = [];
65
+
66
+ emitSetupLog("server_starting", {
67
+ command: `npx nitro dev --port ${PORT}`,
68
+ });
53
69
 
54
70
  server = spawn("npx", ["nitro", "dev", "--port", PORT], {
55
71
  stdio: "pipe",
@@ -57,13 +73,14 @@ export async function setup() { // [!code highlight]
57
73
  env: process.env,
58
74
  });
59
75
 
60
- // Wait for the server to be ready
61
76
  const ready = await new Promise<boolean>((resolve) => {
62
77
  const timeout = setTimeout(() => resolve(false), 15_000);
63
78
 
64
79
  server?.stdout?.on("data", (data) => {
65
80
  const output = data.toString();
66
- console.log("[server]", output);
81
+ stdout.push(output);
82
+ emitSetupLog("server_stdout", { message: output.trim() });
83
+
67
84
  if (output.includes("listening") || output.includes("ready")) {
68
85
  clearTimeout(timeout);
69
86
  resolve(true);
@@ -71,40 +88,66 @@ export async function setup() { // [!code highlight]
71
88
  });
72
89
 
73
90
  server?.stderr?.on("data", (data) => {
74
- console.error("[server]", data.toString());
91
+ const output = data.toString();
92
+ stderr.push(output);
93
+ emitSetupLog("server_stderr", { message: output.trim() });
75
94
  });
76
95
 
77
96
  server?.on("error", (error) => {
78
- console.error("Failed to start server:", error);
97
+ emitSetupLog("server_process_error", {
98
+ name: error.name,
99
+ message: error.message,
100
+ });
79
101
  clearTimeout(timeout);
80
102
  resolve(false);
81
103
  });
104
+
105
+ server?.on("exit", (code, signal) => {
106
+ emitSetupLog("server_exit", { code, signal });
107
+ });
82
108
  });
83
109
 
84
110
  if (!ready) {
85
- throw new Error("Server failed to start within 15 seconds");
111
+ const recentStdout = stdout.join("").trim().slice(-2000);
112
+ const recentStderr = stderr.join("").trim().slice(-2000);
113
+
114
+ throw new Error(
115
+ [
116
+ "Server failed to start within 15 seconds.",
117
+ `Command: npx nitro dev --port ${PORT}`,
118
+ `WORKFLOW_LOCAL_BASE_URL: http://localhost:${PORT}`,
119
+ `Recent stdout:\n${recentStdout || "(empty)"}`,
120
+ `Recent stderr:\n${recentStderr || "(empty)"}`,
121
+ ].join("\n\n")
122
+ );
86
123
  }
87
124
 
88
- await delay(2_000); // Allow full initialization
125
+ await delay(2_000);
89
126
 
90
- // Point the workflow runtime at the local server
91
127
  process.env.WORKFLOW_LOCAL_BASE_URL = `http://localhost:${PORT}`; // [!code highlight]
92
128
 
93
- console.log("Server ready for workflow execution");
129
+ emitSetupLog("server_ready", {
130
+ baseUrl: process.env.WORKFLOW_LOCAL_BASE_URL,
131
+ });
94
132
  }
95
133
 
96
134
  export async function teardown() { // [!code highlight]
97
- if (server) {
98
- console.log("Stopping server...");
99
- server.kill("SIGTERM");
100
- await delay(1_000);
101
- if (!server.killed) {
102
- server.kill("SIGKILL");
103
- }
135
+ if (!server) return;
136
+
137
+ emitSetupLog("server_stopping");
138
+
139
+ server.kill("SIGTERM");
140
+ await delay(1_000);
141
+
142
+ if (!server.killed) {
143
+ emitSetupLog("server_force_kill");
144
+ server.kill("SIGKILL");
104
145
  }
105
146
  }
106
147
  ```
107
148
 
149
+ These JSON log lines are intentional. They give CI jobs, local tooling, and agents stable events to watch for (`server_starting`, `server_stdout`, `server_stderr`, `server_ready`, `server_exit`), and the thrown timeout error includes the command, expected `WORKFLOW_LOCAL_BASE_URL`, and buffered stdout/stderr so a failed setup is actionable without interactive debugging.
150
+
108
151
  The setup script sets `WORKFLOW_LOCAL_BASE_URL` so the workflow runtime sends step execution requests to the running server.
109
152
 
110
153
  <Callout type="info">
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "workflow",
3
- "version": "4.2.0-beta.76",
3
+ "version": "4.2.0-beta.77",
4
4
  "description": "Workflow SDK - Build durable, resilient, and observable workflows",
5
5
  "main": "dist/typescript-plugin.cjs",
6
6
  "type": "module",
@@ -57,18 +57,18 @@
57
57
  },
58
58
  "dependencies": {
59
59
  "ms": "2.1.3",
60
- "@workflow/astro": "4.0.0-beta.50",
61
- "@workflow/cli": "4.2.0-beta.76",
62
- "@workflow/core": "4.2.0-beta.76",
60
+ "@workflow/astro": "4.0.0-beta.51",
61
+ "@workflow/cli": "4.2.0-beta.77",
62
+ "@workflow/core": "4.2.0-beta.77",
63
63
  "@workflow/errors": "4.1.0-beta.20",
64
64
  "@workflow/typescript-plugin": "4.0.1-beta.5",
65
65
  "@workflow/utils": "4.1.0-beta.13",
66
- "@workflow/next": "4.0.1-beta.72",
67
- "@workflow/nest": "0.0.0-beta.25",
68
- "@workflow/nitro": "4.0.1-beta.71",
69
- "@workflow/nuxt": "4.0.1-beta.60",
70
- "@workflow/sveltekit": "4.0.0-beta.65",
71
- "@workflow/rollup": "4.0.0-beta.33"
66
+ "@workflow/next": "4.0.1-beta.73",
67
+ "@workflow/nest": "0.0.0-beta.26",
68
+ "@workflow/nitro": "4.0.1-beta.72",
69
+ "@workflow/nuxt": "4.0.1-beta.61",
70
+ "@workflow/sveltekit": "4.0.0-beta.66",
71
+ "@workflow/rollup": "4.0.0-beta.34"
72
72
  },
73
73
  "devDependencies": {
74
74
  "@types/ms": "2.1.0",