Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .changeset/workflow-v2-routes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
"@workflow/astro": patch
"@workflow/builders": patch
"@workflow/cli": patch
"@workflow/core": patch
"@workflow/nest": patch
"@workflow/next": patch
"@workflow/nitro": patch
"@workflow/sveltekit": patch
"@workflow/utils": patch
"@workflow/world-local": patch
"@workflow/world-postgres": patch
"@workflow/world-testing": patch
---

Move workflow execution and canonical webhook routes to `/v2`, while retaining the `/v1/webhook` compatibility endpoint and cleaning stale v1 execution artifacts.
2 changes: 1 addition & 1 deletion .github/workflows/e2e-community-world.yml
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ jobs:
WORLD_ID: ${{ inputs.world-id }}
DEPLOYMENT_URL: "http://localhost:3000"
run: |
DEV_TEST_CONFIG="$(jq -nc --arg name "$APP_NAME" '{name:$name,project:"workbench-\($name)-workflow",generatedStepPath:"app/.well-known/workflow/v1/flow/__step_registrations.js",generatedWorkflowPath:"app/.well-known/workflow/v1/flow/route.js",apiFilePath:"app/api/chat/route.ts",apiFileImportPath:"../../.."}')"
DEV_TEST_CONFIG="$(jq -nc --arg name "$APP_NAME" '{name:$name,project:"workbench-\($name)-workflow",generatedStepPath:"app/.well-known/workflow/v2/flow/__step_registrations.js",generatedWorkflowPath:"app/.well-known/workflow/v2/flow/route.js",apiFilePath:"app/api/chat/route.ts",apiFileImportPath:"../../.."}')"
export DEV_TEST_CONFIG
cd "workbench/$APP_NAME" && pnpm dev &
cd "$GITHUB_WORKSPACE"
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -718,7 +718,7 @@ jobs:
NODE_OPTIONS: "--enable-source-maps"
APP_NAME: "nextjs-turbopack"
DEPLOYMENT_URL: "http://localhost:3000"
DEV_TEST_CONFIG: '{"generatedStepPath":"app/.well-known/workflow/v1/flow/__step_registrations.js","generatedWorkflowPath":"app/.well-known/workflow/v1/flow/route.js","apiFilePath":"app/api/chat/route.ts","apiFileImportPath":"../../..","port":3000}'
DEV_TEST_CONFIG: '{"generatedStepPath":"app/.well-known/workflow/v2/flow/__step_registrations.js","generatedWorkflowPath":"app/.well-known/workflow/v2/flow/route.js","apiFilePath":"app/api/chat/route.ts","apiFileImportPath":"../../..","port":3000}'

- name: Print Next.js server logs
if: always()
Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ cd workbench/nextjs-turbopack && pnpm start
**These are only relevant when writing code using the Workflow SDK**

- Workflow files go in `workflows/` directory (or `src/workflows/` if using src)
- Generated API routes appear in `app/.well-known/workflow/v1/` (Next.js integration)
- Generated execution API routes appear in `app/.well-known/workflow/v2/` (Next.js integration); the v1 directory retains manifest and webhook compatibility resources
- Workflow files must contain `"use workflow"` or `"use step"` directives to be processed
- Add `.swc` directory to `.gitignore` for SWC plugin cache artifacts

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ Throws an error if the webhook token is not found or invalid.
## Usage Note

<Callout type="warn">
In most cases, you should not need to call `resumeWebhook()` directly. When you use `createWebhook()`, the framework automatically generates a random webhook token and provides a public URL at `/.well-known/workflow/v1/webhook/:token`. External systems can send HTTP requests directly to that URL.
In most cases, you should not need to call `resumeWebhook()` directly. When you use `createWebhook()`, the framework automatically generates a random webhook token and provides a public URL at `/.well-known/workflow/v2/webhook/:token`. External systems can send HTTP requests directly to that URL. URLs generated by earlier v5 builds under `/.well-known/workflow/v1/webhook/:token` remain supported.

For server-side hook resumption with deterministic tokens, use [`resumeHook()`](/docs/api-reference/workflow-api/resume-hook) with [`createHook()`](/docs/api-reference/workflow/create-hook) instead.
</Callout>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ Creates a webhook that can be used to suspend and resume a workflow run upon rec
Webhooks provide a way for external systems to send HTTP requests directly to your workflow. Unlike hooks which accept arbitrary payloads, webhooks work with standard HTTP `Request` objects and can return HTTP `Response` objects.

<Callout type="warn">
`createWebhook()` creates a public endpoint at `/.well-known/workflow/v1/webhook/:token`, and the token in that URL is the only authorization performed for incoming requests resuming that webhook. This is convenient for prototypes and simple resume links because it avoids creating another route, but if you need stronger security, prefer [`createHook()`](/docs/api-reference/workflow/create-hook) behind your own route and authorize the request before calling [`resumeHook()`](/docs/api-reference/workflow-api/resume-hook) to avoid unauthenticated workflow resumptions.
`createWebhook()` creates a public endpoint at `/.well-known/workflow/v2/webhook/:token`, and the token in that URL is the only authorization performed for incoming requests resuming that webhook. Existing URLs under `/.well-known/workflow/v1/webhook/:token` remain accepted for compatibility. This is convenient for prototypes and simple resume links because it avoids creating another route, but if you need stronger security, prefer [`createHook()`](/docs/api-reference/workflow/create-hook) behind your own route and authorize the request before calling [`resumeHook()`](/docs/api-reference/workflow-api/resume-hook) to avoid unauthenticated workflow resumptions.
</Callout>

```ts lineNumbers
Expand Down
16 changes: 8 additions & 8 deletions docs/content/docs/v5/changelog/eager-processing.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,13 @@ The previous architecture used two separate routes,
each backed by its own queue trigger:

```
Queue: __wkf_workflow_* --> /.well-known/workflow/v1/flow (workflow replay in VM)
Queue: __wkf_workflow_* --> legacy flow handler (workflow replay in VM)
|
suspension (step needed)
|
queue step to __wkf_step_*
|
Queue: __wkf_step_* --> /.well-known/workflow/v1/step (step execution in Node.js)
Queue: __wkf_step_* --> legacy step handler (step execution in Node.js)
|
step completes
|
Expand All @@ -35,7 +35,7 @@ Each step required **2 queue messages** (step invoke + workflow continuation) an

## New Architecture

The two routes are merged into a single handler at `/.well-known/workflow/v1/flow` using `workflowEntrypoint()`. The step route is no longer generated.
The two routes are merged into a single handler at `/.well-known/workflow/v2/flow` using `workflowEntrypoint()`. A separate step route is no longer generated.

The handler runs an inline execution loop:

Expand Down Expand Up @@ -183,7 +183,7 @@ All framework builders were updated to use `createCombinedBundle()`:
### Generated File Layout

```
.well-known/workflow/v1/
.well-known/workflow/v2/
flow/
route.js # Handler (workflowEntrypoint)
__step_registrations.js # Step function registrations (side effects)
Expand Down Expand Up @@ -412,7 +412,7 @@ The V2 combined bundle was initially emitted as CJS by the standalone CLI and Ve
1. The BOA builder emits `__step_registrations.mjs` and `index.mjs`, writes `"type": "module"` in `package.json`, and sets `handler: "index.mjs"` in `.vc-config.json`.
2. The standalone builder no longer overrides `format`; it inherits the base builder's `'esm'` default.
3. The standalone config outputs `step.mjs` / `flow.mjs` instead of `.js`.
4. The `world-testing` server uses a native `import { POST } from '../.well-known/workflow/v1/flow.mjs'` instead of `createRequire`.
4. The `world-testing` server uses a native `import { POST } from '../.well-known/workflow/v2/flow.mjs'` instead of `createRequire`.
5. `createCombinedBundle`'s final esbuild pass (for `bundleFinalOutput: true`) now prepends the same `createRequire(import.meta.url)` banner used by the workflow/webhook bundles so CJS dependencies that call `require()` for Node.js builtins (for example the `events` module referenced by bundled libraries) still resolve at runtime.
6. To avoid a duplicate `__createRequire` declaration, the inner steps bundle that gets inlined by the final pass skips the banner — only the outer bundle emits it. This is threaded through via a new `skipEsmRequireBanner` option on `createStepsBundle`.

Expand Down Expand Up @@ -472,7 +472,7 @@ The Redis community-world benchmark still loads an external world package that h

### Build Output API Flow Handler Drift

The Vercel Build Output API builder still emitted the combined flow function as `index.js`, but the surrounding metadata kept pointing at `index.mjs`. That mismatch meant BOA-based preview deployments published neither `/.well-known/workflow/v1/flow` nor the public manifest, so the Vercel production e2e suite collapsed into manifest `404` errors immediately after deployment.
The Vercel Build Output API builder still emitted the combined flow function as `index.js`, but the surrounding metadata kept pointing at `index.mjs`. That mismatch meant BOA-based preview deployments published neither `/.well-known/workflow/v2/flow` nor the public manifest, so the Vercel production e2e suite collapsed into manifest `404` errors immediately after deployment.

**Fix**: Updated `packages/builders/src/vercel-build-output-api.ts` to point both `.vc-config.json` and manifest extraction at `flow.func/index.js`, which matches the CommonJS file the builder actually writes.

Expand All @@ -484,7 +484,7 @@ The first lazy-world-loading fix switched package resolution over to `createRequ

### Core Logger Still Pulled `debug` Into Webpack Flow Routes

Even after lazy world loading stopped eagerly importing `@workflow/world-vercel`, the generated Next.js webpack flow route still evaluated `packages/core/src/logger.ts` at module load. That file had a top-level `import debug from 'debug'`, which in turn pulled `debug/src/node` and its `tty` dynamic require into `/.well-known/workflow/v1/flow`. Webpack then failed during page-data collection with `Dynamic require of "tty" is not supported`.
Even after lazy world loading stopped eagerly importing `@workflow/world-vercel`, the generated Next.js webpack flow route still evaluated `packages/core/src/logger.ts` at module load. That file had a top-level `import debug from 'debug'`, which in turn pulled `debug/src/node` and its `tty` dynamic require into `/.well-known/workflow/v2/flow`. Webpack then failed during page-data collection with `Dynamic require of "tty" is not supported`.

**Fix**: Replace the static `debug` dependency in the core logger with lightweight `process.env.DEBUG` matching plus `console.debug`. That keeps verbose opt-in logging for local debugging without forcing webpack to bundle `debug` and its Node-only terminal helpers into the flow route.

Expand Down Expand Up @@ -541,7 +541,7 @@ Both assumptions break for routes that consume `start` (or any other `getWorldLa
1. Webpack and Turbopack tree-shake the named import `{ getWorld } from './runtime/world.js'` out of `runtime.ts` once a consumer only uses `start`. `world.ts` is dropped from the bundle entirely, so its module-load `globalThis[GetWorldFnKey] ??= getWorld` registration never fires.
2. The dynamic-import fallback inside `get-world-lazy.ts` builds the specifier `./world.js` at runtime to evade bundler tracing — but webpack inlines `get-world-lazy.js` into the route bundle, so the relative specifier resolves against `/var/task/<app>/.next/server/app/<route>/route.js` where no sibling `world.js` exists. Node throws `MODULE_NOT_FOUND`.

The symptom: the very first request that goes through `start()` on a cold serverless invocation fails. Once any other code path (typically the queue-driven `/.well-known/workflow/v1/flow` route, which uses `getWorld` directly via `workflowEntrypoint`) has loaded `world.ts`, subsequent `start()` calls succeed for the rest of the process lifetime making the failure flake-shaped: hard to reproduce in dev where everything tends to be warmed, but reliable on first user traffic into a fresh function instance.
The symptom: the very first request that goes through `start()` on a cold serverless invocation fails. Once any other code path (typically the queue-driven `/.well-known/workflow/v2/flow` route, which uses `getWorld` directly via `workflowEntrypoint`) has loaded `world.ts`, subsequent `start()` calls succeed for the rest of the process lifetime - making the failure flake-shaped: hard to reproduce in dev where everything tends to be warmed, but reliable on first user traffic into a fresh function instance.

**Fix**: Added `@workflow/core/runtime/world-init`, a server-only side-effect module that imports `./world.js` purely for its module-load side effect (the globalThis registration). It's exported via package conditions:

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ export async function POST(request: Request) {

- **`respondWith: "manual"`** gives you control over the HTTP response from inside a step. Use this when you need to validate the request before responding.
- **`for await` on a webhook** lets you process multiple events from the same URL. Use `break` to stop listening after a terminal event.
- **Webhooks auto-generate URLs** at `/.well-known/workflow/v1/webhook/:token`. Pass this URL to external services.
- **Webhooks auto-generate URLs** at `/.well-known/workflow/v2/webhook/:token`. Pass this URL to external services. Existing v1 webhook URLs remain supported.
- **Race webhooks against `sleep()`** for deadlines. If the callback doesn't arrive in time, the workflow can take a fallback action.
- **For large payloads**, use a hook + reference token instead of passing the data through the workflow. The event log serializes all step inputs/outputs, so large payloads hurt performance.

Expand Down
6 changes: 3 additions & 3 deletions docs/content/docs/v5/foundations/hooks.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -219,10 +219,10 @@ While hooks are powerful, they require you to manually handle HTTP requests and
2. Provides an automatically addressable `url` property pointing to the generated webhook endpoint
3. Handles sending HTTP [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response) objects back to the caller

When using Workflow SDK, webhooks are automatically wired up at `/.well-known/workflow/v1/webhook/:token` without any additional setup.
When using Workflow SDK, webhooks are automatically wired up at `/.well-known/workflow/v2/webhook/:token` without any additional setup. Existing v1 webhook URLs continue to resolve for compatibility.

<Callout type="warn">
`createWebhook()` exposes a public route at `/.well-known/workflow/v1/webhook/:token`, and the token in that URL is the only authorization performed for incoming requests. This is convenient for prototypes and a simple developer experience because you can share the webhook URL (endpoint) without creating another route, but if you need stronger security, prefer [`createHook()`](/docs/api-reference/workflow/create-hook) behind your own route and authorize the request before calling [`resumeHook()`](/docs/api-reference/workflow-api/resume-hook) to avoid unauthenticated workflow resumptions.
`createWebhook()` exposes a public route at `/.well-known/workflow/v2/webhook/:token`, and the token in that URL is the only authorization performed for incoming requests. This is convenient for prototypes and a simple developer experience because you can share the webhook URL (endpoint) without creating another route, but if you need stronger security, prefer [`createHook()`](/docs/api-reference/workflow/create-hook) behind your own route and authorize the request before calling [`resumeHook()`](/docs/api-reference/workflow-api/resume-hook) to avoid unauthenticated workflow resumptions.
</Callout>

<Callout type="info">
Expand All @@ -243,7 +243,7 @@ export async function webhookWorkflow() {

// The webhook is automatically available at this URL
console.log("Send HTTP requests to:", webhook.url);
// Example: https://your-app.com/.well-known/workflow/v1/webhook/lJHkuMdQ2FxSFTbUMU84k
// Example: https://your-app.com/.well-known/workflow/v2/webhook/lJHkuMdQ2FxSFTbUMU84k

// Workflow pauses until an HTTP request is received
const request = await webhook;
Expand Down
2 changes: 1 addition & 1 deletion docs/content/docs/v5/getting-started/nestjs.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ WorkflowModule.forRoot({
The `.js` local import specifiers in this example are the ESM form.
</Callout>

The `WorkflowModule` handles workflow bundle building and provides HTTP routing for workflow execution at `.well-known/workflow/v1/`.
The `WorkflowModule` handles workflow bundle building and provides HTTP routing for workflow execution at `.well-known/workflow/v2/flow`, plus webhook handling at `.well-known/workflow/v2/webhook/:token` with compatibility for existing v1 webhook URLs.

</Step>

Expand Down
2 changes: 1 addition & 1 deletion docs/content/docs/v5/getting-started/next.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ If your Next.js app has a [proxy handler](https://nextjs.org/docs/app/api-refere
(formerly known as "middleware"), you'll need to update the matcher pattern to exclude Workflow's
internal paths to prevent the proxy handler from running on them.

If you see `[local world] Queue operation failed` with `Cannot perform ArrayBuffer.prototype.slice on a detached ArrayBuffer`, your proxy matcher is still intercepting Workflow's internal `POST /.well-known/workflow/v1/flow` request. This is especially easy to miss in Next.js 16, where `proxy.ts` replaced `middleware.ts`.
If you see `[local world] Queue operation failed` with `Cannot perform ArrayBuffer.prototype.slice on a detached ArrayBuffer`, your proxy matcher is still intercepting Workflow's internal `POST /.well-known/workflow/v2/flow` request. This is especially easy to miss in Next.js 16, where `proxy.ts` replaced `middleware.ts`.

Add `.well-known/workflow/*` to your matcher exclusion list:

Expand Down
Loading
Loading