workflow 5.0.0-beta.57 → 5.0.0-beta.58
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.d.ts +1 -1
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +1 -1
- package/docs/advanced/dynamic-workflows.mdx +224 -0
- package/docs/api-reference/workflow-api/start.mdx +48 -0
- package/docs/api-reference/workflow-runtime/health-check.mdx +1 -0
- package/docs/configuration/runtime-tuning.mdx +10 -1
- package/docs/configuration/worlds.mdx +2 -2
- package/docs/foundations/serialization.mdx +1 -0
- package/docs/meta.json +1 -0
- package/docs/whats-new.mdx +1 -0
- package/docs/worlds/building-a-world.mdx +2 -1
- package/docs/worlds/upgrading-to-v5.mdx +1 -1
- package/docs/worlds/vercel.mdx +3 -3
- package/package.json +11 -11
package/dist/api.d.ts
CHANGED
|
@@ -3,5 +3,5 @@ export type { CancelRunOptions, Event, StopSleepOptions, StopSleepResult, Workfl
|
|
|
3
3
|
export { type RunCompletedHookParams, type RunFailedHookParams, registerLifecycleHooks, type WorkflowLifecycleHooks, } from '@workflow/core/runtime/lifecycle-hooks';
|
|
4
4
|
export { getHookByToken, type Hook, type ResumedHook, resumeHook, resumeWebhook, } from '@workflow/core/runtime/resume-hook';
|
|
5
5
|
export { getRun, Run, type WorkflowReadableStream, type WorkflowReadableStreamOptions, type WorkflowRunWritableStreamOptions, } from '@workflow/core/runtime/run';
|
|
6
|
-
export { type StartOptions, start, } from '@workflow/core/runtime/start';
|
|
6
|
+
export { type DynamicStartOptions, type DynamicWorkflowOptions, type DynamicWorkflowStepReference, type StartOptions, start, } from '@workflow/core/runtime/start';
|
|
7
7
|
//# sourceMappingURL=api.d.ts.map
|
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,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"}
|
|
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,mBAAmB,EACxB,KAAK,sBAAsB,EAC3B,KAAK,4BAA4B,EACjC,KAAK,YAAY,EACjB,KAAK,GACN,MAAM,8BAA8B,CAAC"}
|
package/dist/api.js
CHANGED
|
@@ -10,4 +10,4 @@ export { registerLifecycleHooks, } from '@workflow/core/runtime/lifecycle-hooks'
|
|
|
10
10
|
export { getHookByToken, resumeHook, resumeWebhook, } from '@workflow/core/runtime/resume-hook';
|
|
11
11
|
export { getRun, Run, } from '@workflow/core/runtime/run';
|
|
12
12
|
export { start, } from '@workflow/core/runtime/start';
|
|
13
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
13
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYXBpLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vc3JjL2FwaS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxxRUFBcUU7QUFDckUseUVBQXlFO0FBQ3pFLDJFQUEyRTtBQUMzRSw0RUFBNEU7QUFDNUUsNkVBQTZFO0FBQzdFLG1CQUFtQjtBQUNuQix5RUFBeUU7QUFDekUsT0FBTyxtQ0FBbUMsQ0FBQztBQVMzQyxPQUFPLEVBR0wsc0JBQXNCLEdBRXZCLE1BQU0sd0NBQXdDLENBQUM7QUFDaEQsT0FBTyxFQUNMLGNBQWMsRUFHZCxVQUFVLEVBQ1YsYUFBYSxHQUNkLE1BQU0sb0NBQW9DLENBQUM7QUFDNUMsT0FBTyxFQUNMLE1BQU0sRUFDTixHQUFHLEdBSUosTUFBTSw0QkFBNEIsQ0FBQztBQUNwQyxPQUFPLEVBS0wsS0FBSyxHQUNOLE1BQU0sOEJBQThCLENBQUMiLCJzb3VyY2VzQ29udGVudCI6WyIvLyBTaWRlLWVmZmVjdCBpbXBvcnQ6IGVuc3VyZSBgd29ybGQudHNgIGlzIGxvYWRlZCBzbyBpdHMgbW9kdWxlLWxvYWRcbi8vIGBnbG9iYWxUaGlzW0dldFdvcmxkRm5LZXldID8/PSBnZXRXb3JsZGAgcmVnaXN0cmF0aW9uIGZpcmVzIGJlZm9yZSBhbnlcbi8vIGhvc3Qgcm91dGUgcmVhY2hlcyBgZ2V0V29ybGRMYXp5KClgLiBXaXRob3V0IHRoaXMsIHdlYnBhY2svdHVyYm9wYWNrIGNhblxuLy8gdHJlZS1zaGFrZSBgd29ybGQudHNgIG91dCBvZiByb3V0ZXMgdGhhdCBvbmx5IHVzZSBgc3RhcnRgLiBSZXNvbHZlZCB0byBhblxuLy8gZW1wdHkgc3R1YiB2aWEgdGhlIGB3b3JrZmxvd2AgZXhwb3J0IGNvbmRpdGlvbiBpbiBWTS9zdGVwIGJ1bmRsZXMsIHNvIHRoaXNcbi8vIHN0YXlzIGhvc3Qtb25seS5cbi8vIFNlZSBgQHdvcmtmbG93L2NvcmUvc3JjL3J1bnRpbWUvd29ybGQtaW5pdC50c2AgZm9yIHRoZSBmdWxsIHJhdGlvbmFsZS5cbmltcG9ydCAnQHdvcmtmbG93L2NvcmUvcnVudGltZS93b3JsZC1pbml0JztcblxuZXhwb3J0IHR5cGUge1xuICBDYW5jZWxSdW5PcHRpb25zLFxuICBFdmVudCxcbiAgU3RvcFNsZWVwT3B0aW9ucyxcbiAgU3RvcFNsZWVwUmVzdWx0LFxuICBXb3JrZmxvd1J1bixcbn0gZnJvbSAnQHdvcmtmbG93L2NvcmUvcnVudGltZSc7XG5leHBvcnQge1xuICB0eXBlIFJ1bkNvbXBsZXRlZEhvb2tQYXJhbXMsXG4gIHR5cGUgUnVuRmFpbGVkSG9va1BhcmFtcyxcbiAgcmVnaXN0ZXJMaWZlY3ljbGVIb29rcyxcbiAgdHlwZSBXb3JrZmxvd0xpZmVjeWNsZUhvb2tzLFxufSBmcm9tICdAd29ya2Zsb3cvY29yZS9ydW50aW1lL2xpZmVjeWNsZS1ob29rcyc7XG5leHBvcnQge1xuICBnZXRIb29rQnlUb2tlbixcbiAgdHlwZSBIb29rLFxuICB0eXBlIFJlc3VtZWRIb29rLFxuICByZXN1bWVIb29rLFxuICByZXN1bWVXZWJob29rLFxufSBmcm9tICdAd29ya2Zsb3cvY29yZS9ydW50aW1lL3Jlc3VtZS1ob29rJztcbmV4cG9ydCB7XG4gIGdldFJ1bixcbiAgUnVuLFxuICB0eXBlIFdvcmtmbG93UmVhZGFibGVTdHJlYW0sXG4gIHR5cGUgV29ya2Zsb3dSZWFkYWJsZVN0cmVhbU9wdGlvbnMsXG4gIHR5cGUgV29ya2Zsb3dSdW5Xcml0YWJsZVN0cmVhbU9wdGlvbnMsXG59IGZyb20gJ0B3b3JrZmxvdy9jb3JlL3J1bnRpbWUvcnVuJztcbmV4cG9ydCB7XG4gIHR5cGUgRHluYW1pY1N0YXJ0T3B0aW9ucyxcbiAgdHlwZSBEeW5hbWljV29ya2Zsb3dPcHRpb25zLFxuICB0eXBlIER5bmFtaWNXb3JrZmxvd1N0ZXBSZWZlcmVuY2UsXG4gIHR5cGUgU3RhcnRPcHRpb25zLFxuICBzdGFydCxcbn0gZnJvbSAnQHdvcmtmbG93L2NvcmUvcnVudGltZS9zdGFydCc7XG4iXX0=
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Dynamic Workflows
|
|
3
|
+
description: Start a workflow run from source code that was not part of your build.
|
|
4
|
+
type: conceptual
|
|
5
|
+
summary: Pass workflow source to start() to run orchestration whose shape is only known after deployment.
|
|
6
|
+
prerequisites:
|
|
7
|
+
- /docs/foundations/starting-workflows
|
|
8
|
+
- /docs/how-it-works/code-transform
|
|
9
|
+
related:
|
|
10
|
+
- /docs/api-reference/workflow-api/start
|
|
11
|
+
- /docs/how-it-works/encryption
|
|
12
|
+
- /docs/configuration/runtime-tuning
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
<Callout type="warning">
|
|
16
|
+
Dynamic workflows are **experimental** and **off by default**. The API may change without a major version bump. A deployment must opt in with `WORKFLOW_EXPERIMENTAL_DYNAMIC_WORKFLOWS=1`, dynamic runs can only start on the current deployment, and the World must support dynamic-source storage. See [Enabling dynamic workflows](#enabling-dynamic-workflows) and [World support](#world-support).
|
|
17
|
+
</Callout>
|
|
18
|
+
|
|
19
|
+
<Callout type="error">
|
|
20
|
+
Dynamic source runs with the **full privileges of your deployment's functions**. It can read every environment variable, use the network and the filesystem, and call any step in the deployment. `experimental_dynamic.steps` is not a security boundary. Only pass source you would merge into your codebase. See [Security](#security).
|
|
21
|
+
</Callout>
|
|
22
|
+
|
|
23
|
+
Normally a workflow function is compiled into your build: the [code transform](/docs/how-it-works/code-transform) rewrites every `"use workflow"` function, the build bundles them, and `start()` names one by importing it.
|
|
24
|
+
|
|
25
|
+
A dynamic workflow skips that. You hand `start()` a string of JavaScript, and it runs — no build, no deploy:
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
import { start } from 'workflow/api';
|
|
29
|
+
import { fetchUser, sendEmail } from './steps';
|
|
30
|
+
|
|
31
|
+
const run = await start(
|
|
32
|
+
`
|
|
33
|
+
async function workflow(input) {
|
|
34
|
+
"use workflow";
|
|
35
|
+
|
|
36
|
+
const user = await steps.fetchUser(input.userId);
|
|
37
|
+
await steps.sendEmail(user.email);
|
|
38
|
+
|
|
39
|
+
return { ok: true };
|
|
40
|
+
}
|
|
41
|
+
`,
|
|
42
|
+
[{ userId: 'user_123' }],
|
|
43
|
+
{
|
|
44
|
+
experimental_dynamic: {
|
|
45
|
+
steps: { fetchUser, sendEmail },
|
|
46
|
+
},
|
|
47
|
+
}
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
console.log(await run.status); // 'running'
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Only the *orchestration* is dynamic. Every step the source calls was deployed with your app, and `experimental_dynamic.steps` names the ones it calls by alias. That map does not stop source from reaching other steps; see [Security](#security). There is no way to define a new step from source.
|
|
54
|
+
|
|
55
|
+
## When to use this
|
|
56
|
+
|
|
57
|
+
Reach for dynamic workflows when the **shape** of the orchestration is only known after you deploy, and the source comes from code you trust as much as your own:
|
|
58
|
+
|
|
59
|
+
- **Orchestration your application assembles** from reviewed templates, over a fixed set of deployed steps.
|
|
60
|
+
- **Experiments** — try a new composition of existing steps without shipping a build.
|
|
61
|
+
|
|
62
|
+
Dynamic workflows are not a way to run code written by your end users or generated by a model from their input. That source would run with your deployment's privileges; see [Security](#security).
|
|
63
|
+
|
|
64
|
+
If your workflows are known at build time, use a normal workflow function. It has better types, better errors, no source validation, and no size limits.
|
|
65
|
+
|
|
66
|
+
## Enabling dynamic workflows
|
|
67
|
+
|
|
68
|
+
Dynamic workflows are off unless the deployment sets:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
WORKFLOW_EXPERIMENTAL_DYNAMIC_WORKFLOWS=1
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Only `1` or `true` (case-insensitive) enables them; any other value, or no value, leaves them off. The runtime reads the variable where workflows execute, when it needs it, so set it on the deployment or dev server rather than at build time. It controls three things:
|
|
75
|
+
|
|
76
|
+
- **Starting.** `start()` with source throws before it contacts the World, creates a run, or enqueues anything unless this process has opted in.
|
|
77
|
+
- **Delivery.** When a dynamic run reaches a deployment that has not opted in, the runtime does not execute its stored code. It fails the run with a `RUNTIME_ERROR` rather than retrying it.
|
|
78
|
+
- **Health check.** A deployment advertises dynamic support in its [health check](/docs/api-reference/workflow-runtime/health-check) only when it has opted in.
|
|
79
|
+
|
|
80
|
+
### Same deployment only
|
|
81
|
+
|
|
82
|
+
A dynamic run must execute on the deployment that started it. `start()` rejects a dynamic start whose target differs from the current deployment. That includes an explicit `deploymentId` for another deployment, `deploymentId: 'latest'` when it resolves to a different deployment, and any concrete target when the current deployment cannot be determined. The rejection happens before any capability check, key lookup, upload, run creation, or queue message.
|
|
83
|
+
|
|
84
|
+
## What the source can use
|
|
85
|
+
|
|
86
|
+
Dynamic source has no imports. Instead, the generated code predefines a small runtime surface:
|
|
87
|
+
|
|
88
|
+
| Binding | What it is |
|
|
89
|
+
| --- | --- |
|
|
90
|
+
| `steps` | Frozen object of the aliases you passed in `experimental_dynamic.steps`. Calling one dispatches that registered step. |
|
|
91
|
+
| `sleep` | The [durable sleep](/docs/api-reference/workflow/sleep) primitive. |
|
|
92
|
+
| `createHook` | The [hook](/docs/foundations/hooks) primitive, for waiting on an external signal. |
|
|
93
|
+
|
|
94
|
+
The source also runs inside the normal deterministic workflow VM, so the usual [workflow globals](/docs/api-reference/workflow-globals) — `Date`, `Math.random`, `crypto`, `URL`, `TextEncoder`, `structuredClone`, and the rest — are available with the same determinism guarantees as a static workflow.
|
|
95
|
+
|
|
96
|
+
Dynamic source exposes only the small set of primitives injected by its generated wrapper. `createWebhook()` also needs the static workflow module's URL and metadata helper, and `getWritable()` needs its workflow-stream helper, so neither is currently injected into dynamic source. Use `createHook()` with server-side `resumeHook()`, and perform streaming through registered steps or a statically compiled workflow.
|
|
97
|
+
|
|
98
|
+
Here is a longer example using a timer and a hook to wait for an approval:
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
import { start } from 'workflow/api';
|
|
102
|
+
import { sendEmail } from './steps';
|
|
103
|
+
|
|
104
|
+
const run = await start(
|
|
105
|
+
`
|
|
106
|
+
async function workflow(input) {
|
|
107
|
+
"use workflow";
|
|
108
|
+
|
|
109
|
+
await sleep("15m");
|
|
110
|
+
|
|
111
|
+
const approval = createHook({ token: input.approvalToken });
|
|
112
|
+
const result = await Promise.race([
|
|
113
|
+
approval,
|
|
114
|
+
sleep("1d").then(() => ({ approved: false, timedOut: true })),
|
|
115
|
+
]);
|
|
116
|
+
|
|
117
|
+
if (result.approved) {
|
|
118
|
+
await steps.sendEmail(input.email);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return result;
|
|
122
|
+
}
|
|
123
|
+
`,
|
|
124
|
+
[{
|
|
125
|
+
userId: 'user_123',
|
|
126
|
+
email: 'ada@example.com',
|
|
127
|
+
approvalToken: 'approval-req_01J...',
|
|
128
|
+
}],
|
|
129
|
+
{
|
|
130
|
+
experimental_dynamic: {
|
|
131
|
+
steps: { sendEmail },
|
|
132
|
+
},
|
|
133
|
+
}
|
|
134
|
+
);
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Supply a unique, deterministic approval token from the caller. The workflow must recreate the same token during replay, while the external service needs that token to call `resumeHook()`; do not use a tenant or user ID alone when concurrent runs can overlap.
|
|
138
|
+
|
|
139
|
+
## Rules for the source
|
|
140
|
+
|
|
141
|
+
`start()` validates the source before it writes anything, so a definition that could never run fails at the call site rather than on a queue delivery:
|
|
142
|
+
|
|
143
|
+
- It must declare `async function workflow(...)`. Pass `experimental_dynamic.exportName` to use a different name; export names may contain letters, digits, and `_`, and cannot start with a digit.
|
|
144
|
+
- The function's first statement must be the `"use workflow"` directive.
|
|
145
|
+
- No `import` or `export`. Reach steps through `steps`, not through modules.
|
|
146
|
+
- JavaScript only — no TypeScript syntax, no npm dependencies, no bundling.
|
|
147
|
+
- No inline `"use step"` functions. Steps come from `experimental_dynamic.steps`.
|
|
148
|
+
- At most 128 KB of source.
|
|
149
|
+
- On Vercel, the run's execution context is limited to 2,048 bytes of JSON, and the `dynamicWorkflow` metadata below counts against it. That leaves room for roughly 30 step aliases, depending on how long the aliases and step IDs are. A start that exceeds it fails before anything is written.
|
|
150
|
+
|
|
151
|
+
Everything a static workflow must obey still applies: the body has to be [deterministic](/docs/foundations/workflows-and-steps), and any side effect belongs in a step.
|
|
152
|
+
|
|
153
|
+
## Workflow IDs
|
|
154
|
+
|
|
155
|
+
You do not choose the workflow ID. It is derived from the source and its step bindings:
|
|
156
|
+
|
|
157
|
+
```
|
|
158
|
+
workflow//dynamic/<source-hash>//<exportName>
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Two consequences worth knowing:
|
|
162
|
+
|
|
163
|
+
- **The same definition always gets the same ID.** Runs of one generated workflow group together in [observability](/docs/observability) and share a queue topic, even across processes.
|
|
164
|
+
- **A caller cannot claim an ID.** Because the hash covers the source *and* the step bindings, arbitrary source cannot be made to run under a static workflow's name — or under another definition's.
|
|
165
|
+
|
|
166
|
+
Changing the source, or pointing an alias at a different step, produces a different workflow.
|
|
167
|
+
|
|
168
|
+
## How the code is stored
|
|
169
|
+
|
|
170
|
+
A dynamic run's workflow function is not in your deployment's bundle, so the run carries its own compiled workflow code — and replaying the run means replaying *that* code, not whatever your deployment contains now.
|
|
171
|
+
|
|
172
|
+
That code uses the same serialization path as workflow inputs. It is compressed when the run protocol supports compression and compression is worthwhile, and encrypted when the World supplies run key material (see [Encryption](/docs/how-it-works/encryption)). Vercel's supported configuration provides encrypted storage; the Local and Postgres Worlds store it in plaintext. Retention and deletion apply whether the stored bytes are plaintext or ciphertext.
|
|
173
|
+
|
|
174
|
+
When a run has key material, or was started with encryption, a delivery only executes code stored in the run's symmetric `encr` envelope. It refuses plaintext and sealed (`encp`) payloads and fails the run. Encryption keeps the code confidential; it does not prove who wrote it. See [Security](#security).
|
|
175
|
+
|
|
176
|
+
On Vercel, durable workflow code storage is ref-backed on the run. The definition's size changes only how those bytes reach the backend:
|
|
177
|
+
|
|
178
|
+
- **Small definitions** (the overwhelming majority) ride inline in the `run_created` request frame. The backend materializes those bytes into the run's ref-backed storage, with no upload request from `start()`.
|
|
179
|
+
- **Larger definitions** are uploaded first, and `run_created` carries the resulting reference. This costs one extra request at `start()`.
|
|
180
|
+
|
|
181
|
+
Both paths are transparent — there is nothing to configure. Here, “inline” describes request transport, not a second durable storage shape.
|
|
182
|
+
|
|
183
|
+
Alongside the serialized code, the run records small plaintext metadata on `executionContext.dynamicWorkflow`: the source hash, the export name, and the alias-to-step-ID map. That is what lets a run be identified as dynamic without decoding the source. It is plaintext even when the code is encrypted, so anyone who can read the run can see which step IDs it was given and the aliases they were given under.
|
|
184
|
+
|
|
185
|
+
## World support
|
|
186
|
+
|
|
187
|
+
Dynamic workflows need a World that can store the run's workflow code.
|
|
188
|
+
|
|
189
|
+
| World | Support |
|
|
190
|
+
| --- | --- |
|
|
191
|
+
| [Vercel](/worlds/vercel) | Encrypted, ref-backed storage (small definitions transported inline; large definitions uploaded first). If dynamic-source storage is not enabled for the project, the backend rejects the run's creation and `start()` throws. |
|
|
192
|
+
| [Local](/worlds/local) | Stored in plaintext on the run record in the local filesystem store. |
|
|
193
|
+
| [Postgres](/worlds/postgres) | Stored in plaintext on the run row. |
|
|
194
|
+
| Others | Supported when the World declares [`capabilities.dynamicWorkflowCode`](/worlds/building-a-world). |
|
|
195
|
+
|
|
196
|
+
After the opt-in and same-deployment checks, `start()` fails a dynamic start on a World that does not declare `capabilities.dynamicWorkflowCode`. On Vercel, `start()` then validates the final execution context against the 2,048-byte limit. All of this happens before serializing or uploading code, creating an event, or publishing a queue message.
|
|
197
|
+
|
|
198
|
+
## Security
|
|
199
|
+
|
|
200
|
+
<Callout type="warning">
|
|
201
|
+
Dynamic source is **trusted application code** with the full privileges of your deployment's functions. The workflow VM is a determinism sandbox, not a security sandbox. Code in it can reach the host process: it can read every environment variable, use the network and the filesystem, and call any step registered in the deployment with any arguments.
|
|
202
|
+
</Callout>
|
|
203
|
+
|
|
204
|
+
- **`steps` is not a boundary.** The `steps` object contains only the aliases you passed and is frozen, so ordinary code that calls `steps.somethingElse()` fails the run instead of dispatching a step it was not given. Code that is trying to reach other steps, or the host, can.
|
|
205
|
+
- **Only start source you would merge.** Do not build source from end-user input, and do not run model output generated from untrusted input. Either one gives whoever controls that input your deployment's privileges.
|
|
206
|
+
- **Opting in is a deployment decision.** A deployment that sets `WORKFLOW_EXPERIMENTAL_DYNAMIC_WORKFLOWS` executes stored code for any dynamic run it receives. Anyone who can start a workflow on it can run code with its privileges.
|
|
207
|
+
- **Encryption gives confidentiality only.** Where the World encrypts stored code, it cannot be read at rest without the run's key, and a delivery refuses code that is not encrypted with that key. Anyone who can obtain the run's key can still write valid code, so encryption does not replace the opt-in.
|
|
208
|
+
- **Plaintext Worlds turn storage write access into code execution.** The Local and Postgres Worlds store the code in plaintext. On an opted-in deployment, anyone who can write to the Postgres database or the local data directory can make every worker execute code of their choosing.
|
|
209
|
+
- **The step map is readable.** `executionContext.dynamicWorkflow.steps` stores the alias-to-step-ID map in plaintext, so anyone with read access to the run sees the step IDs the source was given.
|
|
210
|
+
|
|
211
|
+
Treat dynamic source the way you would treat code in a pull request: written or reviewed by someone you trust with the deployment.
|
|
212
|
+
|
|
213
|
+
## Limitations
|
|
214
|
+
|
|
215
|
+
- Experimental — the API may change without a major version bump.
|
|
216
|
+
- Off unless the deployment sets `WORKFLOW_EXPERIMENTAL_DYNAMIC_WORKFLOWS=1`.
|
|
217
|
+
- Same-deployment starts only.
|
|
218
|
+
- JavaScript only. No TypeScript syntax, npm dependencies, or bundling.
|
|
219
|
+
- Steps must already be registered in the deployment; no runtime step registration.
|
|
220
|
+
- No inline `"use step"` functions, `createWebhook`, or `getWritable`.
|
|
221
|
+
- No caller-provided workflow IDs.
|
|
222
|
+
- Parser-based validation checks JavaScript syntax and the required source/wrapper shape without executing it. It does not validate behavior, determinism, or intent.
|
|
223
|
+
- On Vercel, roughly 30 step aliases fit the 2,048-byte execution-context limit.
|
|
224
|
+
- Requires a World with dynamic-source storage.
|
|
@@ -7,6 +7,7 @@ prerequisites:
|
|
|
7
7
|
- /docs/foundations/starting-workflows
|
|
8
8
|
related:
|
|
9
9
|
- /docs/foundations/idempotency
|
|
10
|
+
- /docs/advanced/dynamic-workflows
|
|
10
11
|
---
|
|
11
12
|
|
|
12
13
|
Start/enqueue a new workflow run.
|
|
@@ -153,3 +154,50 @@ The returned `Run` object is fully functional inside a workflow. Each property a
|
|
|
153
154
|
<Callout type="warn">
|
|
154
155
|
`returnValue` polls the child run every second and holds the polling step's worker slot open for as long as the child takes to finish. For long-running children, spawn without awaiting `returnValue` and have the child resume a [hook](/docs/foundations/hooks) when it completes. See the [`startAndWait()` pattern](/cookbook/advanced/child-workflows).
|
|
155
156
|
</Callout>
|
|
157
|
+
|
|
158
|
+
### Dynamic Workflow Source
|
|
159
|
+
|
|
160
|
+
<Callout type="warning">
|
|
161
|
+
Experimental and off by default. The API may change without a major version bump. The deployment must set `WORKFLOW_EXPERIMENTAL_DYNAMIC_WORKFLOWS=1`, and dynamic runs can only start on the current deployment.
|
|
162
|
+
</Callout>
|
|
163
|
+
|
|
164
|
+
`start()` also accepts a string of workflow **source** instead of an imported function, for orchestration whose shape is only known after you deploy. The source is compiled and stored with the run through the same serialization path as other run payloads, including encryption when the World supplies run key material.
|
|
165
|
+
|
|
166
|
+
Dynamic source runs with the full privileges of your deployment's functions: it can read environment variables, use the network and filesystem, and call any step in the deployment. Only pass source you trust as much as your own code.
|
|
167
|
+
|
|
168
|
+
`experimental_dynamic.steps` maps the aliases the source may call to steps that are already registered in your deployment. It keeps ordinary source from calling a step by a name it was not given, but it is not a security boundary. There is no way to define a new step from source.
|
|
169
|
+
|
|
170
|
+
```typescript
|
|
171
|
+
import { start } from "workflow/api";
|
|
172
|
+
import { fetchUser, sendEmail } from "./steps";
|
|
173
|
+
|
|
174
|
+
const run = await start(
|
|
175
|
+
`
|
|
176
|
+
async function workflow(input) {
|
|
177
|
+
"use workflow";
|
|
178
|
+
|
|
179
|
+
const user = await steps.fetchUser(input.userId);
|
|
180
|
+
await steps.sendEmail(user.email);
|
|
181
|
+
|
|
182
|
+
return { ok: true };
|
|
183
|
+
}
|
|
184
|
+
`,
|
|
185
|
+
[{ userId: "user_123" }],
|
|
186
|
+
{
|
|
187
|
+
experimental_dynamic: { // [!code highlight]
|
|
188
|
+
steps: { fetchUser, sendEmail }, // [!code highlight]
|
|
189
|
+
}, // [!code highlight]
|
|
190
|
+
}
|
|
191
|
+
);
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
| Option | Type | Description |
|
|
195
|
+
| --- | --- | --- |
|
|
196
|
+
| `experimental_dynamic.steps` | `Record<string, StepFunction \| { stepId: string }>` | Required. Registered steps the source may call, keyed by the alias it calls them under. |
|
|
197
|
+
| `experimental_dynamic.exportName` | `string` | Name of the async workflow function in the source. Use letters, digits, and `_`, not starting with a digit. Defaults to `"workflow"`. |
|
|
198
|
+
|
|
199
|
+
The return type is `Run<unknown>`: the source's shape is only known to whatever generated it, so there is nothing to infer. The workflow ID is derived from the source and its step bindings — it cannot be supplied.
|
|
200
|
+
|
|
201
|
+
A dynamic start throws a `WorkflowRuntimeError`, before anything is written, when the deployment has not opted in, when `deploymentId` targets another deployment (including `'latest'` resolving to one), or when the World's backend does not support dynamic-source storage.
|
|
202
|
+
|
|
203
|
+
See [Dynamic Workflows](/docs/advanced/dynamic-workflows) for the opt-in, the source rules and limits, the predefined runtime bindings (`steps`, `sleep`, `createHook`), how the code is stored, and the security model.
|
|
@@ -48,3 +48,4 @@ Returns a `Promise<HealthCheckResult>`:
|
|
|
48
48
|
| `latencyMs` | `number \| undefined` | Round-trip latency when the check succeeded |
|
|
49
49
|
| `specVersion` | `number \| undefined` | Workflow spec version of the responding deployment |
|
|
50
50
|
| `workflowCoreVersion` | `string \| undefined` | `@workflow/core` version of the responding deployment |
|
|
51
|
+
| `dynamicWorkflowVersion` | `number \| undefined` | Dynamic-workflow runtime version of the responding deployment. Present only when it has opted in with `WORKFLOW_EXPERIMENTAL_DYNAMIC_WORKFLOWS`; see [Dynamic Workflows](/docs/advanced/dynamic-workflows) |
|
|
@@ -237,6 +237,15 @@ For example, a workflow can run a 10-minute inline step even with `WORKFLOW_REPL
|
|
|
237
237
|
- A bundle whose module scope consumes randomness, reads the clock, or replaces a serialization intrinsic cannot be snapshotted safely. The runtime detects these cases when preparing the snapshot and falls back to per-invocation evaluation.
|
|
238
238
|
- Set `0` or `false` to always evaluate the bundle per invocation.
|
|
239
239
|
|
|
240
|
+
## Dynamic workflows
|
|
241
|
+
|
|
242
|
+
### `WORKFLOW_EXPERIMENTAL_DYNAMIC_WORKFLOWS`
|
|
243
|
+
|
|
244
|
+
- Default: disabled
|
|
245
|
+
- Set `1` or `true` (case-insensitive) to let this deployment start [dynamic workflows](/docs/advanced/dynamic-workflows), execute their stored code on delivery, and advertise dynamic support in its health check. Any other value leaves it disabled.
|
|
246
|
+
- Dynamic source runs with the full privileges of the deployment's functions. Enable it only on deployments whose dynamic source you trust as much as your own code.
|
|
247
|
+
- When disabled, `start()` with source throws before writing anything, and a delivered dynamic run fails instead of executing.
|
|
248
|
+
|
|
240
249
|
## Compression and tracing
|
|
241
250
|
|
|
242
251
|
### `WORKFLOW_DISABLE_COMPRESSION`
|
|
@@ -281,7 +290,7 @@ For example, a workflow can run a 10-minute inline step even with `WORKFLOW_REPL
|
|
|
281
290
|
|
|
282
291
|
Node's own modules do less than the client they replace, so enabling this drops the per-call-site tuning the Worlds configure:
|
|
283
292
|
|
|
284
|
-
- Event-log requests lose HTTP/2, so concurrent reads and writes no longer share one connection, and the enlarged HTTP/2 receive windows no longer apply. This is the largest difference, and it slows down replays that read a big event log. It does not apply to event writes on the [WebSocket events transport](/docs/configuration/worlds#workflow_events_transport), which
|
|
293
|
+
- Event-log requests lose HTTP/2, so concurrent reads and writes no longer share one connection, and the enlarged HTTP/2 receive windows no longer apply. This is the largest difference, and it slows down replays that read a big event log. It does not apply to event writes on the opt-in [WebSocket events transport](/docs/configuration/worlds#workflow_events_transport), which takes neither transport.
|
|
285
294
|
- Requests lose their transport-level retry. Failures still surface to the layers above, which retry event writes and redeliver queue messages, so nothing is silently dropped, but a failure that a same-connection retry would have hidden now costs a full redelivery.
|
|
286
295
|
- Stream close loses its retry of retriable server errors. A transient failure at close can leave a stream marked closing until the run expires, where it would previously have resolved on the retry.
|
|
287
296
|
|
|
@@ -310,6 +310,6 @@ When enabled (the default), a suspension's eager `step_created` and `wait_create
|
|
|
310
310
|
|
|
311
311
|
- Factory option: none
|
|
312
312
|
- CLI flag: none
|
|
313
|
-
- Default: `
|
|
314
|
-
-
|
|
313
|
+
- Default: `http`
|
|
314
|
+
- Set to `ws` to ship workflow run events to the Vercel World over a WebSocket instead of one HTTP request each. Only `ws` (case-insensitive) opts in; any other value, including unset, empty, or `http`, keeps HTTP.
|
|
315
315
|
- Ignored when the World is configured with `projectConfig` and routes through the `api-workflow` proxy: that endpoint is an HTTP-only REST gateway and does not forward a WebSocket upgrade, so events stay on HTTP.
|
package/docs/meta.json
CHANGED
package/docs/whats-new.mdx
CHANGED
|
@@ -175,6 +175,7 @@ All three first-party Worlds now implement it: Vercel accepts up to 30 days, and
|
|
|
175
175
|
| `NestLocalBuilder` moved out of `@workflow/nest` root | Import it from `workflow/nest/builder`, so `WorkflowModule` no longer pulls the build toolchain into the runtime bundle. `NestVercelBuilder` lives at `workflow/nest/vercel-builder`. |
|
|
176
176
|
| `workflow/internal/private` and `@workflow/core/private` removed | These were never public API. The compiler no longer emits imports from them, so regenerate build output rather than importing them yourself. |
|
|
177
177
|
| The legacy trace viewer is gone from `@workflow/web-shared` | Only affects apps embedding the observability UI. `RunTraceView` and `WorkflowTraceViewer` are removed, and `NewTraceViewer` is now `TraceViewer` (module path `trace-viewer`). `Span`, `SpanEvent`, and `Trace` are still exported from the package root. |
|
|
178
|
+
| `startServer()` from `@workflow/web/server` resolves a `srvx` `Server` | Only affects apps that self-host the observability UI with it. Stop the server with `await server.close()`, and reach the Node `http.Server` at `server.node.server` to listen for its events. The server now also answers conditional and range requests and compresses responses. |
|
|
178
179
|
|
|
179
180
|
Runs created on 4.x keep executing on the deployment that created them, so upgrading a deployment does not migrate in-flight runs. One storage caveat is worth knowing about: failed runs stored by `@workflow/world-postgres` before the upgrade read back with `error: undefined`, because the payload lives in the legacy `error` text column rather than `errorJson`.
|
|
180
181
|
|
|
@@ -43,6 +43,7 @@ interface WorldCapabilities {
|
|
|
43
43
|
};
|
|
44
44
|
maxConcurrency?: boolean;
|
|
45
45
|
hookForceClaim?: boolean;
|
|
46
|
+
dynamicWorkflowCode?: boolean;
|
|
46
47
|
}
|
|
47
48
|
|
|
48
49
|
interface World extends Storage, Queue, Streamer {
|
|
@@ -61,7 +62,7 @@ interface World extends Storage, Queue, Streamer {
|
|
|
61
62
|
|
|
62
63
|
`specVersion` is required. See [Declaring the spec version](#declaring-the-spec-version).
|
|
63
64
|
|
|
64
|
-
The optional `capabilities` object advertises additional behavior, and every capability **fails closed**. A missing member means "unsupported," and the runtime keeps its conservative behavior. Set `hookRetention.active` to `true` only when the World implements hook token retention. Set `maxConcurrency` only when the World's queue supports `maxConcurrency`-limited consumption, which `WORKFLOW_SEQUENTIAL_REPLAYS=1` uses. Set `hookForceClaim` only when the World implements [hook token takeover](/worlds/upgrading-to-v5#new-optional-surface). Without it, `createHook({ experimental_force: true })` fails the workflow when it registers the hook.
|
|
65
|
+
The optional `capabilities` object advertises additional behavior, and every capability **fails closed**. A missing member means "unsupported," and the runtime keeps its conservative behavior. Set `hookRetention.active` to `true` only when the World implements hook token retention. Set `maxConcurrency` only when the World's queue supports `maxConcurrency`-limited consumption, which `WORKFLOW_SEQUENTIAL_REPLAYS=1` uses. Set `hookForceClaim` only when the World implements [hook token takeover](/worlds/upgrading-to-v5#new-optional-surface). Without it, `createHook({ experimental_force: true })` fails the workflow when it registers the hook. Set `dynamicWorkflowCode` only when the World persists `dynamicWorkflowCode` from `run_created` (and from a `run_started` that creates the run), echoes it on the created run, and returns it from `runs.get` with `resolveData: 'all'`. Without it, `start()` refuses [dynamic workflows](/docs/advanced/dynamic-workflows). The code is sent inline unless the World also implements `uploadDynamicWorkflowCode` for large definitions.
|
|
65
66
|
|
|
66
67
|
[Slot-numbered event IDs](#event-id-allocation) are a contract requirement rather than a capability, so there is no flag or fallback path.
|
|
67
68
|
|
|
@@ -117,7 +117,7 @@ None of this is required. Each entry is a hook the runtime uses if your World pr
|
|
|
117
117
|
|
|
118
118
|
| Member | What it buys |
|
|
119
119
|
| --- | --- |
|
|
120
|
-
| `capabilities` | Advertises `hookRetention.active`, `hookResumeDedup`, `hookForceClaim`, `deploymentAffinity`, and `
|
|
120
|
+
| `capabilities` | Advertises `hookRetention.active`, `hookResumeDedup`, `hookForceClaim`, `deploymentAffinity`, `maxConcurrency`, and `dynamicWorkflowCode`. See the contract note above about failing closed. Event ID allocation is *not* in here: it is a requirement, not a capability. |
|
|
121
121
|
| `analytics` | A metadata-only read namespace for observability surfaces. Payload-bearing reads stay on `runs`, `steps`, `events`, and `hooks`. |
|
|
122
122
|
| `runs.experimentalSetAttributes` | Backs `setAttributes()` from application code. Without it, run attributes are unavailable. |
|
|
123
123
|
| `runs.cancelMany` | Bulk cancellation: up to 500 unique run IDs per request (`BULK_CANCEL_MAX_RUN_IDS`), an optional `cancelReason` of at most 512 characters, and a per-run outcome for every ID. Without it, the runtime falls back to bounded-concurrency individual cancels. |
|
package/docs/worlds/vercel.mdx
CHANGED
|
@@ -252,11 +252,11 @@ Before routine authentication expiry or server max duration, the server sends a
|
|
|
252
252
|
|
|
253
253
|
### `WORKFLOW_EVENTS_TRANSPORT`
|
|
254
254
|
|
|
255
|
-
|
|
255
|
+
Opt-in WebSocket transport for workflow run events, which ships them to the Vercel World over one socket per run instead of one HTTP request each. Default: `http`.
|
|
256
256
|
|
|
257
|
-
Set `WORKFLOW_EVENTS_TRANSPORT=
|
|
257
|
+
Set `WORKFLOW_EVENTS_TRANSPORT=ws` to opt in. Only that value (case-insensitive) enables the WebSocket — any other value, including unset, empty, or `http`, keeps HTTP — so a typo fails toward the default rather than enabling a transport nobody asked for.
|
|
258
258
|
|
|
259
|
-
The setting is ignored when the World is configured with `projectConfig` and therefore routes through the `api-workflow` proxy: that endpoint is an HTTP-only REST gateway and does not forward a WebSocket upgrade, so events stay on HTTP. The fallback is silent
|
|
259
|
+
The setting is ignored when the World is configured with `projectConfig` and therefore routes through the `api-workflow` proxy: that endpoint is an HTTP-only REST gateway and does not forward a WebSocket upgrade, so events stay on HTTP. The fallback is silent, because the variable is typically set deployment-wide and a `projectConfig` World (such as the CLI) cannot act on it, and is reported once per process under `DEBUG=workflow:*`. `workflow.events.transport` on the per-write span records which transport actually carried a run.
|
|
260
260
|
|
|
261
261
|
Tracing is unaffected by the choice. Each event write emits an `http POST` client span against the same `url.full`, regardless of which transport carries it. On the WebSocket path, the span is synthesized around the frame because no HTTP request is made. Attributes distinguish the transports:
|
|
262
262
|
|
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.58",
|
|
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.58",
|
|
62
|
+
"@workflow/cli": "5.0.0-beta.58",
|
|
63
|
+
"@workflow/core": "5.0.0-beta.58",
|
|
64
|
+
"@workflow/errors": "5.0.0-beta.25",
|
|
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.58",
|
|
69
|
+
"@workflow/nest": "5.0.0-beta.58",
|
|
70
|
+
"@workflow/nitro": "5.0.0-beta.58",
|
|
71
|
+
"@workflow/nuxt": "5.0.0-beta.58",
|
|
72
|
+
"@workflow/sveltekit": "5.0.0-beta.58",
|
|
73
|
+
"@workflow/rollup": "5.0.0-beta.58"
|
|
74
74
|
},
|
|
75
75
|
"devDependencies": {
|
|
76
76
|
"@types/ms": "2.1.0",
|