workflow 5.0.0-beta.46 → 5.0.0-beta.48
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/internal/errors.d.ts +1 -1
- package/dist/internal/errors.d.ts.map +1 -1
- package/dist/internal/errors.js +2 -2
- package/docs/api-reference/workflow-api/resume-hook.mdx +5 -3
- package/docs/api-reference/workflow-api/start.mdx +12 -0
- package/docs/api-reference/workflow-runtime/world/analytics.mdx +185 -12
- package/docs/changelog/index.mdx +1 -1
- package/docs/changelog/lazy-hook-resume.mdx +60 -33
- package/docs/changelog/resilient-resume.mdx +8 -6
- package/docs/configuration/cli-and-web-ui.mdx +62 -3
- package/docs/configuration/runtime-tuning.mdx +4 -12
- package/docs/errors/corrupted-event-log.mdx +16 -7
- package/docs/foundations/errors-and-retries.mdx +1 -1
- package/docs/getting-started/python.mdx +113 -24
- package/docs/observability/attributes.mdx +23 -1
- package/docs/whats-new.mdx +1 -1
- package/package.json +15 -15
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { EntityConflictError, HookConflictError, HookNotFoundError, PreconditionFailedError, RunExpiredError, RunNotSupportedError, StepNotRegisteredError, ThrottleError, TooEarlyError, WorkflowError, WorkflowNotRegisteredError, WorkflowRunCancelledError, WorkflowRunFailedError, WorkflowRunNotCompletedError, WorkflowRunNotFoundError, WorkflowRuntimeError, WorkflowWorldError, } from '@workflow/errors';
|
|
1
|
+
export { EntityConflictError, HookConflictError, HookNotFoundError, PreconditionFailedError, RunExpiredError, RunNotSupportedError, StepNotRegisteredError, StreamError, ThrottleError, TooEarlyError, WorkflowError, WorkflowNotRegisteredError, WorkflowRunCancelledError, WorkflowRunFailedError, WorkflowRunNotCompletedError, WorkflowRunNotFoundError, WorkflowRuntimeError, WorkflowWorldError, } from '@workflow/errors';
|
|
2
2
|
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/internal/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,mBAAmB,EACnB,iBAAiB,EACjB,iBAAiB,EACjB,uBAAuB,EACvB,eAAe,EACf,oBAAoB,EACpB,sBAAsB,EACtB,aAAa,EACb,aAAa,EACb,aAAa,EACb,0BAA0B,EAC1B,yBAAyB,EACzB,sBAAsB,EACtB,4BAA4B,EAC5B,wBAAwB,EACxB,oBAAoB,EACpB,kBAAkB,GACnB,MAAM,kBAAkB,CAAC"}
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/internal/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,mBAAmB,EACnB,iBAAiB,EACjB,iBAAiB,EACjB,uBAAuB,EACvB,eAAe,EACf,oBAAoB,EACpB,sBAAsB,EACtB,WAAW,EACX,aAAa,EACb,aAAa,EACb,aAAa,EACb,0BAA0B,EAC1B,yBAAyB,EACzB,sBAAsB,EACtB,4BAA4B,EAC5B,wBAAwB,EACxB,oBAAoB,EACpB,kBAAkB,GACnB,MAAM,kBAAkB,CAAC"}
|
package/dist/internal/errors.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { EntityConflictError, HookConflictError, HookNotFoundError, PreconditionFailedError, RunExpiredError, RunNotSupportedError, StepNotRegisteredError, ThrottleError, TooEarlyError, WorkflowError, WorkflowNotRegisteredError, WorkflowRunCancelledError, WorkflowRunFailedError, WorkflowRunNotCompletedError, WorkflowRunNotFoundError, WorkflowRuntimeError, WorkflowWorldError, } from '@workflow/errors';
|
|
2
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
1
|
+
export { EntityConflictError, HookConflictError, HookNotFoundError, PreconditionFailedError, RunExpiredError, RunNotSupportedError, StepNotRegisteredError, StreamError, ThrottleError, TooEarlyError, WorkflowError, WorkflowNotRegisteredError, WorkflowRunCancelledError, WorkflowRunFailedError, WorkflowRunNotCompletedError, WorkflowRunNotFoundError, WorkflowRuntimeError, WorkflowWorldError, } from '@workflow/errors';
|
|
2
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZXJyb3JzLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL2ludGVybmFsL2Vycm9ycy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEVBQ0wsbUJBQW1CLEVBQ25CLGlCQUFpQixFQUNqQixpQkFBaUIsRUFDakIsdUJBQXVCLEVBQ3ZCLGVBQWUsRUFDZixvQkFBb0IsRUFDcEIsc0JBQXNCLEVBQ3RCLFdBQVcsRUFDWCxhQUFhLEVBQ2IsYUFBYSxFQUNiLGFBQWEsRUFDYiwwQkFBMEIsRUFDMUIseUJBQXlCLEVBQ3pCLHNCQUFzQixFQUN0Qiw0QkFBNEIsRUFDNUIsd0JBQXdCLEVBQ3hCLG9CQUFvQixFQUNwQixrQkFBa0IsR0FDbkIsTUFBTSxrQkFBa0IsQ0FBQyIsInNvdXJjZXNDb250ZW50IjpbImV4cG9ydCB7XG4gIEVudGl0eUNvbmZsaWN0RXJyb3IsXG4gIEhvb2tDb25mbGljdEVycm9yLFxuICBIb29rTm90Rm91bmRFcnJvcixcbiAgUHJlY29uZGl0aW9uRmFpbGVkRXJyb3IsXG4gIFJ1bkV4cGlyZWRFcnJvcixcbiAgUnVuTm90U3VwcG9ydGVkRXJyb3IsXG4gIFN0ZXBOb3RSZWdpc3RlcmVkRXJyb3IsXG4gIFN0cmVhbUVycm9yLFxuICBUaHJvdHRsZUVycm9yLFxuICBUb29FYXJseUVycm9yLFxuICBXb3JrZmxvd0Vycm9yLFxuICBXb3JrZmxvd05vdFJlZ2lzdGVyZWRFcnJvcixcbiAgV29ya2Zsb3dSdW5DYW5jZWxsZWRFcnJvcixcbiAgV29ya2Zsb3dSdW5GYWlsZWRFcnJvcixcbiAgV29ya2Zsb3dSdW5Ob3RDb21wbGV0ZWRFcnJvcixcbiAgV29ya2Zsb3dSdW5Ob3RGb3VuZEVycm9yLFxuICBXb3JrZmxvd1J1bnRpbWVFcnJvcixcbiAgV29ya2Zsb3dXb3JsZEVycm9yLFxufSBmcm9tICdAd29ya2Zsb3cvZXJyb3JzJztcbiJdfQ==
|
|
@@ -12,9 +12,11 @@ related:
|
|
|
12
12
|
|
|
13
13
|
Resumes a workflow run by sending a payload to a hook identified by its token.
|
|
14
14
|
|
|
15
|
-
It publishes a workflow
|
|
15
|
+
It durably writes the `hook_received` event and only then publishes a workflow wake. The call resolves only after both operations succeed, in that order.
|
|
16
16
|
|
|
17
|
-
`resumeHook()` throws `HookNotFoundError` when no hook holds the token
|
|
17
|
+
`resumeHook()` throws `HookNotFoundError` when no hook holds the token or when its `hook_received` write is refused because the hook was disposed or the run ended. See [durable hook resume](/docs/changelog/lazy-hook-resume).
|
|
18
|
+
|
|
19
|
+
If `resumeHook()` throws any other error, the outcome is ambiguous only in dispatch, never in durability: the event may already be durable even though the workflow wake failed, and any later wake of the run delivers it. Calling `resumeHook()` again creates a new `resumeId` and can append a second `hook_received`. Callers that need at-most-once behavior across separate invocations must retain and deduplicate their own request key.
|
|
18
20
|
|
|
19
21
|
<Callout type="warn">
|
|
20
22
|
`resumeHook` is a runtime function that must be called from outside a workflow function.
|
|
@@ -50,7 +52,7 @@ showSections={["parameters"]}
|
|
|
50
52
|
|
|
51
53
|
### Returns
|
|
52
54
|
|
|
53
|
-
Returns a `Promise<ResumedHook>`, a `Hook` extended with an optional `resilientResume` flag. Resolving means the
|
|
55
|
+
Returns a `Promise<ResumedHook>`, a `Hook` extended with an optional `resilientResume` flag. Resolving means the payload is durably recorded as `hook_received` and the workflow wake was accepted. `resilientResume` is retained for source compatibility and is no longer set by any path. The resolved hook:
|
|
54
56
|
|
|
55
57
|
<TSDoc
|
|
56
58
|
definition={`
|
|
@@ -108,6 +108,18 @@ The `deploymentId` option is currently a Vercel-specific feature. Other Worlds m
|
|
|
108
108
|
In Worlds without atomic, immutable deployments (such as local development or self-hosted Postgres), there is no notion of multiple deployments to resolve between, so `deploymentId: "latest"` has no effect: the SDK logs a warning and the run targets the current deployment. This means a workflow that opts into `"latest"` on Vercel still runs unchanged in local development.
|
|
109
109
|
</Callout>
|
|
110
110
|
|
|
111
|
+
<Callout type="info">
|
|
112
|
+
Resolving `"latest"` is the one `start()` path that calls the Vercel API, so it
|
|
113
|
+
needs an identity that can see the calling deployment. Inside a Vercel
|
|
114
|
+
deployment the SDK authenticates with the deployment's own OIDC token, which
|
|
115
|
+
carries the owning team, and this takes precedence over a `VERCEL_TOKEN` set in
|
|
116
|
+
the function's environment. A `VERCEL_TOKEN` belongs to a *user* and carries no
|
|
117
|
+
team, so authenticating with it scopes the lookup to that user's default team
|
|
118
|
+
and fails with a 404 whenever that is not the team that owns the deployment.
|
|
119
|
+
Outside a deployment (CLI, CI, the dashboard) `VERCEL_TOKEN` is still used;
|
|
120
|
+
configure the World's `teamId` so the request is scoped explicitly.
|
|
121
|
+
</Callout>
|
|
122
|
+
|
|
111
123
|
<Callout type="warn">
|
|
112
124
|
When using `deploymentId: "latest"`, the workflow run will execute on a potentially different deployment than the one calling `start()`. Be mindful of forward and backward compatibility:
|
|
113
125
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
title: Analytics
|
|
3
3
|
description: Metadata-only read APIs for runs, steps, events, hooks, waits, and attributes, backed by the observability pipeline.
|
|
4
4
|
type: reference
|
|
5
|
-
summary: "Interfaces: world.analytics.runs, .attributes, .steps, .events, .hooks, .waits. Metadata-only listings with plan-based lookback windows; filter runs by attribute key=value."
|
|
5
|
+
summary: "Interfaces: world.analytics.runs, .attributes, .steps, .events, .hooks, .waits. Metadata-only listings with plan-based lookback windows; filter runs by attribute key=value. Page limits are 1000 run-scoped, 100 cross-run."
|
|
6
6
|
prerequisites:
|
|
7
7
|
- /docs/api-reference/workflow-runtime/get-world
|
|
8
8
|
related:
|
|
@@ -13,7 +13,8 @@ keywords:
|
|
|
13
13
|
- analytics.runs
|
|
14
14
|
- analytics.attributes
|
|
15
15
|
- attribute filter
|
|
16
|
-
-
|
|
16
|
+
- getMany
|
|
17
|
+
- pagination limit
|
|
17
18
|
- lookback window
|
|
18
19
|
- observability-upgrade-required
|
|
19
20
|
- pageInfo
|
|
@@ -22,9 +23,10 @@ keywords:
|
|
|
22
23
|
|
|
23
24
|
`world.analytics` is an optional, read-only namespace for observability surfaces: dashboards, command-line interface (CLI) tools, and admin tools that list large numbers of runs without touching payload data.
|
|
24
25
|
|
|
25
|
-
|
|
26
|
-
[
|
|
27
|
-
|
|
26
|
+
Prefer this namespace for observability: listing, filtering, and inspecting
|
|
27
|
+
workflow state. Use the [Storage](/docs/api-reference/workflow-runtime/world/storage)
|
|
28
|
+
API for payload-bearing reads, and for anything operational that has to see the
|
|
29
|
+
canonical, up-to-the-moment record.
|
|
28
30
|
|
|
29
31
|
It differs from [Storage](/docs/api-reference/workflow-runtime/world/storage) in two ways:
|
|
30
32
|
|
|
@@ -108,19 +110,190 @@ for (const { key, runCount, lastSeenAt } of page.data) {
|
|
|
108
110
|
|
|
109
111
|
---
|
|
110
112
|
|
|
111
|
-
## analytics.steps
|
|
113
|
+
## analytics.steps
|
|
112
114
|
|
|
113
|
-
Run-scoped listings mirroring their [Storage](/docs/api-reference/workflow-runtime/world/storage) counterparts, minus payload data
|
|
115
|
+
Run-scoped step listings mirroring their [Storage](/docs/api-reference/workflow-runtime/world/storage) counterparts, minus payload data.
|
|
116
|
+
|
|
117
|
+
### steps.list()
|
|
118
|
+
|
|
119
|
+
```typescript lineNumbers
|
|
120
|
+
const steps = await world.analytics.steps.list({
|
|
121
|
+
runId,
|
|
122
|
+
pagination: { limit: 200, sortOrder: "asc" },
|
|
123
|
+
});
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
| Parameter | Type | Description |
|
|
127
|
+
|-----------|------|-------------|
|
|
128
|
+
| `params.runId` | `string` | Required. The run to list steps for |
|
|
129
|
+
| `params.pagination` | `PaginationOptions` | Cursor pagination, `limit` up to 1000 |
|
|
130
|
+
|
|
131
|
+
**Returns:** `PaginatedResponse<AnalyticsStep>`. Each step includes `stepId`, `stepName`, `status`, `attempt`, lifecycle timestamps, `errorCode`, and the `computeInstanceId` of the latest attempt.
|
|
132
|
+
|
|
133
|
+
### steps.get()
|
|
134
|
+
|
|
135
|
+
```typescript lineNumbers
|
|
136
|
+
const step = await world.analytics.steps.get(runId, stepId);
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
**Returns:** `AnalyticsStep`. A step id is only unique within its run, so both arguments are required.
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## analytics.events
|
|
144
|
+
|
|
145
|
+
### events.list()
|
|
146
|
+
|
|
147
|
+
```typescript lineNumbers
|
|
148
|
+
const events = await world.analytics.events.list({
|
|
149
|
+
runId,
|
|
150
|
+
eventType: "step_failed", // [!code highlight]
|
|
151
|
+
pagination: { limit: 1000 },
|
|
152
|
+
});
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
| Parameter | Type | Description |
|
|
156
|
+
|-----------|------|-------------|
|
|
157
|
+
| `params.runId` | `string` | Required. The run to list events for |
|
|
158
|
+
| `params.eventType` | `string` | One event type, for example `run_failed` or `step_retrying` |
|
|
159
|
+
| `params.correlationId` | `string` | Narrow to one entity: a step, hook, wait, or attribute id |
|
|
160
|
+
| `params.pagination` | `PaginationOptions` | Cursor pagination, `limit` up to 1000 |
|
|
161
|
+
|
|
162
|
+
**Returns:** `PaginatedResponse<AnalyticsEvent>`. Each event includes `eventId`, `eventType`, `correlationId`, `stepName`, `createdAt`, and provenance fields (`region`, `requestId`, `computeInstanceId`).
|
|
163
|
+
|
|
164
|
+
Pass a step id as `correlationId` to build that step's timeline: `step_created` through `step_completed`, `step_failed`, or `step_retrying`.
|
|
165
|
+
|
|
166
|
+
### events.get()
|
|
167
|
+
|
|
168
|
+
```typescript lineNumbers
|
|
169
|
+
const event = await world.analytics.events.get(runId, eventId);
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
**Returns:** `AnalyticsEvent`.
|
|
173
|
+
|
|
174
|
+
### events.getMany()
|
|
175
|
+
|
|
176
|
+
Look up a bounded set of event ids in one run with a single request.
|
|
177
|
+
|
|
178
|
+
```typescript lineNumbers
|
|
179
|
+
const events = await world.analytics.events.getMany(runId, eventIds); // [!code highlight]
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
**Returns:** `AnalyticsEvent[]` — not paginated, and no `pageInfo`. Duplicate ids are looked up once, and ids with no analytics row yet are **omitted rather than erroring**, since ingestion can trail canonical storage. Compare the returned length against your input to detect that.
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## analytics.hooks
|
|
187
|
+
|
|
188
|
+
### hooks.list()
|
|
114
189
|
|
|
115
190
|
```typescript lineNumbers
|
|
116
|
-
const steps = await world.analytics.steps.list({ runId });
|
|
117
|
-
const events = await world.analytics.events.list({ runId, eventType: "step_failed" });
|
|
118
|
-
const related = await world.analytics.events.listByCorrelationId({ runId, correlationId });
|
|
119
191
|
const hooks = await world.analytics.hooks.list({ runId });
|
|
120
|
-
const waits = await world.analytics.waits.list({ runId, status: "waiting" });
|
|
121
192
|
```
|
|
122
193
|
|
|
123
|
-
|
|
194
|
+
| Parameter | Type | Description |
|
|
195
|
+
|-----------|------|-------------|
|
|
196
|
+
| `params.runId` | `string` | Required. The run to list hooks for |
|
|
197
|
+
| `params.pagination` | `PaginationOptions` | Cursor pagination, `limit` up to 100 |
|
|
198
|
+
|
|
199
|
+
**Returns:** `PaginatedResponse<AnalyticsHook>`: `hookId`, `status` (`created`, `received`, `disposed`, or `conflict`), `receivedAt`, `disposedAt`, `isWebhook`, `isSystem`.
|
|
200
|
+
|
|
201
|
+
### hooks.get()
|
|
202
|
+
|
|
203
|
+
```typescript lineNumbers
|
|
204
|
+
const hook = await world.analytics.hooks.get(hookId);
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
Unlike steps and waits, a hook id identifies one hook on its own, so no `runId` is needed. Pass `{ runId }` to scope the lookup when you already know it.
|
|
208
|
+
|
|
209
|
+
<Callout>
|
|
210
|
+
Hook listings never include the hook token. Resolve it separately through the
|
|
211
|
+
runtime APIs if you need to deliver a payload.
|
|
212
|
+
</Callout>
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
## analytics.waits
|
|
217
|
+
|
|
218
|
+
### waits.list()
|
|
219
|
+
|
|
220
|
+
```typescript lineNumbers
|
|
221
|
+
const waits = await world.analytics.waits.list({
|
|
222
|
+
runId,
|
|
223
|
+
status: "waiting", // [!code highlight]
|
|
224
|
+
});
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
| Parameter | Type | Description |
|
|
228
|
+
|-----------|------|-------------|
|
|
229
|
+
| `params.runId` | `string` | Required. The run to list waits for |
|
|
230
|
+
| `params.status` | `string` | `waiting` or `completed` |
|
|
231
|
+
| `params.pagination` | `PaginationOptions` | Cursor pagination, `limit` up to 1000 |
|
|
232
|
+
|
|
233
|
+
**Returns:** `PaginatedResponse<AnalyticsWait>`: `waitId`, `status`, `resumeAt`, `completedAt`.
|
|
234
|
+
|
|
235
|
+
### waits.get()
|
|
236
|
+
|
|
237
|
+
```typescript lineNumbers
|
|
238
|
+
const wait = await world.analytics.waits.get(runId, waitId);
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
**Returns:** `AnalyticsWait`. A wait id is only unique within its run, so both arguments are required.
|
|
242
|
+
|
|
243
|
+
---
|
|
244
|
+
|
|
245
|
+
## Limits and validation
|
|
246
|
+
|
|
247
|
+
Arguments are validated in your process before a request goes out. An
|
|
248
|
+
out-of-range or malformed argument throws a `RangeError` naming the bound it
|
|
249
|
+
broke, rather than reaching the backend and coming back as a 400 — which
|
|
250
|
+
matters because analytics is optional and callers commonly wrap it in a
|
|
251
|
+
`try`/`catch`, where a rejected request is easy to mistake for "no data".
|
|
252
|
+
|
|
253
|
+
### Page limits
|
|
254
|
+
|
|
255
|
+
`pagination.limit` defaults to 40 everywhere. The maximum depends on whether
|
|
256
|
+
the listing scans within one run or across runs:
|
|
257
|
+
|
|
258
|
+
| Method | Max `limit` |
|
|
259
|
+
|--------|-------------|
|
|
260
|
+
| `steps.list()`, `events.list()`, `waits.list()` | 1000 |
|
|
261
|
+
| `runs.list()`, `attributes.list()`, `hooks.list()` | 100 |
|
|
262
|
+
|
|
263
|
+
`events.getMany()` is not paginated; it accepts 1 to 100 event ids per call.
|
|
264
|
+
|
|
265
|
+
<Callout type="warn">
|
|
266
|
+
The two page caps differ by a factor of ten, and `hooks.list()` takes the
|
|
267
|
+
lower one despite being run-scoped. Reusing one page size across listings is
|
|
268
|
+
the most common way to trip this.
|
|
269
|
+
</Callout>
|
|
270
|
+
|
|
271
|
+
### Identifiers
|
|
272
|
+
|
|
273
|
+
Every id is a prefix plus a ULID, and each is checked before the request:
|
|
274
|
+
|
|
275
|
+
| Parameter | Shape |
|
|
276
|
+
|-----------|-------|
|
|
277
|
+
| `runId` | `wrun_` |
|
|
278
|
+
| `stepId` | `step_` |
|
|
279
|
+
| `eventId` | `evnt_` |
|
|
280
|
+
| `hookId` | `hook_` |
|
|
281
|
+
| `waitId` | `wait_` |
|
|
282
|
+
| `correlationId` | `step_`, `hook_`, `wait_`, or `attr_` |
|
|
283
|
+
|
|
284
|
+
Step, event, and wait ids are only unique **within** their run, so the methods that take them require a `runId` too. A hook id stands alone.
|
|
285
|
+
|
|
286
|
+
### Time windows
|
|
287
|
+
|
|
288
|
+
`startTime` and `endTime` must be supplied **together** and be parseable ISO 8601 timestamps with `startTime` no later than `endTime`. Passing one without the other throws: it used to be dropped silently, which turned a listing you meant to bound into a scan of the whole retention window that looked like a successful answer.
|
|
289
|
+
|
|
290
|
+
### Attribute filters
|
|
291
|
+
|
|
292
|
+
`runs.list({ attributes })` accepts 1 to 8 pairs. Keys are 1 to 256 characters; values are at most 256 UTF-8 bytes. Reserved `$`-prefixed keys are valid in a filter even though user code cannot write them.
|
|
293
|
+
|
|
294
|
+
### Pagination
|
|
295
|
+
|
|
296
|
+
`cursor` is an opaque token from the previous response; do not construct or parse one. Branch on `hasMore`, not on `cursor` being non-null, and do not change `sortOrder` mid-walk — the cursor encodes the sort position, so reversing it can skip or repeat rows.
|
|
124
297
|
|
|
125
298
|
---
|
|
126
299
|
|
package/docs/changelog/index.mdx
CHANGED
|
@@ -12,7 +12,7 @@ Stay up to date with the latest changes to Workflow SDK.
|
|
|
12
12
|
|
|
13
13
|
## 2026
|
|
14
14
|
|
|
15
|
-
- [
|
|
15
|
+
- [Durable hook resume](/docs/changelog/lazy-hook-resume) (August 2026)
|
|
16
16
|
- [Resilient hook resume](/docs/changelog/resilient-resume) (July 2026)
|
|
17
17
|
- [Eager processing of steps and incremental event replay](/docs/changelog/eager-processing) (March 2026)
|
|
18
18
|
- Serializable AbortController and AbortSignal (March 12, 2026)
|
|
@@ -1,51 +1,78 @@
|
|
|
1
1
|
---
|
|
2
|
-
title:
|
|
3
|
-
description: resumeHook()
|
|
2
|
+
title: Durable hook resume
|
|
3
|
+
description: resumeHook() durably writes hook_received and only then publishes the workflow wake, so a resolved call can never be lost to a disposal race.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
#
|
|
6
|
+
# Durable hook resume
|
|
7
7
|
|
|
8
8
|
## Motivation
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
The previous lazy path published the serialized hook payload on the workflow
|
|
11
|
+
queue and left the queue consumer to create `hook_received`. If the hook was
|
|
12
|
+
disposed after `resumeHook()` returned but before the consumer write committed,
|
|
13
|
+
that write was rejected and the acknowledged queue delivery could not resume the
|
|
14
|
+
workflow: the caller was told the resume succeeded, and it was lost.
|
|
11
15
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
This change drops the producer's write entirely. On the lazy path `resumeHook()` publishes the queue message and nothing else.
|
|
16
|
+
`resumeHook()` now resolves only after both the durable event write and the
|
|
17
|
+
workflow wake have succeeded, in that order.
|
|
15
18
|
|
|
16
19
|
## Design
|
|
17
20
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
- `
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
21
|
+
The dispatch is strictly serial:
|
|
22
|
+
|
|
23
|
+
1. The hook is resolved by token. An unknown token throws `HookNotFoundError`.
|
|
24
|
+
2. The producer writes `hook_received` durably into the run's event log. A
|
|
25
|
+
client-minted `resumeId` and payload digest ride the write when the backend
|
|
26
|
+
supports atomic resume claims, so transport-level retries of the same write
|
|
27
|
+
converge on exactly one committed event. A write refused because the hook
|
|
28
|
+
was disposed or the run ended throws `HookNotFoundError`.
|
|
29
|
+
3. Only after the write is acknowledged does the producer publish the workflow
|
|
30
|
+
wake. The wake carries no payload — the payload lives in the event log — so
|
|
31
|
+
nothing rides on the queue message but the trigger. Publication is retried
|
|
32
|
+
a bounded number of times.
|
|
33
|
+
|
|
34
|
+
Because the event is committed before the wake exists, a disposal or run
|
|
35
|
+
completion racing the queue delivery cannot erase a resume the caller was told
|
|
36
|
+
succeeded: the delivery replays the committed event from the log.
|
|
37
|
+
|
|
38
|
+
- `ResumedHook.resilientResume` remains on the type for source compatibility
|
|
39
|
+
and is no longer set. The internal `resumeHookDurable()` entry point is
|
|
40
|
+
removed; `resumeHook()` itself now provides the durable guarantee.
|
|
41
|
+
|
|
42
|
+
A resolved call proves that the event is durable and the wake was accepted.
|
|
43
|
+
`HookNotFoundError` proves this invocation committed no event. Any other thrown
|
|
44
|
+
error is ambiguous only in *dispatch*, never in durability: a wake failure
|
|
45
|
+
after the write leaves the event committed, and any later wake of the run
|
|
46
|
+
(from any source) delivers it. A fresh `resumeHook()` invocation mints a new
|
|
47
|
+
`resumeId`, so blindly retrying a failed call can append a second
|
|
48
|
+
`hook_received`; callers that need at-most-once behavior across separate
|
|
49
|
+
invocations must deduplicate on their own request key.
|
|
34
50
|
|
|
35
51
|
## Behavior change: resumes against an ended run
|
|
36
52
|
|
|
37
|
-
The
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
53
|
+
The lazy path never observed the server's rejection — it published a message
|
|
54
|
+
and resolved, so a resume against a run that had already ended reported
|
|
55
|
+
success (reachable whenever the hook record outlives its run, e.g. token
|
|
56
|
+
retention). The durable write restores the check: **a resume against an ended
|
|
57
|
+
run now throws `HookNotFoundError`**, and a late webhook delivery to a
|
|
58
|
+
finished run answers 404 where it previously answered 202. Senders that treat
|
|
59
|
+
4xx as terminal will stop retrying such deliveries; that is the correct
|
|
60
|
+
signal, since nothing can resume an ended run.
|
|
42
61
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
62
|
+
A transient write conflict (HTTP 409, e.g. an event-slot conflict that
|
|
63
|
+
escaped the server's internal retry budget under contention) is no longer
|
|
64
|
+
re-keyed to `HookNotFoundError`. It surfaces as a retryable error, and its
|
|
65
|
+
rejected transaction committed nothing, so retrying the resume is safe.
|
|
46
66
|
|
|
47
67
|
## Compatibility
|
|
48
68
|
|
|
49
|
-
|
|
69
|
+
Nothing about the queue message changes: the wake has the same shape the
|
|
70
|
+
sequential path always published, so no consumer, backend, or server
|
|
71
|
+
coordination is needed and either side can roll back independently.
|
|
72
|
+
|
|
73
|
+
Consumers continue to accept legacy `hookInput` messages from older producers,
|
|
74
|
+
materializing their payload before replay. This permits rolling upgrades
|
|
75
|
+
without a coordinated producer and consumer deployment.
|
|
50
76
|
|
|
51
|
-
|
|
77
|
+
`WORKFLOW_DISABLE_LAZY_HOOK_RESUME` no longer gates anything and is ignored:
|
|
78
|
+
there is no lazy path left to disable.
|
|
@@ -6,11 +6,13 @@ description: resumeHook() now tolerates transient event storage failures when th
|
|
|
6
6
|
# Resilient `resumeHook()`
|
|
7
7
|
|
|
8
8
|
<Callout type="info">
|
|
9
|
-
Superseded by [
|
|
10
|
-
`resumeHook()`
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
9
|
+
Superseded by [durable hook resume](/docs/changelog/lazy-hook-resume):
|
|
10
|
+
`resumeHook()` now writes the `hook_received` event durably and only then
|
|
11
|
+
publishes the workflow wake, so the two-writer design, the queue-carried
|
|
12
|
+
payload, and the `resilientResume` flag described below are historical. The
|
|
13
|
+
`(runId, resumeId)` constraint remains, converging transport-level retries
|
|
14
|
+
of the producer's own write (and legacy `hookInput` redeliveries from older
|
|
15
|
+
producers).
|
|
14
16
|
</Callout>
|
|
15
17
|
|
|
16
18
|
## Motivation
|
|
@@ -27,4 +29,4 @@ description: resumeHook() now tolerates transient event storage failures when th
|
|
|
27
29
|
|
|
28
30
|
## Compatibility
|
|
29
31
|
|
|
30
|
-
The parallel fast path is gated per resume: it activates only when both the target run's queue consumer and the live backend independently attest dedup support (re-checked on every resume, so rollout and rollback both degrade safely). Otherwise (for oversized payloads
|
|
32
|
+
The parallel fast path is gated per resume: it activates only when both the target run's queue consumer and the live backend independently attest dedup support (re-checked on every resume, so rollout and rollback both degrade safely). Otherwise (for oversized payloads or legacy runs), `resumeHook()` falls back to the original sequential write-then-dispatch path. Because runs keep executing on the deployment they were created on, a resume targeting a run from an older deployment uses the sequential path.
|
|
@@ -94,14 +94,14 @@ Vercel project and auth settings can often be inferred from `.vercel/project.jso
|
|
|
94
94
|
### `--sort`
|
|
95
95
|
|
|
96
96
|
- Environment variable: none
|
|
97
|
-
- Default: `desc`
|
|
97
|
+
- Default: `desc` for time-ordered listings; `workflow inspect attributes` orders keys alphabetically unless you pass this flag
|
|
98
98
|
- Sort order for list commands. Accepts `asc` or `desc`.
|
|
99
99
|
|
|
100
100
|
### `--limit`
|
|
101
101
|
|
|
102
102
|
- Environment variable: none
|
|
103
103
|
- Default: `20`
|
|
104
|
-
- Number of items returned per page for list commands.
|
|
104
|
+
- Number of items returned per page for list commands. `workflow cancel` bounds it separately; see [`--limit` (cancel)](#--limit-cancel).
|
|
105
105
|
|
|
106
106
|
### `--cursor`
|
|
107
107
|
|
|
@@ -115,6 +115,63 @@ Vercel project and auth settings can often be inferred from `.vercel/project.jso
|
|
|
115
115
|
- Default: disabled
|
|
116
116
|
- Enables keyboard-controlled pagination for supported list commands.
|
|
117
117
|
|
|
118
|
+
## Inspect filtering
|
|
119
|
+
|
|
120
|
+
Flags for `workflow inspect`. Each list view accepts a different subset, noted
|
|
121
|
+
per flag.
|
|
122
|
+
|
|
123
|
+
### `--runId` / `-r`
|
|
124
|
+
|
|
125
|
+
- Command: `workflow inspect`
|
|
126
|
+
- Default: unset
|
|
127
|
+
- Scopes the listing to one run. Required for `steps`, `events`, and `sleeps`; optional for `hooks`. Must be a run ID: `wrun_` followed by a 26-character ULID.
|
|
128
|
+
|
|
129
|
+
### `--limit` (inspect)
|
|
130
|
+
|
|
131
|
+
- Command: `workflow inspect`
|
|
132
|
+
- Default: `20`
|
|
133
|
+
- Must be an integer between 1 and 100, the smallest page any inspect listing accepts. Larger pages are reachable by paging: pass `--cursor`, or `--interactive` to walk them.
|
|
134
|
+
|
|
135
|
+
### `--stepId` / `-s`
|
|
136
|
+
|
|
137
|
+
- Command: `workflow inspect events`
|
|
138
|
+
- Default: unset
|
|
139
|
+
- Filters events to one step.
|
|
140
|
+
|
|
141
|
+
### `--hookId`
|
|
142
|
+
|
|
143
|
+
- Command: `workflow inspect events`
|
|
144
|
+
- Default: unset
|
|
145
|
+
- Filters events to one hook.
|
|
146
|
+
|
|
147
|
+
### `--attribute`
|
|
148
|
+
|
|
149
|
+
- Command: `workflow inspect runs`
|
|
150
|
+
- Default: unset
|
|
151
|
+
- Filters runs to those whose [attributes](/docs/observability/attributes) match every `key=value` pair given. Repeatable up to 8 times, and splits on the first `=` so a value may contain one.
|
|
152
|
+
- Requires a backend with the analytics read path; ignored with a warning otherwise.
|
|
153
|
+
- Cannot be combined with `--url` or `--web`, which hand off to the dashboard, or with `--withData`, which reads payloads from storage. Storage carries no attribute index.
|
|
154
|
+
- Use `workflow inspect attributes` to discover which keys exist.
|
|
155
|
+
|
|
156
|
+
### `--since` / `--until`
|
|
157
|
+
|
|
158
|
+
- Command: `workflow inspect runs`, `workflow inspect attributes`
|
|
159
|
+
- Default: the backend's own window
|
|
160
|
+
- Bounds the listing to a window. `--since` opens the window and accepts a relative duration (`30m`, `12h`, `7d`, `2w`) or a timestamp. `--until` is optional and defaults to now, so `--until` on its own is rejected.
|
|
161
|
+
- Requires a backend with the analytics read path; ignored with a warning otherwise.
|
|
162
|
+
|
|
163
|
+
### `--withData` / `-d`
|
|
164
|
+
|
|
165
|
+
- Command: `workflow inspect`
|
|
166
|
+
- Default: disabled
|
|
167
|
+
- Includes full input and output payloads in list views. Deprecated for list views — use `workflow inspect <resource> <id>` to read one item's payloads. Setting it also moves the read off the analytics path, which carries metadata only.
|
|
168
|
+
|
|
169
|
+
### `--decrypt`
|
|
170
|
+
|
|
171
|
+
- Command: `workflow inspect`
|
|
172
|
+
- Default: disabled
|
|
173
|
+
- Decrypts encrypted values. Triggers an audit-logged key retrieval.
|
|
174
|
+
|
|
118
175
|
## Bulk cancel
|
|
119
176
|
|
|
120
177
|
`workflow cancel <run-id>` cancels one run. Given a filter instead, it bulk-cancels a batch; bulk mode requires `--status` or `--workflowName`.
|
|
@@ -124,18 +181,20 @@ Vercel project and auth settings can often be inferred from `.vercel/project.jso
|
|
|
124
181
|
- Command: `workflow cancel`
|
|
125
182
|
- Default: unset
|
|
126
183
|
- Restricts the batch to this status. Only `pending` and `running` are accepted; terminal runs cannot be canceled.
|
|
184
|
+
- Also filters `workflow inspect runs`, which accepts any run status. It does not narrow `workflow inspect attributes`, which indexes keys per tenant rather than per run; passing it there warns and lists every key.
|
|
127
185
|
|
|
128
186
|
### `--workflowName` / `-n`
|
|
129
187
|
|
|
130
188
|
- Command: `workflow cancel`
|
|
131
189
|
- Default: unset
|
|
132
190
|
- Restricts the batch to one workflow. Expects the generated workflow ID from `workflow inspect runs`, not the short function name.
|
|
191
|
+
- Also filters `workflow inspect runs` and `workflow inspect attributes`.
|
|
133
192
|
|
|
134
193
|
### `--limit` (cancel)
|
|
135
194
|
|
|
136
195
|
- Command: `workflow cancel`
|
|
137
196
|
- Default: `50`
|
|
138
|
-
- Maximum runs to cancel in one batch (1–
|
|
197
|
+
- Maximum runs to cancel in one batch (1–100), the largest page the run listing serves. Only one batch is canceled per invocation; run the command again to cancel the next batch.
|
|
139
198
|
|
|
140
199
|
### `--confirm` / `-y`
|
|
141
200
|
|
|
@@ -81,14 +81,6 @@ For example, a workflow can run a 10-minute inline step even with `WORKFLOW_REPL
|
|
|
81
81
|
- Default: `3`
|
|
82
82
|
- Recovery replays before replay divergence is recorded as corruption.
|
|
83
83
|
|
|
84
|
-
### `WORKFLOW_DISABLE_LAZY_HOOK_RESUME`
|
|
85
|
-
|
|
86
|
-
- Default: enabled (lazy hook resume on)
|
|
87
|
-
- Resuming a hook publishes the workflow invocation and writes no event, so the resume costs one round trip. The queue message carries the payload, and the consumer creates the `hook_received` event from it before replay. A backend `(runId, resumeId)` constraint keeps redeliveries of that message converging on exactly one event. Because the message is the only copy of the payload, a failed publish fails the resume.
|
|
88
|
-
- The runtime falls back to the sequential path when the consumer or backend does not attest dedup support (or the payload is too large to inline on the queue message). On the sequential path, the event is written *before* dispatch, and its failure fails the resume.
|
|
89
|
-
- The two paths differ on ended runs: the sequential write is rejected and surfaces as `HookNotFoundError`, while the lazy path resolves because it never writes. Neither resumes the run.
|
|
90
|
-
- Set `1` to force the sequential path as a kill switch. The chosen strategy is reported on the resume span as `workflow.hook.resume_strategy`.
|
|
91
|
-
|
|
92
84
|
### `WORKFLOW_DEPLOYMENT_MISMATCH_MAX_RETRIES`
|
|
93
85
|
|
|
94
86
|
- Default: `3`
|
|
@@ -100,7 +92,7 @@ For example, a workflow can run a 10-minute inline step even with `WORKFLOW_REPL
|
|
|
100
92
|
### `WORKFLOW_RESILIENT_STEP_DISPATCH`
|
|
101
93
|
|
|
102
94
|
- Default: disabled
|
|
103
|
-
- When a suspension hands newly created steps to the queue, the runtime publishes each step's execution message in parallel with its `step_created` event write instead of sequencing them, cutting a round trip per dispatched step. The message also carries the serialized step input (`stepInput`), so a transient `step_created` write failure (429 / 5xx / transport) still executes the step. The queue consumer idempotently re-ensures the event before running it, converging with the producer's write on the step's correlation ID. This mirrors resilient start (`runInput`) and the lazy hook resume
|
|
95
|
+
- When a suspension hands newly created steps to the queue, the runtime publishes each step's execution message in parallel with its `step_created` event write instead of sequencing them, cutting a round trip per dispatched step. The message also carries the serialized step input (`stepInput`), so a transient `step_created` write failure (429 / 5xx / transport) still executes the step. The queue consumer idempotently re-ensures the event before running it, converging with the producer's write on the step's correlation ID. This mirrors resilient start (`runInput`) and the legacy lazy hook resume's `hookInput` (which current producers no longer send; see [durable hook resume](/docs/changelog/lazy-hook-resume)).
|
|
104
96
|
- It is off by default because the publish races the create's verdict, and a create can come back refused: as a duplicate this replay should stop pursuing, or as a [stale write](#stale-reads-and-why-nothing-has-to-be-rejected) on a World that refuses rather than reports. Either way the message carrying the payload is already out, so the consumer can materialize a step whose create was refused, and nothing orders the verdict before the consumer's redelivery re-ensure. The sequential path is the only one that gives the message a happens-after edge over it.
|
|
105
97
|
- Even when enabled, the runtime falls back to the sequential create-then-publish dispatch when the step input is too large to inline on the queue message, or when the run's queue transport cannot carry binary payloads (pre-CBOR spec versions).
|
|
106
98
|
- Producer-side recoveries are reported on the suspension span as `workflow.step.resilient_dispatch_recovered`; a consumer that materialized the event reports `workflow.step.resilient_dispatch_materialized`.
|
|
@@ -193,10 +185,10 @@ For example, a workflow can run a 10-minute inline step even with `WORKFLOW_REPL
|
|
|
193
185
|
### `WORKFLOW_RETAINED_VM`
|
|
194
186
|
|
|
195
187
|
- Default: enabled
|
|
196
|
-
- Keeps the suspended workflow VM alive across inline steps within one invocation, so each iteration of the inline loop appends only the newly written events instead of replaying the whole event log in a fresh VM.
|
|
197
|
-
- A step-driven suspension can keep the VM retained even when hooks are open or created at the same boundary. Hook-only suspensions park the invocation
|
|
188
|
+
- Node.js VM engine only. Keeps the suspended workflow VM alive across inline steps within one invocation, so each iteration of the inline loop appends only the newly written events instead of replaying the whole event log in a fresh VM. QuickJS manages its own retained inline loop independently of this setting.
|
|
189
|
+
- A step- or attribute-driven suspension can keep the VM retained even when hooks or waits are open or created at the same boundary. Hook- or wait-only suspensions park the invocation because nothing in the current delivery can advance them, with one exception: when the hook's own create is what the workflow is waiting on (a `hook.getConflict()` awaiter, or a create whose token is already claimed), the invocation resumes the retained VM over the committed `hook_created` or `hook_conflict` instead of re-invoking through the queue. Any replay divergence falls back to a full replay.
|
|
198
190
|
- Step inputs made of plain data (objects, arrays, primitives) and standard built-ins (`Map`, `Set`, `Date`, `RegExp`, typed arrays, `ArrayBuffer`, `URL`, `Headers`) keep the VM retained. Patching or polyfilling built-in prototypes doesn't change that because serialization never calls them. A boundary falls back to a full replay only when serializing its arguments runs code the workflow controls, such as a getter, a proxy, or a custom class serializer, or computes an `Error`'s stack trace.
|
|
199
|
-
- Set `0` or `false` to replay from scratch in a fresh VM on every iteration.
|
|
191
|
+
- Set `0` or `false` to replay the Node.js workflow from scratch in a fresh VM on every iteration.
|
|
200
192
|
|
|
201
193
|
### `WORKFLOW_INLINE_OWNERSHIP`
|
|
202
194
|
|
|
@@ -1,37 +1,46 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: corrupted-event-log
|
|
3
|
-
description: The workflow's event log contains an event that
|
|
3
|
+
description: The workflow's event log contains an event that cannot be processed or a stored payload that cannot be read.
|
|
4
4
|
type: troubleshooting
|
|
5
|
-
summary: Resolve corrupted event log errors caused by
|
|
5
|
+
summary: Resolve corrupted event log errors caused by invalid events or unreadable stored payloads.
|
|
6
6
|
prerequisites:
|
|
7
7
|
- /docs/foundations/workflows-and-steps
|
|
8
8
|
related:
|
|
9
9
|
- /docs/foundations/errors-and-retries
|
|
10
10
|
---
|
|
11
11
|
|
|
12
|
-
This error occurs when the Workflow runtime
|
|
12
|
+
This error occurs when the Workflow runtime cannot safely replay the event log. The log may be in an invalid state, such as an orphaned event or one no consumer can attribute to anything the workflow did, or it may reference a stored payload that the World can no longer read.
|
|
13
13
|
|
|
14
|
-
This is a **workflow-level fatal error**. It cannot be caught or handled inside your workflow code. The runtime
|
|
14
|
+
This is a **workflow-level fatal error**. It cannot be caught or handled inside your workflow code. The runtime retries transient replay divergence automatically, but an unreadable stored payload is terminal immediately because replaying cannot restore it.
|
|
15
15
|
|
|
16
16
|
## Error message
|
|
17
17
|
|
|
18
|
+
For replay divergence:
|
|
19
|
+
|
|
18
20
|
```text
|
|
19
21
|
Workflow replay diverged <divergenceCount> times after <maxRecoveryReplays> recovery replays; latest divergent event was <eventId>. Last divergence: <details>
|
|
20
22
|
```
|
|
21
23
|
|
|
24
|
+
For an unreadable stored payload:
|
|
25
|
+
|
|
26
|
+
```text
|
|
27
|
+
the event log references a payload that no longer exists in storage: <details>
|
|
28
|
+
```
|
|
29
|
+
|
|
22
30
|
## Why this happens
|
|
23
31
|
|
|
24
32
|
Workflows persist their progress as an ordered event log. During replay, the runtime processes each event in sequence. Every event must be consumed by a matching callback, such as a step or sleep waiting for its result. An event no callback ever claims is one the runtime would have to drop to finish the run, so it fails the run instead of returning a result that silently ignored it.
|
|
25
33
|
|
|
26
34
|
A delivery written from outside the replay, such as a hook firing or a step completing on another invocation, can land ahead of the events the replay is writing itself. That is ordinary concurrency rather than corruption, so the runtime holds such an event and offers it to each consumer the replay registers afterwards. The failure comes only when the workflow function returns while an event is still held, at which point no consumer can ever appear. A replay that suspends still holding one reports it on the span (`workflow.events.parked.count`, `.event_id`, `.event_type`) and leaves the decision to the replay that follows.
|
|
27
35
|
|
|
28
|
-
Before failing, the runtime retries
|
|
36
|
+
Before failing on divergence, the runtime retries the replay and surfaces this terminal error only if replay still cannot recover. It does not retry a payload that the World reports as permanently missing.
|
|
29
37
|
|
|
30
38
|
Common scenarios that produce this error:
|
|
31
39
|
|
|
32
40
|
- **An unclaimed event that repeats nothing**: A duplicate of a kind the log already records for that entity is read past rather than failing the run, so a second `step_completed` or `wait_completed` is not this error (see [Duplicate Events](/docs/how-it-works/event-sourcing#duplicate-events)). What fails is an unclaimed event with no earlier counterpart to defer to: a `step_started` behind a `step_completed` on a log that never recorded a `step_started`, for instance. No consumer remains for the step, and there is no earlier event of that kind the replay could be reading instead.
|
|
33
41
|
- **Orphaned events**: A `step_completed` or `wait_completed` event whose `correlationId` doesn't match any step or sleep in the workflow code, so the replay reaches its end still holding it.
|
|
34
42
|
- **A hole in the log**: Events are numbered by their position in the run's log, and those positions are dense, so a position below the log's highest that holds no event means the log the replay loaded is incomplete. The runtime cannot tell a position no write ever occupied from one whose event it failed to read, so it refuses to replay rather than produce a result that may be silently wrong. See [`WORKFLOW_SLOT_GAP_CHECK`](/docs/configuration/runtime-tuning#workflow_slot_gap_check).
|
|
43
|
+
- **An unreadable stored payload**: An event row still references a payload object, but the World reports that the object no longer exists in its storage. The same log would fail on every replay, so the run fails immediately instead of retrying forever.
|
|
35
44
|
|
|
36
45
|
## What to do
|
|
37
46
|
|
|
@@ -47,7 +56,7 @@ npm install workflow@latest
|
|
|
47
56
|
|
|
48
57
|
### 2. Retry the failed run
|
|
49
58
|
|
|
50
|
-
If this error
|
|
59
|
+
If this error reports replay divergence, automatic replay recovery has already been exhausted. If it reports an unreadable payload, recovery cannot recreate that payload. In either case, the run has been marked as `failed`. You can re-run the workflow using the **Re-run** button in the Workflow Dashboard; a re-run starts a new run with a new event log.
|
|
51
60
|
|
|
52
61
|
### 3. Report the issue
|
|
53
62
|
|
|
@@ -55,7 +64,7 @@ If the error persists after upgrading, [open an issue on GitHub](https://github.
|
|
|
55
64
|
|
|
56
65
|
- The version of the `workflow` package you are using
|
|
57
66
|
- The run ID(s) of the affected workflow run(s)
|
|
58
|
-
- The error message
|
|
67
|
+
- The complete error message, including any `eventType`, `correlationId`, `eventId`, or payload details
|
|
59
68
|
- Any details about the event log or the workflow that triggered the error
|
|
60
69
|
|
|
61
70
|
## This error cannot be caught
|
|
@@ -195,7 +195,7 @@ try {
|
|
|
195
195
|
| `MAX_DELIVERIES_EXCEEDED` | The run exceeded the maximum number of queue deliveries |
|
|
196
196
|
| `REPLAY_TIMEOUT` | A workflow replay exceeded the maximum allowed duration |
|
|
197
197
|
| `REPLAY_DIVERGENCE` | A replay could not consume the event log deterministically, usually because of non-deterministic workflow code. |
|
|
198
|
-
| `CORRUPTED_EVENT_LOG` | The event log contains orphaned or mismatched events
|
|
198
|
+
| `CORRUPTED_EVENT_LOG` | The event log cannot be replayed: it contains orphaned or mismatched events, or one of its stored payloads is no longer readable from the World's storage. If you see this, please [file an issue](https://github.com/vercel/workflow/issues) |
|
|
199
199
|
| `WORLD_CONTRACT_ERROR` | A World response violated the SDK contract; points at a World implementation bug |
|
|
200
200
|
| `RUNTIME_ERROR` | An internal runtime error. If you see this, please [file an issue](https://github.com/vercel/workflow/issues) |
|
|
201
201
|
|
|
@@ -11,23 +11,23 @@ related:
|
|
|
11
11
|
---
|
|
12
12
|
|
|
13
13
|
<CopyPrompt
|
|
14
|
-
text="
|
|
14
|
+
text="Set up Workflow in this Python project. In `pyproject.toml`, add `requires-python = ">=3.12"` and `dependencies = ["vercel-workflow"]` under `[project]`, then add `[[tool.vercel.workflows]]` with `entrypoint = "app.workflows:wf"`. Create `app/workflow.py` with `from vercel import workflow` and `wf = workflow.Workflows()`. Create `app/steps/generate_draft.py`, import `wf`, and define async step functions such as `generate_draft` and `summarize_draft`, decorating each with `@wf.step`. Then create `app/workflows/ai_content_workflow.py`, import `wf` and those step functions, and define `@wf.workflow async def ai_content_workflow(*, topic: str)` to orchestrate them and return the result. In `app/workflows/__init__.py`, export `wf` and import the workflow module so its definitions are registered. From server-side code, start it with `await workflow.start(ai_content_workflow, topic=...)`; use the returned `Run` to access its ID, check its status, or await its return value. Where the workflow needs a durable delay, use `await workflow.sleep(timedelta(days=7))` after importing `timedelta` from `datetime`. Where it needs an external approval event, define a Pydantic model that also extends `workflow.BaseHook`, wait with `.wait(token=...)`, and resume it from server-side code with `.resume(token)`."
|
|
15
15
|
/>
|
|
16
16
|
|
|
17
17
|
<Callout type="warn">
|
|
18
|
-
The Python SDK is currently in **beta**. APIs and behavior may change.
|
|
18
|
+
The Python SDK is currently in **beta**. APIs and behavior may change.
|
|
19
19
|
</Callout>
|
|
20
20
|
|
|
21
|
-
You can build durable workflows in Python using the [`vercel`
|
|
21
|
+
You can build durable workflows in Python using the [`vercel-workflow` SDK](https://pypi.org/project/vercel-workflow/). Your workflow code can pause, resume, and maintain state, just like the JavaScript and TypeScript Workflow SDK.
|
|
22
22
|
|
|
23
23
|
## Getting started
|
|
24
24
|
|
|
25
|
-
Add the `vercel` package and workflow entrypoint to `pyproject.toml`:
|
|
25
|
+
Add the `vercel-workflow` package and workflow entrypoint to `pyproject.toml`:
|
|
26
26
|
|
|
27
27
|
```toml filename="pyproject.toml"
|
|
28
28
|
[project]
|
|
29
29
|
requires-python = ">=3.12"
|
|
30
|
-
dependencies = ["vercel"]
|
|
30
|
+
dependencies = ["vercel-workflow"]
|
|
31
31
|
|
|
32
32
|
[[tool.vercel.workflows]]
|
|
33
33
|
entrypoint = "app.workflows:wf"
|
|
@@ -39,16 +39,17 @@ The workflow `entrypoint` uses the `module:object` format and points to the expo
|
|
|
39
39
|
|
|
40
40
|
A workflow is a stateful function that coordinates multi-step logic over time. Create a `Workflows` instance and use the `@wf.workflow` decorator to mark a function as durable:
|
|
41
41
|
|
|
42
|
-
```python filename="app/workflow.py"
|
|
42
|
+
```python filename="app/workflow.py"
|
|
43
43
|
from vercel import workflow
|
|
44
44
|
|
|
45
|
-
wf = workflow.Workflows()
|
|
45
|
+
wf = workflow.Workflows() # [!code highlight]
|
|
46
46
|
```
|
|
47
47
|
|
|
48
|
-
```python filename="app/workflows/ai_content_workflow.py"
|
|
48
|
+
```python filename="app/workflows/ai_content_workflow.py"
|
|
49
49
|
from app.workflow import wf
|
|
50
|
+
from app.steps.generate_draft import generate_draft, summarize_draft
|
|
50
51
|
|
|
51
|
-
@wf.workflow
|
|
52
|
+
@wf.workflow # [!code highlight]
|
|
52
53
|
async def ai_content_workflow(*, topic: str):
|
|
53
54
|
draft = await generate_draft(topic=topic)
|
|
54
55
|
summary = await summarize_draft(draft=draft)
|
|
@@ -74,15 +75,15 @@ Under the hood, the workflow compiles into a route that orchestrates execution.
|
|
|
74
75
|
|
|
75
76
|
A step is a stateless function that runs a unit of durable work inside a workflow. Use `@wf.step` to mark a function as a step:
|
|
76
77
|
|
|
77
|
-
```python filename="app/steps/generate_draft.py"
|
|
78
|
+
```python filename="app/steps/generate_draft.py"
|
|
78
79
|
import random
|
|
79
80
|
from app.workflow import wf
|
|
80
81
|
|
|
81
|
-
@wf.step
|
|
82
|
+
@wf.step # [!code highlight]
|
|
82
83
|
async def generate_draft(*, topic: str):
|
|
83
84
|
return await ai_generate(prompt=f"Write a blog post about {topic}")
|
|
84
85
|
|
|
85
|
-
@wf.step
|
|
86
|
+
@wf.step # [!code highlight]
|
|
86
87
|
async def summarize_draft(*, draft: str):
|
|
87
88
|
summary = await ai_summarize(text=draft)
|
|
88
89
|
|
|
@@ -95,18 +96,42 @@ async def summarize_draft(*, draft: str):
|
|
|
95
96
|
|
|
96
97
|
Each step executes separately from the workflow orchestrator. While the step executes, the workflow suspends without consuming resources. When the step completes, the workflow resumes automatically where it left off.
|
|
97
98
|
|
|
99
|
+
## Starting a workflow
|
|
100
|
+
|
|
101
|
+
Call `workflow.start()` from server-side code to start a workflow. It returns a `Run` that you can use to identify the run, check its status, and wait for its result:
|
|
102
|
+
|
|
103
|
+
```python filename="app/api/generate.py"
|
|
104
|
+
from app.workflows.ai_content_workflow import ai_content_workflow
|
|
105
|
+
from vercel import workflow
|
|
106
|
+
|
|
107
|
+
@app.post("/api/generate")
|
|
108
|
+
async def generate_content(*, topic: str):
|
|
109
|
+
run = await workflow.start(ai_content_workflow, topic=topic) # [!code highlight]
|
|
110
|
+
|
|
111
|
+
print(run.run_id)
|
|
112
|
+
print(await run.status()) # [!code highlight]
|
|
113
|
+
|
|
114
|
+
# Wait until the workflow completes and return its result.
|
|
115
|
+
return await run.return_value() # [!code highlight]
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Starting a workflow only waits until the run has been created and queued. Await `return_value()` to wait for the workflow to finish, or save its `run_id` and recreate the handle later with `workflow.Run(run_id)`.
|
|
119
|
+
|
|
98
120
|
## Sleep
|
|
99
121
|
|
|
100
122
|
Sleep pauses a workflow for a specified duration without consuming compute resources:
|
|
101
123
|
|
|
102
|
-
```python filename="app/workflows/ai_refine.py"
|
|
124
|
+
```python filename="app/workflows/ai_refine.py"
|
|
125
|
+
from datetime import timedelta
|
|
126
|
+
|
|
127
|
+
from app.workflow import wf
|
|
103
128
|
from vercel import workflow
|
|
104
129
|
|
|
105
130
|
@wf.workflow
|
|
106
131
|
async def ai_refine_workflow(*, draft_id: str):
|
|
107
132
|
draft = await fetch_draft(draft_id)
|
|
108
133
|
|
|
109
|
-
await workflow.sleep(
|
|
134
|
+
await workflow.sleep(timedelta(days=7)) # Wait 7 days to gather more signals. # [!code highlight]
|
|
110
135
|
|
|
111
136
|
refined = await refine_draft(draft)
|
|
112
137
|
|
|
@@ -116,7 +141,31 @@ async def ai_refine_workflow(*, draft_id: str):
|
|
|
116
141
|
}
|
|
117
142
|
```
|
|
118
143
|
|
|
119
|
-
The
|
|
144
|
+
The parameter accepts four forms:
|
|
145
|
+
|
|
146
|
+
| Form | Description | Example |
|
|
147
|
+
| --- | --- | --- |
|
|
148
|
+
| `str` | Human-readable duration string | `"2 days"`, `"1w"`, `"1h 30m"` |
|
|
149
|
+
| `int` or `float` | Seconds from now | `5` (5 seconds) |
|
|
150
|
+
| `datetime.timedelta` | Duration from now | `timedelta(days=7)` |
|
|
151
|
+
| `datetime.datetime` | Absolute wake-up time (must be timezone-aware) | `datetime(2025, 1, 1, tzinfo=UTC)` |
|
|
152
|
+
|
|
153
|
+
The string form accepts one or more `<value><unit>` pairs. Supported units:
|
|
154
|
+
|
|
155
|
+
| Duration | Unit |
|
|
156
|
+
| --- | --- |
|
|
157
|
+
| Milliseconds | `ms` |
|
|
158
|
+
| Seconds | `s`, `second`, `seconds` |
|
|
159
|
+
| Minutes | `m`, `minute`, `minutes` |
|
|
160
|
+
| Hours | `h`, `hour`, `hours` |
|
|
161
|
+
| Days | `d`, `day`, `days` |
|
|
162
|
+
| Weeks | `w`, `week`, `weeks` |
|
|
163
|
+
|
|
164
|
+
<Callout>
|
|
165
|
+
`sleep()` must be called from the workflow body, not from inside a step. Calling it from a step raises a `RuntimeError`.
|
|
166
|
+
</Callout>
|
|
167
|
+
|
|
168
|
+
The sleep consumes no resources. The workflow resumes automatically when the time expires.
|
|
120
169
|
|
|
121
170
|
## Hooks
|
|
122
171
|
|
|
@@ -124,13 +173,17 @@ A hook lets a workflow wait for external events such as user actions, webhooks,
|
|
|
124
173
|
|
|
125
174
|
Define a hook model with Pydantic and `workflow.BaseHook`:
|
|
126
175
|
|
|
127
|
-
```python filename="app/workflows/approval.py"
|
|
176
|
+
```python filename="app/workflows/approval.py"
|
|
177
|
+
import typing
|
|
178
|
+
|
|
179
|
+
import pydantic
|
|
180
|
+
from app.workflow import wf
|
|
128
181
|
from vercel import workflow
|
|
129
182
|
|
|
130
|
-
class Approval(BaseModel, workflow.BaseHook):
|
|
183
|
+
class Approval(pydantic.BaseModel, workflow.BaseHook): # [!code highlight]
|
|
131
184
|
"""Human approval for AI-generated drafts"""
|
|
132
185
|
|
|
133
|
-
decision: Literal["approved", "changes"]
|
|
186
|
+
decision: typing.Literal["approved", "changes"]
|
|
134
187
|
notes: str | None = None
|
|
135
188
|
|
|
136
189
|
@wf.workflow
|
|
@@ -138,7 +191,7 @@ async def ai_approval_workflow(*, topic: str):
|
|
|
138
191
|
draft = await generate_draft(topic=topic)
|
|
139
192
|
|
|
140
193
|
# Wait for human approval events
|
|
141
|
-
async for event in Approval.wait(token="draft-123"):
|
|
194
|
+
async for event in Approval.wait(token="draft-123"): # [!code highlight]
|
|
142
195
|
if event.decision == "approved":
|
|
143
196
|
await publish_draft(draft)
|
|
144
197
|
break
|
|
@@ -149,20 +202,56 @@ async def ai_approval_workflow(*, topic: str):
|
|
|
149
202
|
|
|
150
203
|
Resume the workflow when data arrives:
|
|
151
204
|
|
|
152
|
-
```python filename="app/api/resume.py"
|
|
205
|
+
```python filename="app/api/resume.py"
|
|
206
|
+
from app.workflows.approval import Approval
|
|
207
|
+
|
|
153
208
|
@app.post("/api/resume")
|
|
154
|
-
async def resume(approval: Approval):
|
|
209
|
+
async def resume(approval: Approval): # [!code highlight]
|
|
155
210
|
"""Resume the workflow when an approval is received"""
|
|
156
211
|
|
|
157
|
-
await approval.resume("draft-123")
|
|
212
|
+
await approval.resume("draft-123") # [!code highlight]
|
|
158
213
|
return {"ok": True}
|
|
159
214
|
```
|
|
160
215
|
|
|
161
216
|
When a hook receives data, the workflow resumes automatically. You don't need polling, message queues, or manual state management.
|
|
162
217
|
|
|
163
|
-
##
|
|
218
|
+
## Streaming
|
|
219
|
+
|
|
220
|
+
Steps can stream progress while a workflow is running. Get the run's writable stream inside a step, write values to it, and close it when no more values will be sent:
|
|
221
|
+
|
|
222
|
+
```python filename="app/workflows/streaming.py"
|
|
223
|
+
from app.workflow import wf
|
|
224
|
+
from vercel import workflow
|
|
225
|
+
|
|
226
|
+
@wf.step
|
|
227
|
+
async def write_progress():
|
|
228
|
+
writable = workflow.get_writable() # [!code highlight]
|
|
229
|
+
|
|
230
|
+
for message in ["Drafting", "Reviewing", "Complete"]:
|
|
231
|
+
await writable.write(message) # [!code highlight]
|
|
232
|
+
|
|
233
|
+
await writable.close()
|
|
234
|
+
|
|
235
|
+
@wf.workflow
|
|
236
|
+
async def streaming_workflow():
|
|
237
|
+
await write_progress()
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
Read the values from the returned `Run` as they arrive:
|
|
241
|
+
|
|
242
|
+
```python filename="app/api/stream.py"
|
|
243
|
+
from app.workflows.streaming import streaming_workflow
|
|
244
|
+
from vercel import workflow
|
|
245
|
+
|
|
246
|
+
@app.post("/api/stream")
|
|
247
|
+
async def stream_progress():
|
|
248
|
+
run = await workflow.start(streaming_workflow)
|
|
249
|
+
|
|
250
|
+
async for message in run.readable(): # [!code highlight]
|
|
251
|
+
print(message)
|
|
252
|
+
```
|
|
164
253
|
|
|
165
|
-
|
|
254
|
+
Streams are not closed automatically. Close the writable in the last step that writes to it so readers know when the stream is complete.
|
|
166
255
|
|
|
167
256
|
## Next steps
|
|
168
257
|
|
|
@@ -11,7 +11,7 @@ related:
|
|
|
11
11
|
- /docs/api-reference/workflow-errors/workflow-world-error
|
|
12
12
|
---
|
|
13
13
|
|
|
14
|
-
[`setAttributes`](/docs/api-reference/workflow/set-attributes) attaches plaintext string metadata to the current workflow run. These attributes appear in the Workflow CLI and web UI, and you can
|
|
14
|
+
[`setAttributes`](/docs/api-reference/workflow/set-attributes) attaches plaintext string metadata to the current workflow run. These attributes appear in the Workflow CLI and web UI, and you can search and filter runs by them from either the [CLI](#from-the-cli) or the [Analytics API](/docs/api-reference/workflow-runtime/world/analytics).
|
|
15
15
|
|
|
16
16
|
You can also seed any attributes directly when starting a run:
|
|
17
17
|
|
|
@@ -82,6 +82,28 @@ Expanding an `attr_set` event (in the run sidebar or the Events tab) shows the c
|
|
|
82
82
|
|
|
83
83
|
## Searching and filtering by attributes
|
|
84
84
|
|
|
85
|
+
### From the CLI
|
|
86
|
+
|
|
87
|
+
`workflow inspect attributes` lists the keys recorded on this project's runs,
|
|
88
|
+
with how many runs carry each and when it was first and last seen:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
workflow inspect attributes
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Pass one or more `--attribute key=value` pairs to `inspect runs` to list the
|
|
95
|
+
runs carrying them. Repeatable up to 8 times:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
workflow inspect runs --attribute phase=received --status running
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Both require a backend with the analytics read path. `--attribute` is ignored
|
|
102
|
+
with a warning on backends without one, and `inspect attributes` reports that
|
|
103
|
+
it is unavailable.
|
|
104
|
+
|
|
105
|
+
### From the Analytics API
|
|
106
|
+
|
|
85
107
|
The [Analytics API](/docs/api-reference/workflow-runtime/world/analytics) can discover which attribute keys exist and filter run listings by them. The `analytics` namespace is optional on `World`, so feature-detect it before use; it is absent on local, Postgres, and other custom Worlds:
|
|
86
108
|
|
|
87
109
|
```typescript lineNumbers
|
package/docs/whats-new.mdx
CHANGED
|
@@ -33,7 +33,7 @@ The largest change in v5 has no API surface: the runtime does far less work per
|
|
|
33
33
|
|
|
34
34
|
**The runtime avoids waiting on the persistence layer where it can determine that is safe for your workload.** The runtime skips many API calls when they aren't needed, such as requesting the event log on a run's first invocation. Step creation is folded into step execution rather than being its own round trip. The inline loop consumes the event-log delta from the previous step's write instead of re-listing events. Each optimization is gated on specific runtime conditions and can be turned off individually. See [Runtime tuning](/docs/configuration/runtime-tuning).
|
|
35
35
|
|
|
36
|
-
**The workflow VM is kept alive across inline steps.** Within one invocation, a step-driven suspension keeps the live VM and hydrated state, including when hooks are open or created at the same boundary, so the next iteration appends only the newly written events instead of rebuilding the sandbox and replaying the whole log. Step inputs made of plain data or standard built-ins keep this fast path; see [`WORKFLOW_RETAINED_VM`](/docs/configuration/runtime-tuning#workflow_retained_vm).
|
|
36
|
+
**The workflow VM is kept alive across inline steps.** Within one invocation, a step- or attribute-driven suspension keeps the live VM and hydrated state, including when hooks or waits are open or created at the same boundary, so the next iteration appends only the newly written events instead of rebuilding the sandbox and replaying the whole log. Step inputs made of plain data or standard built-ins keep this fast path; see [`WORKFLOW_RETAINED_VM`](/docs/configuration/runtime-tuning#workflow_retained_vm).
|
|
37
37
|
|
|
38
38
|
**Resuming a hook takes one round trip instead of two.** `resumeHook()` writes the `hook_received` event and dispatches the queue message concurrently, with a `(runId, resumeId)` dedup constraint keeping the two writers converging on exactly one event. See [Resilient hook resumption](/docs/changelog/resilient-resume).
|
|
39
39
|
|
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.48",
|
|
4
4
|
"description": "Workflow SDK - Build durable, resilient, and observable workflows",
|
|
5
5
|
"main": "dist/typescript-plugin.cjs",
|
|
6
6
|
"type": "module",
|
|
@@ -58,25 +58,25 @@
|
|
|
58
58
|
}
|
|
59
59
|
},
|
|
60
60
|
"dependencies": {
|
|
61
|
-
"
|
|
62
|
-
"@workflow/
|
|
63
|
-
"@workflow/
|
|
64
|
-
"@workflow/
|
|
65
|
-
"@workflow/errors": "5.0.0-beta.18",
|
|
61
|
+
"@workflow/astro": "5.0.0-beta.48",
|
|
62
|
+
"@workflow/cli": "5.0.0-beta.48",
|
|
63
|
+
"@workflow/core": "5.0.0-beta.48",
|
|
64
|
+
"@workflow/errors": "5.0.0-beta.20",
|
|
66
65
|
"@workflow/typescript-plugin": "5.0.0-beta.5",
|
|
67
|
-
"@workflow/utils": "5.0.0-beta.
|
|
68
|
-
"
|
|
69
|
-
"@workflow/
|
|
70
|
-
"@workflow/
|
|
71
|
-
"@workflow/
|
|
72
|
-
"@workflow/
|
|
73
|
-
"@workflow/
|
|
66
|
+
"@workflow/utils": "5.0.0-beta.10",
|
|
67
|
+
"ms": "2.1.3",
|
|
68
|
+
"@workflow/next": "5.0.0-beta.48",
|
|
69
|
+
"@workflow/nest": "5.0.0-beta.48",
|
|
70
|
+
"@workflow/nitro": "5.0.0-beta.48",
|
|
71
|
+
"@workflow/nuxt": "5.0.0-beta.48",
|
|
72
|
+
"@workflow/sveltekit": "5.0.0-beta.48",
|
|
73
|
+
"@workflow/rollup": "5.0.0-beta.48"
|
|
74
74
|
},
|
|
75
75
|
"devDependencies": {
|
|
76
76
|
"@types/ms": "2.1.0",
|
|
77
77
|
"@types/node": "22.19.0",
|
|
78
|
-
"
|
|
79
|
-
"
|
|
78
|
+
"@workflow/tsconfig": "5.0.0-beta.0",
|
|
79
|
+
"typescript": "^6.0.3"
|
|
80
80
|
},
|
|
81
81
|
"peerDependencies": {
|
|
82
82
|
"@opentelemetry/api": "1"
|