workflow 5.0.0-beta.34 → 5.0.0-beta.35

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.
@@ -1,2 +1,2 @@
1
- export { EntityConflictError, HookConflictError, HookNotFoundError, 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, 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,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,aAAa,EACb,aAAa,EACb,aAAa,EACb,0BAA0B,EAC1B,yBAAyB,EACzB,sBAAsB,EACtB,4BAA4B,EAC5B,wBAAwB,EACxB,oBAAoB,EACpB,kBAAkB,GACnB,MAAM,kBAAkB,CAAC"}
@@ -1,2 +1,2 @@
1
- export { EntityConflictError, HookConflictError, HookNotFoundError, RunExpiredError, RunNotSupportedError, StepNotRegisteredError, ThrottleError, TooEarlyError, WorkflowError, WorkflowNotRegisteredError, WorkflowRunCancelledError, WorkflowRunFailedError, WorkflowRunNotCompletedError, WorkflowRunNotFoundError, WorkflowRuntimeError, WorkflowWorldError, } from '@workflow/errors';
2
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZXJyb3JzLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL2ludGVybmFsL2Vycm9ycy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEVBQ0wsbUJBQW1CLEVBQ25CLGlCQUFpQixFQUNqQixpQkFBaUIsRUFDakIsZUFBZSxFQUNmLG9CQUFvQixFQUNwQixzQkFBc0IsRUFDdEIsYUFBYSxFQUNiLGFBQWEsRUFDYixhQUFhLEVBQ2IsMEJBQTBCLEVBQzFCLHlCQUF5QixFQUN6QixzQkFBc0IsRUFDdEIsNEJBQTRCLEVBQzVCLHdCQUF3QixFQUN4QixvQkFBb0IsRUFDcEIsa0JBQWtCLEdBQ25CLE1BQU0sa0JBQWtCLENBQUMiLCJzb3VyY2VzQ29udGVudCI6WyJleHBvcnQge1xuICBFbnRpdHlDb25mbGljdEVycm9yLFxuICBIb29rQ29uZmxpY3RFcnJvcixcbiAgSG9va05vdEZvdW5kRXJyb3IsXG4gIFJ1bkV4cGlyZWRFcnJvcixcbiAgUnVuTm90U3VwcG9ydGVkRXJyb3IsXG4gIFN0ZXBOb3RSZWdpc3RlcmVkRXJyb3IsXG4gIFRocm90dGxlRXJyb3IsXG4gIFRvb0Vhcmx5RXJyb3IsXG4gIFdvcmtmbG93RXJyb3IsXG4gIFdvcmtmbG93Tm90UmVnaXN0ZXJlZEVycm9yLFxuICBXb3JrZmxvd1J1bkNhbmNlbGxlZEVycm9yLFxuICBXb3JrZmxvd1J1bkZhaWxlZEVycm9yLFxuICBXb3JrZmxvd1J1bk5vdENvbXBsZXRlZEVycm9yLFxuICBXb3JrZmxvd1J1bk5vdEZvdW5kRXJyb3IsXG4gIFdvcmtmbG93UnVudGltZUVycm9yLFxuICBXb3JrZmxvd1dvcmxkRXJyb3IsXG59IGZyb20gJ0B3b3JrZmxvdy9lcnJvcnMnO1xuIl19
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,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZXJyb3JzLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL2ludGVybmFsL2Vycm9ycy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEVBQ0wsbUJBQW1CLEVBQ25CLGlCQUFpQixFQUNqQixpQkFBaUIsRUFDakIsdUJBQXVCLEVBQ3ZCLGVBQWUsRUFDZixvQkFBb0IsRUFDcEIsc0JBQXNCLEVBQ3RCLGFBQWEsRUFDYixhQUFhLEVBQ2IsYUFBYSxFQUNiLDBCQUEwQixFQUMxQix5QkFBeUIsRUFDekIsc0JBQXNCLEVBQ3RCLDRCQUE0QixFQUM1Qix3QkFBd0IsRUFDeEIsb0JBQW9CLEVBQ3BCLGtCQUFrQixHQUNuQixNQUFNLGtCQUFrQixDQUFDIiwic291cmNlc0NvbnRlbnQiOlsiZXhwb3J0IHtcbiAgRW50aXR5Q29uZmxpY3RFcnJvcixcbiAgSG9va0NvbmZsaWN0RXJyb3IsXG4gIEhvb2tOb3RGb3VuZEVycm9yLFxuICBQcmVjb25kaXRpb25GYWlsZWRFcnJvcixcbiAgUnVuRXhwaXJlZEVycm9yLFxuICBSdW5Ob3RTdXBwb3J0ZWRFcnJvcixcbiAgU3RlcE5vdFJlZ2lzdGVyZWRFcnJvcixcbiAgVGhyb3R0bGVFcnJvcixcbiAgVG9vRWFybHlFcnJvcixcbiAgV29ya2Zsb3dFcnJvcixcbiAgV29ya2Zsb3dOb3RSZWdpc3RlcmVkRXJyb3IsXG4gIFdvcmtmbG93UnVuQ2FuY2VsbGVkRXJyb3IsXG4gIFdvcmtmbG93UnVuRmFpbGVkRXJyb3IsXG4gIFdvcmtmbG93UnVuTm90Q29tcGxldGVkRXJyb3IsXG4gIFdvcmtmbG93UnVuTm90Rm91bmRFcnJvcixcbiAgV29ya2Zsb3dSdW50aW1lRXJyb3IsXG4gIFdvcmtmbG93V29ybGRFcnJvcixcbn0gZnJvbSAnQHdvcmtmbG93L2Vycm9ycyc7XG4iXX0=
@@ -79,6 +79,9 @@ All errors extend [`WorkflowError`](/docs/api-reference/workflow-errors/workflow
79
79
  <Card href="/docs/api-reference/workflow-errors/entity-conflict-error" title="EntityConflictError">
80
80
  Thrown when a storage operation conflicts with the current entity state.
81
81
  </Card>
82
+ <Card href="/docs/api-reference/workflow-errors/precondition-failed-error" title="PreconditionFailedError">
83
+ Thrown when an event creation is rejected because the client's event-log snapshot is stale.
84
+ </Card>
82
85
  <Card href="/docs/api-reference/workflow-errors/too-early-error" title="TooEarlyError">
83
86
  Thrown when a request is made before the system is ready to process it.
84
87
  </Card>
@@ -14,6 +14,7 @@
14
14
  "workflow-world-error",
15
15
  "throttle-error",
16
16
  "entity-conflict-error",
17
+ "precondition-failed-error",
17
18
  "run-expired-error",
18
19
  "run-not-supported-error",
19
20
  "too-early-error"
@@ -0,0 +1,62 @@
1
+ ---
2
+ title: PreconditionFailedError
3
+ description: Thrown when an event creation is rejected because the client's event-log snapshot is stale.
4
+ type: reference
5
+ summary: Catch PreconditionFailedError when a world rejects an event creation made from a stale event-log snapshot.
6
+ related:
7
+ - /docs/api-reference/workflow-errors/workflow-world-error
8
+ - /docs/api-reference/workflow-errors/entity-conflict-error
9
+ ---
10
+
11
+ `PreconditionFailedError` is thrown by world implementations when an event creation is rejected because the client's event-log snapshot is stale — a newer out-of-band event (such as a received hook or a completed step) was recorded after the snapshot the client replayed from. It corresponds to HTTP 412 Precondition Failed semantics.
12
+
13
+ This only occurs when the optimistic-concurrency guard is enabled via `WORKFLOW_PRECONDITION_GUARD=1` (see [Runtime Tuning](/docs/configuration/runtime-tuning)); event creations that carry no snapshot are never rejected with this error.
14
+
15
+ <Callout>
16
+ The Workflow runtime handles this error automatically: it reloads the event log and retries, ultimately re-enqueueing the run for a fresh replay if it cannot catch up. You will only encounter it when interacting with world storage APIs directly.
17
+ </Callout>
18
+
19
+ ```typescript lineNumbers
20
+ import { PreconditionFailedError } from "workflow/errors"
21
+ declare const world: { events: { create(...args: any[]): Promise<any> } }; // @setup
22
+ declare const runId: string; // @setup
23
+ declare const event: any; // @setup
24
+
25
+ try {
26
+ await world.events.create(runId, event);
27
+ } catch (error) {
28
+ if (PreconditionFailedError.is(error)) { // [!code highlight]
29
+ console.log("Snapshot is stale; reload the event log and retry");
30
+ }
31
+ }
32
+ ```
33
+
34
+ ## API Signature
35
+
36
+ ### Properties
37
+
38
+ <TSDoc
39
+ definition={`
40
+ interface PreconditionFailedError {
41
+ /** Delay in seconds before the operation should be retried. Present when the server sends a Retry-After header. */
42
+ retryAfter?: number;
43
+ /** The error message. */
44
+ message: string;
45
+ }
46
+ export default PreconditionFailedError;`}
47
+ />
48
+
49
+ ### Static Methods
50
+
51
+ #### `PreconditionFailedError.is(value)`
52
+
53
+ Type-safe check for `PreconditionFailedError` instances. Preferred over `instanceof` because it works across module boundaries and VM contexts.
54
+
55
+ ```typescript
56
+ import { PreconditionFailedError } from "workflow/errors"
57
+ declare const error: unknown; // @setup
58
+
59
+ if (PreconditionFailedError.is(error)) {
60
+ // error is typed as PreconditionFailedError
61
+ }
62
+ ```
@@ -38,6 +38,13 @@ For example, a workflow can run a 10-minute inline step even with `WORKFLOW_REPL
38
38
  - Default: `3`
39
39
  - Recovery replays before replay divergence is recorded as corruption.
40
40
 
41
+ ### `WORKFLOW_PRECONDITION_GUARD`
42
+
43
+ - Default: disabled
44
+ - Set `1` to enable an optimistic-concurrency guard for event creation: replay-context event creations send a `stateUpdatedAt` snapshot timestamp, and a backend that supports the guard rejects a creation with 412 ([`PreconditionFailedError`](/docs/api-reference/workflow-errors/precondition-failed-error)) when a newer out-of-band event (a received hook or a completed step) was recorded after that snapshot.
45
+ - On rejection the runtime reloads the event log and retries, falling back to a queue re-invocation with a fresh replay if it cannot catch up.
46
+ - Backends that do not support the guard ignore the snapshot.
47
+
41
48
  ## Inline execution
42
49
 
43
50
  ### `WORKFLOW_V2_TIMEOUT_MS`
@@ -82,9 +82,9 @@ The Local World is the default outside Vercel and is intended for development.
82
82
 
83
83
  ### `recoverActiveRuns`
84
84
 
85
- - Environment variable: none
85
+ - Environment variable: `WORKFLOW_LOCAL_RECOVER_ACTIVE_RUNS`
86
86
  - Default: `true`
87
- - Re-enqueues pending and running local runs when the World starts.
87
+ - Re-enqueues pending and running local runs when the World starts. Set the environment variable to `0` or `false` to skip recovery; the factory option wins when both are set.
88
88
 
89
89
  ### `tag`
90
90
 
@@ -201,8 +201,7 @@ Platform-provided values such as `VERCEL_DEPLOYMENT_ID`, `VERCEL_PROJECT_ID`, an
201
201
  - Default: disabled
202
202
  - Set `1` to serialize orchestrator (flow) invocations per run: each run's replays get their own queue topic and the flow trigger is generated with `maxConcurrency: 1`. Inline step executions get per-step topics and keep full parallelism.
203
203
  - Read at **both build time and runtime** — set it as a project-level environment variable so the generated trigger and the runtime queue routing agree.
204
- - Also enabled by `WORKFLOW_SAFE_MODE=1` when not set explicitly.
205
- - Costs: flow-route push deliveries under `maxConcurrency` are billed at 2x units, queue observability sees one topic per run, and deliveries can queue behind the per-run slot — including hook resumes, aborts, and run-timeout enforcement, which are delayed while a replay is in flight. See [Vercel World](/docs/deploying/world/vercel-world#workflow_sequential_replays) for details.
204
+ - Routing each run through a dedicated `maxConcurrency: 1` topic might lead to higher queue performance overhead. See [Vercel World](/docs/deploying/world/vercel-world#workflow_sequential_replays) for details.
206
205
 
207
206
  ### `VERCEL_WORKFLOW_SERVER_URL`
208
207
 
@@ -62,6 +62,10 @@ Maximum number of concurrent queue message handlers. Default: `1000`
62
62
 
63
63
  Maximum number of seconds a local queue message can stay hidden before the handler rechecks the run. Default: unlimited.
64
64
 
65
+ ### `WORKFLOW_LOCAL_RECOVER_ACTIVE_RUNS`
66
+
67
+ Whether pending and running runs found in the data directory are re-enqueued when the world starts. Set to `0` or `false` to skip recovery and leave stale runs untouched. Default: `true`
68
+
65
69
  ### `WORKFLOW_STREAM_FLUSH_INTERVAL_MS`
66
70
 
67
71
  Flush interval, in milliseconds, for buffered stream writes. Default: `10`.
@@ -78,7 +82,7 @@ export default createWorld({
78
82
  port: 5173,
79
83
  // baseUrl overrides port if set
80
84
  baseUrl: "https://local.example.com:3000",
81
- recoverActiveRuns: true,
85
+ recoverActiveRuns: true, // overrides WORKFLOW_LOCAL_RECOVER_ACTIVE_RUNS
82
86
  streamFlushIntervalMs: 10, // overrides WORKFLOW_STREAM_FLUSH_INTERVAL_MS
83
87
  });
84
88
  ```
@@ -171,21 +171,16 @@ Custom workflow-server URL for direct runtime requests, or for the proxy to forw
171
171
 
172
172
  ### `WORKFLOW_SEQUENTIAL_REPLAYS`
173
173
 
174
- Set `WORKFLOW_SEQUENTIAL_REPLAYS=1` to guarantee that **at most one orchestrator (flow) invocation runs at a time per workflow run**. This behavior is off by default; without it, the runtime relies on idempotency and the event log to tolerate concurrent flow invocations of the same run. It is also enabled by `WORKFLOW_SAFE_MODE=1` when `WORKFLOW_SEQUENTIAL_REPLAYS` is not set explicitly.
174
+ Set `WORKFLOW_SEQUENTIAL_REPLAYS=1` to guarantee that **at most one orchestrator (flow) invocation runs at a time per workflow run**. This behavior is off by default; without it, the runtime relies on idempotency and the event log to tolerate concurrent flow invocations of the same run.
175
175
 
176
176
  When enabled, each run's orchestrator messages are given their own queue topic and the flow trigger is configured with `maxConcurrency: 1`, so [Vercel Queues](https://vercel.com/docs/queues) processes replays for a given run strictly one at a time. Step executions (which ride the flow topic in the combined handler model) get a per-step topic, so steps keep full parallelism.
177
177
 
178
178
  <Callout type="warn">
179
179
  This variable is read at **both build time and runtime**, so it must be set as a project-level environment variable that applies to your build and your deployed functions. Setting it for only one will produce an inconsistent configuration. The same applies to framework integrations that write their own queue trigger configuration instead of using `getWorkflowQueueTrigger()` from `@workflow/builders`: they only get the runtime half (per-run topics) unless they also emit `maxConcurrency: 1` on their flow trigger.
180
-
181
- Enabling sequential replays has a cost. Per [Vercel Queues pricing](https://vercel.com/docs/queues/pricing), push deliveries under `maxConcurrency` are billed at **2x units** for that operation, so every flow-route delivery costs double while this is enabled. It also creates one queue topic per run, which increases the number of distinct queues surfaced in queue observability, and each flow invocation waits for a per-run concurrency slot before delivery, which can add queueing latency. Leave it off unless you specifically need the per-run serialization guarantee.
182
-
183
-
184
- While a replay holds a run's slot — including time spent executing steps inline — other wake messages for that run (hook resumes, aborts and cancellations, and run-timeout enforcement) wait for the slot. Expect aborts and timeouts to be delayed by up to the duration of the longest single invocation.
185
-
186
- The guarantee covers messages sent by the Workflow SDK itself. External producers that compute a flow topic name directly (rather than enqueueing through the SDK) still deliver, but bypass the per-run serialization slot.
187
180
  </Callout>
188
181
 
182
+ Because it routes each run's flow invocations through a dedicated `maxConcurrency: 1` queue topic, enabling this might lead to higher queue performance overhead.
183
+
189
184
  ### `VERCEL_QUEUE_MAX_DELAY_SECONDS`
190
185
 
191
186
  Maximum delay, in seconds, that Workflow uses for one Vercel Queues continuation message when implementing `sleep()`. If a workflow sleeps longer than this, the runtime schedules another continuation message when the first one fires, repeating until the sleep's target time is reached. Default: `82800` (23 hours).
@@ -9,6 +9,7 @@ related:
9
9
  ---
10
10
 
11
11
  import { Next, Nitro, SvelteKit, Nuxt, Hono, Bun, AstroDark, AstroLight, TanStack, Vite, Express, Nest, Fastify, Python } from "@/app/[lang]/(home)/components/frameworks";
12
+ import { SiReactrouter } from "@icons-pack/react-simple-icons";
12
13
 
13
14
  <Cards>
14
15
  <Card href="/docs/getting-started/next">
@@ -20,6 +21,12 @@ import { Next, Nitro, SvelteKit, Nuxt, Hono, Bun, AstroDark, AstroLight, TanStac
20
21
  <span className="font-medium">Vite</span>
21
22
  </div>
22
23
  </Card>
24
+ <Card href="/docs/getting-started/react-router">
25
+ <div className="flex flex-col items-center justify-center gap-2">
26
+ <SiReactrouter className="size-16" />
27
+ <span className="font-medium">React Router</span>
28
+ </div>
29
+ </Card>
23
30
  <Card href="/docs/getting-started/astro">
24
31
  <div className="flex flex-col items-center justify-center gap-2">
25
32
  <AstroLight className="size-16 dark:hidden" />
@@ -2,6 +2,7 @@
2
2
  "title": "Getting Started",
3
3
  "pages": [
4
4
  "next",
5
+ "react-router",
5
6
  "astro",
6
7
  "express",
7
8
  "fastify",
@@ -11,7 +11,7 @@ related:
11
11
  ---
12
12
 
13
13
  <CopyPrompt
14
- text="In this Python project, run `pip install vercel`. Add `vercel.json` with `experimentalServices.ai_content_workflow.type = &quot;worker&quot;`, `entrypoint = &quot;app/workflows/ai_content_workflow.py&quot;`, and `topics = [&quot;__wkf_*&quot;]`. Create `app/workflow.py` with `from vercel import workflow` and `wf = workflow.Workflows()`. Create `app/workflows/ai_content_workflow.py` importing `wf`, define `@wf.workflow async def ai_content_workflow(*, topic: str)`, and call step functions such as `generate_draft` and `summarize_draft`. Mark step functions with `@wf.step`, use `await workflow.sleep(&quot;7 days&quot;)` for durable delays where needed, and use a `workflow.BaseHook` Pydantic model plus `.wait(token=...)` and `.resume(token)` for external approval events. Verify the worker entrypoint and route names match the project."
14
+ text="In this Python project, add `requires-python = &quot;&gt;=3.12&quot;` and `dependencies = [&quot;vercel&quot;]` under `[project]` in `pyproject.toml`. Add `[[tool.vercel.workflows]]` with `entrypoint = &quot;app.workflows:wf&quot;`. Create `app/workflow.py` with `from vercel import workflow` and `wf = workflow.Workflows()`. Create `app/workflows/ai_content_workflow.py` importing `wf`, define `@wf.workflow async def ai_content_workflow(*, topic: str)`, and call step functions such as `generate_draft` and `summarize_draft`. Export `wf` from `app/workflows/__init__.py` and import the workflow module so its definitions are registered. Mark step functions with `@wf.step`, use `await workflow.sleep(&quot;7 days&quot;)` for durable delays where needed, and use a `workflow.BaseHook` Pydantic model plus `.wait(token=...)` and `.resume(token)` for external approval events. Verify the workflow entrypoint uses the `module:object` format and points to the exported `Workflows` registry."
15
15
  />
16
16
 
17
17
  <Callout type="warn">
@@ -22,26 +22,19 @@ You can build durable workflows in Python using the [`vercel` Python SDK](https:
22
22
 
23
23
  ## Getting Started
24
24
 
25
- Install the `vercel` package:
25
+ Add the `vercel` package and workflow entrypoint to `pyproject.toml`:
26
26
 
27
- ```bash filename="Terminal"
28
- pip install vercel
29
- ```
30
-
31
- Configure `experimentalServices` in your `vercel.json`:
27
+ ```toml filename="pyproject.toml"
28
+ [project]
29
+ requires-python = ">=3.12"
30
+ dependencies = ["vercel"]
32
31
 
33
- ```json filename="vercel.json"
34
- {
35
- "experimentalServices": {
36
- "ai_content_workflow": {
37
- "type": "worker",
38
- "entrypoint": "app/workflows/ai_content_workflow.py",
39
- "topics": ["__wkf_*"]
40
- }
41
- }
42
- }
32
+ [[tool.vercel.workflows]]
33
+ entrypoint = "app.workflows:wf"
43
34
  ```
44
35
 
36
+ The workflow `entrypoint` uses the `module:object` format and points to the exported `Workflows` registry.
37
+
45
38
  ## Workflows
46
39
 
47
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:
@@ -66,6 +59,15 @@ async def ai_content_workflow(*, topic: str):
66
59
  }
67
60
  ```
68
61
 
62
+ Export the registry from the workflow package and import the module containing your workflow so its definitions are registered:
63
+
64
+ ```python filename="app/workflows/__init__.py"
65
+ from app.workflow import wf
66
+ from app.workflows import ai_content_workflow
67
+
68
+ __all__ = ["ai_content_workflow", "wf"]
69
+ ```
70
+
69
71
  Under the hood, the workflow compiles into a route that orchestrates execution. All inputs and outputs are recorded in an event log. If a deploy or crash happens, the system replays execution deterministically from where it stopped.
70
72
 
71
73
  ## Steps
@@ -0,0 +1,33 @@
1
+ ---
2
+ title: React Router
3
+ description: Run durable workflows in a React Router framework-mode app using Nitro.
4
+ type: overview
5
+ summary: Choose your React Router version and connect React Router, Nitro, and Workflow SDK.
6
+ related:
7
+ - /docs/getting-started/nitro
8
+ - /docs/getting-started/vite
9
+ - /docs/foundations/workflows-and-steps
10
+ ---
11
+
12
+ React Router framework mode builds the browser application and its server-rendering code, but it still needs a server to receive requests. [Nitro](https://v3.nitro.build) provides that server. Workflow SDK integrates with Nitro to add the durable workflow routes and build artifacts.
13
+
14
+ The three pieces share one Vite build:
15
+
16
+ 1. **React Router** builds your routes, loaders, actions, and browser assets.
17
+ 2. **Nitro** runs the React Router request handler and any routes in `server/routes`.
18
+ 3. **Workflow SDK** finds files with `"use workflow"` and `"use step"`, then adds its runtime routes to Nitro.
19
+
20
+ Choose the guide that matches your React Router major version:
21
+
22
+ <AutoCards />
23
+
24
+ <Callout>
25
+ These guides require **Nitro v3**. Nitro v2 does not provide the Vite
26
+ environment integration used by this setup.
27
+ </Callout>
28
+
29
+ ## What the bridge does
30
+
31
+ The setup adds a small `server/ssr.ts` file. It turns React Router's generated server build into a standard Fetch API handler that Nitro can run. The Vite config then points Nitro's server and public output at the same `build` directory React Router uses.
32
+
33
+ This is configuration in your application, not a separate React Router adapter. Your React Router routes remain React Router routes, while Nitro owns the HTTP server and Workflow SDK uses Nitro's lifecycle and routing.
@@ -0,0 +1,5 @@
1
+ {
2
+ "title": "React Router",
3
+ "pages": ["v7", "v8"],
4
+ "defaultOpen": true
5
+ }
@@ -0,0 +1,237 @@
1
+ ---
2
+ title: React Router v7
3
+ description: Add durable workflows to a React Router v7 framework-mode app using Nitro v3.
4
+ type: guide
5
+ summary: Enable the Vite Environment API and configure React Router v7, Nitro v3, and Workflow SDK.
6
+ prerequisites:
7
+ - /docs/getting-started/react-router
8
+ related:
9
+ - /docs/getting-started/nitro
10
+ - /docs/foundations/workflows-and-steps
11
+ ---
12
+
13
+ This guide starts with an existing React Router v7 framework-mode app. It is verified with v7.18.1; if your config does not recognize `v8_viteEnvironmentApi`, update to the latest v7 release.
14
+
15
+ <Steps>
16
+
17
+ <Step>
18
+
19
+ ## Install Nitro and Workflow SDK
20
+
21
+ <Tabs items={["npm", "pnpm", "bun", "yarn"]} defaultValue="pnpm">
22
+
23
+ <Tab value="npm">
24
+
25
+ ```bash
26
+ npm install nitro workflow
27
+ ```
28
+
29
+ </Tab>
30
+
31
+ <Tab value="pnpm">
32
+
33
+ ```bash
34
+ pnpm add nitro workflow
35
+ ```
36
+
37
+ </Tab>
38
+
39
+ <Tab value="bun">
40
+
41
+ ```bash
42
+ bun add nitro workflow
43
+ ```
44
+
45
+ </Tab>
46
+
47
+ <Tab value="yarn">
48
+
49
+ ```bash
50
+ yarn add nitro workflow
51
+ ```
52
+
53
+ </Tab>
54
+
55
+ </Tabs>
56
+
57
+ This integration requires Nitro v3.
58
+
59
+ </Step>
60
+
61
+ <Step>
62
+
63
+ ## Enable the Vite Environment API
64
+
65
+ React Router v7 keeps the Vite Environment API behind a future flag. Enable the required flag and set an explicit build directory:
66
+
67
+ ```typescript title="react-router.config.ts" lineNumbers
68
+ import type { Config } from "@react-router/dev/config";
69
+
70
+ export default {
71
+ ssr: true,
72
+ buildDirectory: "build", // [!code highlight]
73
+ future: {
74
+ v8_viteEnvironmentApi: true, // [!code highlight]
75
+ },
76
+ } satisfies Config;
77
+ ```
78
+
79
+ </Step>
80
+
81
+ <Step>
82
+
83
+ ## Create the React Router server handler
84
+
85
+ Create `server/ssr.ts`:
86
+
87
+ ```typescript title="server/ssr.ts" lineNumbers
88
+ import { createRequestHandler } from "react-router";
89
+
90
+ export default {
91
+ fetch: createRequestHandler(
92
+ () => import("virtual:react-router/server-build"),
93
+ import.meta.env.MODE,
94
+ ),
95
+ };
96
+ ```
97
+
98
+ This adapts React Router's generated server build to the Fetch API handler Nitro expects.
99
+
100
+ </Step>
101
+
102
+ <Step>
103
+
104
+ ## Configure Vite
105
+
106
+ Update `vite.config.ts`:
107
+
108
+ ```typescript title="vite.config.ts" lineNumbers
109
+ import { reactRouter } from "@react-router/dev/vite";
110
+ import { nitro } from "nitro/vite";
111
+ import { defineConfig } from "vite";
112
+ import { workflow } from "workflow/vite";
113
+ import reactRouterConfig from "./react-router.config";
114
+
115
+ export default defineConfig({
116
+ plugins: [
117
+ reactRouter(),
118
+ nitro({
119
+ serverDir: "./server",
120
+ output: {
121
+ dir: reactRouterConfig.buildDirectory,
122
+ serverDir: `${reactRouterConfig.buildDirectory}/server`,
123
+ publicDir: `${reactRouterConfig.buildDirectory}/client`,
124
+ },
125
+ }),
126
+ workflow({ dirs: ["workflows"] }),
127
+ ],
128
+ environments: {
129
+ ssr: {
130
+ build: {
131
+ rollupOptions: {
132
+ input: "./server/ssr.ts",
133
+ },
134
+ },
135
+ },
136
+ },
137
+ });
138
+ ```
139
+
140
+ Keep `dirs: ["workflows"]` so subsequent builds do not scan generated files under `build`. Place `reactRouter()` before `nitro()` in the plugin array.
141
+
142
+ </Step>
143
+
144
+ <Step>
145
+
146
+ ## Create a workflow
147
+
148
+ Create `workflows/greeting.ts`:
149
+
150
+ ```typescript title="workflows/greeting.ts" lineNumbers
151
+ export async function greetingWorkflow(name: string) {
152
+ "use workflow";
153
+
154
+ return greet(name);
155
+ }
156
+
157
+ async function greet(name: string) {
158
+ "use step";
159
+
160
+ return `Hello, ${name}!`;
161
+ }
162
+ ```
163
+
164
+ </Step>
165
+
166
+ <Step>
167
+
168
+ ## Start the workflow from a Nitro route
169
+
170
+ Create `server/routes/api/greeting.post.ts`:
171
+
172
+ ```typescript title="server/routes/api/greeting.post.ts" lineNumbers
173
+ import { defineHandler } from "nitro";
174
+ import { start } from "workflow/api";
175
+ import { greetingWorkflow } from "../../../workflows/greeting";
176
+
177
+ export default defineHandler(async (event) => {
178
+ const { name } = (await event.req.json()) as { name: string };
179
+ const run = await start(greetingWorkflow, [name]);
180
+
181
+ return { runId: run.runId };
182
+ });
183
+ ```
184
+
185
+ React Router continues to handle your application routes. Nitro handles this server route at `POST /api/greeting`, as well as Workflow SDK's internal routes.
186
+
187
+ </Step>
188
+
189
+ <Step>
190
+
191
+ ## Run the app
192
+
193
+ Start the development server:
194
+
195
+ ```bash
196
+ pnpm vite dev
197
+ ```
198
+
199
+ Then start a workflow:
200
+
201
+ ```bash
202
+ curl -X POST \
203
+ -H "content-type: application/json" \
204
+ -d '{"name":"Workflow"}' \
205
+ http://localhost:3000/api/greeting
206
+ ```
207
+
208
+ Build and start the production server:
209
+
210
+ ```bash
211
+ pnpm vite build
212
+ node ./build/server/index.mjs
213
+ ```
214
+
215
+ You can inspect local runs with `pnpm workflow web`.
216
+
217
+ </Step>
218
+
219
+ </Steps>
220
+
221
+ ## Troubleshooting
222
+
223
+ ### Vite reports an invalid SSR input or `path.replace is not a function`
224
+
225
+ Set `future.v8_viteEnvironmentApi` to `true` in `react-router.config.ts`.
226
+
227
+ ### React Router pages return 404
228
+
229
+ Check that the `ssr` environment input points to `./server/ssr.ts`.
230
+
231
+ ### A second build tries to compile files under `build/server`
232
+
233
+ Use `workflow({ dirs: ["workflows"] })`, remove the existing `build` directory once, and rebuild.
234
+
235
+ ### `vite build` finishes output but does not exit
236
+
237
+ Use `workflow@5.0.0-beta.33` or later with Nitro v3.
@@ -0,0 +1,232 @@
1
+ ---
2
+ title: React Router v8
3
+ description: Add durable workflows to a React Router v8 framework-mode app using Nitro v3.
4
+ type: guide
5
+ summary: Configure React Router v8, Nitro v3, and Workflow SDK in one Vite build.
6
+ prerequisites:
7
+ - /docs/getting-started/react-router
8
+ related:
9
+ - /docs/getting-started/nitro
10
+ - /docs/foundations/workflows-and-steps
11
+ ---
12
+
13
+ This guide starts with an existing React Router v8 framework-mode app.
14
+
15
+ <Steps>
16
+
17
+ <Step>
18
+
19
+ ## Install Nitro and Workflow SDK
20
+
21
+ <Tabs items={["npm", "pnpm", "bun", "yarn"]} defaultValue="pnpm">
22
+
23
+ <Tab value="npm">
24
+
25
+ ```bash
26
+ npm install nitro workflow
27
+ ```
28
+
29
+ </Tab>
30
+
31
+ <Tab value="pnpm">
32
+
33
+ ```bash
34
+ pnpm add nitro workflow
35
+ ```
36
+
37
+ </Tab>
38
+
39
+ <Tab value="bun">
40
+
41
+ ```bash
42
+ bun add nitro workflow
43
+ ```
44
+
45
+ </Tab>
46
+
47
+ <Tab value="yarn">
48
+
49
+ ```bash
50
+ yarn add nitro workflow
51
+ ```
52
+
53
+ </Tab>
54
+
55
+ </Tabs>
56
+
57
+ This integration requires Nitro v3.
58
+
59
+ </Step>
60
+
61
+ <Step>
62
+
63
+ ## Use a shared build directory
64
+
65
+ Set an explicit build directory in your React Router config:
66
+
67
+ ```typescript title="react-router.config.ts" lineNumbers
68
+ import type { Config } from "@react-router/dev/config";
69
+
70
+ export default {
71
+ ssr: true,
72
+ buildDirectory: "build", // [!code highlight]
73
+ } satisfies Config;
74
+ ```
75
+
76
+ React Router will place browser assets in `build/client`. Nitro will place the runnable server in `build/server`.
77
+
78
+ </Step>
79
+
80
+ <Step>
81
+
82
+ ## Create the React Router server handler
83
+
84
+ Create `server/ssr.ts`:
85
+
86
+ ```typescript title="server/ssr.ts" lineNumbers
87
+ import { createRequestHandler } from "react-router";
88
+
89
+ export default {
90
+ fetch: createRequestHandler(
91
+ () => import("virtual:react-router/server-build"),
92
+ import.meta.env.MODE,
93
+ ),
94
+ };
95
+ ```
96
+
97
+ This adapts React Router's generated server build to the Fetch API handler Nitro expects.
98
+
99
+ </Step>
100
+
101
+ <Step>
102
+
103
+ ## Configure Vite
104
+
105
+ Update `vite.config.ts`:
106
+
107
+ ```typescript title="vite.config.ts" lineNumbers
108
+ import { reactRouter } from "@react-router/dev/vite";
109
+ import { nitro } from "nitro/vite";
110
+ import { defineConfig } from "vite";
111
+ import { workflow } from "workflow/vite";
112
+ import reactRouterConfig from "./react-router.config";
113
+
114
+ export default defineConfig({
115
+ plugins: [
116
+ reactRouter(),
117
+ nitro({
118
+ serverDir: "./server",
119
+ output: {
120
+ dir: reactRouterConfig.buildDirectory,
121
+ serverDir: `${reactRouterConfig.buildDirectory}/server`,
122
+ publicDir: `${reactRouterConfig.buildDirectory}/client`,
123
+ },
124
+ }),
125
+ workflow({ dirs: ["workflows"] }),
126
+ ],
127
+ environments: {
128
+ ssr: {
129
+ build: {
130
+ rollupOptions: {
131
+ input: "./server/ssr.ts",
132
+ },
133
+ },
134
+ },
135
+ },
136
+ });
137
+ ```
138
+
139
+ Keep `dirs: ["workflows"]` so subsequent builds do not scan generated files under `build`. Place `reactRouter()` before `nitro()` in the plugin array.
140
+
141
+ </Step>
142
+
143
+ <Step>
144
+
145
+ ## Create a workflow
146
+
147
+ Create `workflows/greeting.ts`:
148
+
149
+ ```typescript title="workflows/greeting.ts" lineNumbers
150
+ export async function greetingWorkflow(name: string) {
151
+ "use workflow";
152
+
153
+ return greet(name);
154
+ }
155
+
156
+ async function greet(name: string) {
157
+ "use step";
158
+
159
+ return `Hello, ${name}!`;
160
+ }
161
+ ```
162
+
163
+ </Step>
164
+
165
+ <Step>
166
+
167
+ ## Start the workflow from a Nitro route
168
+
169
+ Create `server/routes/api/greeting.post.ts`:
170
+
171
+ ```typescript title="server/routes/api/greeting.post.ts" lineNumbers
172
+ import { defineHandler } from "nitro";
173
+ import { start } from "workflow/api";
174
+ import { greetingWorkflow } from "../../../workflows/greeting";
175
+
176
+ export default defineHandler(async (event) => {
177
+ const { name } = (await event.req.json()) as { name: string };
178
+ const run = await start(greetingWorkflow, [name]);
179
+
180
+ return { runId: run.runId };
181
+ });
182
+ ```
183
+
184
+ React Router continues to handle your application routes. Nitro handles this server route at `POST /api/greeting`, as well as Workflow SDK's internal routes.
185
+
186
+ </Step>
187
+
188
+ <Step>
189
+
190
+ ## Run the app
191
+
192
+ Start the development server:
193
+
194
+ ```bash
195
+ pnpm vite dev
196
+ ```
197
+
198
+ Then start a workflow:
199
+
200
+ ```bash
201
+ curl -X POST \
202
+ -H "content-type: application/json" \
203
+ -d '{"name":"Workflow"}' \
204
+ http://localhost:3000/api/greeting
205
+ ```
206
+
207
+ Build and start the production server:
208
+
209
+ ```bash
210
+ pnpm vite build
211
+ node ./build/server/index.mjs
212
+ ```
213
+
214
+ You can inspect local runs with `pnpm workflow web`.
215
+
216
+ </Step>
217
+
218
+ </Steps>
219
+
220
+ ## Troubleshooting
221
+
222
+ ### React Router pages return 404
223
+
224
+ Check that the `ssr` environment input points to `./server/ssr.ts`.
225
+
226
+ ### A second build tries to compile files under `build/server`
227
+
228
+ Use `workflow({ dirs: ["workflows"] })`, remove the existing `build` directory once, and rebuild.
229
+
230
+ ### `vite build` finishes output but does not exit
231
+
232
+ Use `workflow@5.0.0-beta.33` or later with Nitro v3.
@@ -414,7 +414,7 @@ const flowTriggers = [getWorkflowQueueTrigger()];
414
414
  const stepTriggers = [STEP_QUEUE_TRIGGER];
415
415
  ```
416
416
 
417
- If your integration constructs the flow trigger object itself instead of calling `getWorkflowQueueTrigger()`, it must add `maxConcurrency: 1` to that trigger when sequential replays are enabled at build time (`WORKFLOW_SEQUENTIAL_REPLAYS=1`, or `WORKFLOW_SAFE_MODE=1` when the specific variable is unset — the exported `isSequentialReplaysEnabled()` helper implements this check). The runtime half of the feature (per-run queue topics) activates from the environment variable alone — without the trigger half, those per-run topics are not serialized and the setting only adds queue-topic cardinality.
417
+ If your integration constructs the flow trigger object itself instead of calling `getWorkflowQueueTrigger()`, it must add `maxConcurrency: 1` to that trigger when sequential replays are enabled at build time (`WORKFLOW_SEQUENTIAL_REPLAYS=1` — the exported `isSequentialReplaysEnabled()` helper implements this check). The runtime half of the feature (per-run queue topics) activates from the environment variable alone — without the trigger half, those per-run topics are not serialized and the setting only adds queue-topic cardinality.
418
418
 
419
419
 
420
420
  ### Custom implementations
@@ -57,6 +57,15 @@ export async function cleanupAttributes() {
57
57
 
58
58
  Attribute keys must be 1-256 characters, values must be strings up to 256 bytes, and each run can have up to 64 attributes. Keys that start with `$` are reserved for framework and library code.
59
59
 
60
+ ## Reserved keys
61
+
62
+ When `start()` is called from inside a running workflow or step, the new run is automatically tagged with two reserved attributes:
63
+
64
+ - `$parentRunId`: the run that started it.
65
+ - `$rootRunId`: the root of the chain. It is inherited, so every run in a daisy chain or fan-out shares one root id.
66
+
67
+ Top-level runs (started outside any workflow or step) are not tagged.
68
+
60
69
  ## Viewing attributes
61
70
 
62
71
  The run details panel in the observability UI shows the run's current attributes as key-value rows. Reserved `$`-prefixed keys are marked with a badge and sorted after user keys:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "workflow",
3
- "version": "5.0.0-beta.34",
3
+ "version": "5.0.0-beta.35",
4
4
  "description": "Workflow SDK - Build durable, resilient, and observable workflows",
5
5
  "main": "dist/typescript-plugin.cjs",
6
6
  "type": "module",
@@ -57,18 +57,18 @@
57
57
  },
58
58
  "dependencies": {
59
59
  "ms": "2.1.3",
60
- "@workflow/astro": "5.0.0-beta.34",
61
- "@workflow/cli": "5.0.0-beta.34",
62
- "@workflow/core": "5.0.0-beta.34",
63
- "@workflow/errors": "5.0.0-beta.10",
60
+ "@workflow/astro": "5.0.0-beta.35",
61
+ "@workflow/cli": "5.0.0-beta.35",
62
+ "@workflow/core": "5.0.0-beta.35",
63
+ "@workflow/errors": "5.0.0-beta.11",
64
64
  "@workflow/typescript-plugin": "5.0.0-beta.5",
65
65
  "@workflow/utils": "5.0.0-beta.6",
66
- "@workflow/next": "5.0.0-beta.34",
67
- "@workflow/nest": "5.0.0-beta.34",
68
- "@workflow/nitro": "5.0.0-beta.34",
69
- "@workflow/nuxt": "5.0.0-beta.34",
70
- "@workflow/sveltekit": "5.0.0-beta.34",
71
- "@workflow/rollup": "5.0.0-beta.34"
66
+ "@workflow/next": "5.0.0-beta.35",
67
+ "@workflow/nest": "5.0.0-beta.35",
68
+ "@workflow/nitro": "5.0.0-beta.35",
69
+ "@workflow/nuxt": "5.0.0-beta.35",
70
+ "@workflow/sveltekit": "5.0.0-beta.35",
71
+ "@workflow/rollup": "5.0.0-beta.35"
72
72
  },
73
73
  "devDependencies": {
74
74
  "@types/ms": "2.1.0",