[core] Flow v2, eager-processing of workflows, combine flow/step bundles#1338
[core] Flow v2, eager-processing of workflows, combine flow/step bundles#1338VaguelySerious wants to merge 68 commits intomainfrom
Conversation
Signed-off-by: Peter Wielander <mittgfu@gmail.com>
🦋 Changeset detectedLatest commit: 3625e94 The changes in this PR will be included in the next version bump. This PR includes changesets to release 20 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
📊 Benchmark Results
⏳ Benchmarks are running... Started at: 2026-03-16T16:25:53Z 📊 Benchmark Results
workflow with no steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Next.js (Turbopack) workflow with 1 step💻 Local Development
▲ Production (Vercel)
🔍 Observability: Next.js (Turbopack) workflow with 10 sequential steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Next.js (Turbopack) workflow with 25 sequential steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Next.js (Turbopack) workflow with 50 sequential steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Next.js (Turbopack) Promise.all with 10 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Next.js (Turbopack) Promise.all with 25 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Next.js (Turbopack) Promise.all with 50 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Next.js (Turbopack) Promise.race with 10 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Next.js (Turbopack) Promise.race with 25 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Next.js (Turbopack) Promise.race with 50 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Next.js (Turbopack) Stream Benchmarks (includes TTFB metrics)workflow with stream💻 Local Development
▲ Production (Vercel)
🔍 Observability: Next.js (Turbopack) SummaryFastest Framework by WorldWinner determined by most benchmark wins
Fastest World by FrameworkWinner determined by most benchmark wins
Column Definitions
Worlds:
|
🧪 E2E Test Results❌ Some tests failed Summary
❌ Failed Tests💻 Local Development (2 failed)astro-stable (1 failed):
sveltekit-stable (1 failed):
📦 Local Production (2 failed)astro-stable (1 failed):
sveltekit-stable (1 failed):
🌍 Community Worlds (55 failed)mongodb (3 failed):
redis (2 failed):
turso (50 failed):
Details by Category✅ ▲ Vercel Production
❌ 💻 Local Development
❌ 📦 Local Production
✅ 🐘 Local Postgres
✅ 🪟 Windows
❌ 🌍 Community Worlds
✅ 📋 Other
❌ Some E2E test jobs failed:
Check the workflow run for details. |
…nd step handler Transient network errors (ECONNRESET, etc.) during infrastructure calls (event listing, event creation) were caught by a shared try/catch that also handles user code errors, incorrectly marking runs as run_failed or steps as step_failed instead of letting the queue redeliver. - runtime.ts: Move infrastructure calls outside the user-code try/catch so errors propagate to the queue handler for automatic retry - step-handler.ts: Same structural separation — only stepFn.apply() is wrapped in the try/catch that produces step_failed/step_retrying - helpers.ts: Add isTransientNetworkError() and update withServerErrorRetry to retry network errors in addition to 5xx responses - helpers.test.ts: Add tests for network error detection and retry
Merge flow and step routes into a single combined handler that executes steps inline when possible, reducing function invocations and queue overhead. Serial workflows can now complete in a single function invocation instead of 2N+1 invocations. Key changes: - Add `combinedEntrypoint()` to core runtime with inline step execution loop - Extract reusable step execution logic into `step-executor.ts` - Add `handleSuspensionV2()` that creates events without queuing steps - Add `stepId` field to `WorkflowInvokePayload` for background step dispatch - Add `createCombinedBundle()` to base builder - Update Next.js builder to generate combined route at v1/flow - Update health check e2e tests for single-route architecture Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ing `<` and `>` as JSX tag delimiters. This commit fixes the issue reported at docs/content/docs/changelog/eager-processing.mdx:129 **Bug explanation:** In MDX v3 (used by this project via `@mdx-js/mdx` ^3.1.1 and fumadocs-mdx), all content is parsed as a mix of Markdown and JSX. The `<` character in prose text is interpreted as the start of a JSX element. When the MDX parser encounters `<=50%`, it sees `<` as opening a JSX tag and then `=` as the next character, which is not a valid character to start an element name (element names must start with a letter, `$`, or `_`). This causes a parse error: ``` 124:143: eager-processing.mdx:124:143: Unexpected character `=` (U+003D) before name, expected a character that can start a name, such as a letter, `$`, or `_` ``` This directly caused the Vercel build failure (deployment `dpl_GLdsAtQ1WM7xFGXUFT2SvswuNWkW`), as the `docs#build` task failed. **Fix explanation:** Replaced `<=50%` with `≤50%` (Unicode "less-than or equal to" symbol, U+2264) and `>=50%` with `≥50%` (Unicode "greater-than or equal to" symbol, U+2265). These Unicode characters are semantically equivalent, render correctly in browsers, and are not interpreted as JSX syntax by the MDX parser. This is a common and clean approach for MDX content — arguably even more readable than the ASCII approximations. Co-authored-by: Vercel <vercel[bot]@users.noreply.github.com> Co-authored-by: VaguelySerious <mittgfu@gmail.com>
Merge fix/separate-infra-user-error-handling into peter/v2-flow. Resolves conflicts in runtime.ts and ports #1339's changes to the V2 combined entrypoint: - Remove withThrottleRetry wrapper (undici RetryAgent handles retries) - Remove serverErrorRetryCount tracking - Remove withServerErrorRetry from step-executor.ts - Separate infrastructure vs user code error handling Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Take main's runtime.ts as base for V1 workflowEntrypoint (with #1339's final error handling structure), then re-apply V2 combinedEntrypoint and helpers on top. Fix getAllWorkflowRunEvents to return Event[] again (WithCursor variant for V2 incremental loading). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Port the V2 combined bundle to all remaining frameworks: - Standalone builder - Vercel Build Output API builder (used by Nitro, Astro production) - NestJS builder - SvelteKit builder - Nitro local builder - Astro local builder Each builder now uses createCombinedBundle() instead of separate createStepsBundle() + createWorkflowsBundle(). The step route is no longer generated. Framework-specific post-processing (SvelteKit, Astro request normalization) updated to match combinedEntrypoint. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove: - V1 workflowEntrypoint (replaced by combined handler) - V1 handleSuspension (replaced by V2 that returns pending steps) - runStep placeholder - stepEntrypoint re-export from workflow/runtime - STEP_QUEUE_TRIGGER and COMBINED_QUEUE_TRIGGER constants - stepEntrypoint export from createStepsBundle virtual entry Rename: - combinedEntrypoint → workflowEntrypoint - handleSuspensionV2 → handleSuspension - SuspensionHandlerV2Result → SuspensionHandlerResult - COMBINED_QUEUE_TRIGGER → WORKFLOW_QUEUE_TRIGGER The step-handler.ts and step-executor.ts both remain — step-handler is the V1 queue handler (may still be useful for testing), and step-executor is the shared execution logic used by the combined handler. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Peter Wielander <mittgfu@gmail.com>
Replace all references to combinedEntrypoint, handleSuspensionV2, COMBINED_QUEUE_TRIGGER, stepEntrypoint with their final names. Remove mentions of V1 backward compatibility since V1 code is deleted. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Take our runtime.ts (V1 removed), main's http-client.ts (H2/undici), main's pnpm-lock.yaml. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Resolve runtime.ts/lockfile/changeset conflicts with main - Add 'node' condition to workflow package exports to prevent esbuild from resolving to typescript-plugin.cjs via the 'require' condition - Revert standalone builder to use separate step/workflow bundles (createCombinedBundle re-bundling creates duplicate module instances) - Update world-testing server: side-effect import for step registrations, remove step route (V2 handles everything via flow route) Known issue: standalone builder's separate bundles have duplicate @workflow/core/private instances — step registrations populate one map, runtime looks up from another. Will fix by inlining step registrations into the combined route's virtual entry. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When bundleFinalOutput is true, the combined route imports the step registrations file which gets re-bundled by esbuild. This still creates separate module scopes for the step registrations and runtime. The standalone builder (used by world-testing/CLI) hits this issue: registerStepFunction and getStepFunction use different Maps because esbuild wraps each source module in isolated scopes even within the same output file. This is a known limitation for bundleFinalOutput: true. Framework integrations (Next.js, SvelteKit, etc.) use bundleFinalOutput: false where the framework's own bundler handles resolution correctly. Also adds "node" condition to workflow package.json exports to prevent esbuild from resolving to typescript-plugin.cjs via "require" condition. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Change the registeredSteps Map in @workflow/core/private from a module-scoped variable to a globalThis singleton via Symbol.for. This ensures all esbuild module scopes within the same bundle share the same Map, fixing step registration lookup for standalone and Vercel BOA builds (bundleFinalOutput: true). The Symbol.for pattern is already used by: - @workflow/core/runtime/world.ts (World singleton) - @workflow/core/symbols.ts (class registry) Also simplifies createCombinedBundle by removing the unnecessary bundleFinalOutput branching — both paths produce identical code. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Make registeredSteps Map and contextStorage AsyncLocalStorage into globalThis singletons via Symbol.for. This fixes the module scope duplication issue where esbuild creates separate instances in re-bundled output (bundleFinalOutput: true). Also: - Add "node" condition to workflow package exports to prevent esbuild from resolving to typescript-plugin.cjs - Set WORKFLOW_LOCAL_QUEUE_CONCURRENCY=1 in world-testing to prevent concurrent replay interference in local world - Document non-Next.js integration challenges in changelog Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Update SvelteKit and Astro generatedStepPath in create-test-matrix.mjs to match V2 output (__step_registrations.js instead of step/+server.js) - Set WORKFLOW_POSTGRES_WORKER_CONCURRENCY=1 in test server to prevent concurrent replay interference in postgres world tests Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Nitro: remove virtual handler for /.well-known/workflow/v1/step since V2 handles everything via the flow route. NestJS: remove @post('step') handler, import step registrations as side effects in the flow handler. Also serialize postgres worker concurrency in tests. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ings All ESM builders (Nitro, NestJS, SvelteKit, Astro) need bundleFinalOutput: false and externalizeNonSteps: true to avoid "Dynamic require of X is not supported" errors. When bundleFinalOutput: true with ESM format, esbuild wraps CJS require() calls in a polyfill that fails in ESM context. Only CJS builders (standalone, Vercel BOA) can use bundleFinalOutput: true safely. Also explicitly set format: 'cjs' for Vercel BOA to match its commonjs package.json. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The deferred builder was writing step registrations to a .temp file, which canary Turbopack rejected as an unknown module type. Write directly to the final __step_registrations.js name since it doesn't need the copyFileIfChanged temp mechanism (it's a side-effect import, not a route). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
SvelteKit and Astro builders now remove the old V1 step route directory/file during build. This prevents Vercel build cache from preserving stale files that import the removed stepEntrypoint function. Also adds ESM bundleFinalOutput documentation to changelog. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The beforeExit hook that patches .vc-config.json was still trying to configure the V1 step.func directory, which no longer exists in V2. Remove the step entry and add an existence check for safety. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The steps bundle only contains side-effect code (registerStepFunction calls) with no exports. When rollup processes the combined route that imports this module, it tree-shakes the entire module away because it has no used exports. Fix: add a sentinel export (__steps_registered) to the steps bundle and import it in the combined route. This gives rollup a used binding to track, preventing it from dropping the module and its side effects. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When the V2 inline execution loop advances ahead (e.g., completes batch 1 inline and creates step_created events for batch 2), concurrent replays from batch 1 background step continuations may encounter batch 2's events without matching subscribers. Fix: the EventsConsumer's onUnconsumedEvent callback now returns true to skip step lifecycle events (step_created, step_started, step_completed, step_failed, step_retrying) that have a corresponding step_created event in the log — confirming they're from a legitimate concurrent handler. Orphaned events with unknown correlationIds still error. Also: steps bundle exports __steps_registered sentinel to prevent rollup from tree-shaking the side-effect-only module. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ation Two fixes: 1. Step executor: await ops (stream writes) with a 5-second bounded wait before creating step_completed. In V1, each step was a separate function invocation and waitUntil ensured ops completed before the function ended. In V2 inline execution, the handler loop continues immediately, leaving stream data uncommitted. The bounded wait ensures data reaches S3 before proceeding, with waitUntil as a safety net for ops that need more time. 2. Step executor: only enforce max retries when step.error exists (actual retry after failure). V2 concurrent replays can inflate the attempt counter via simultaneous step_started calls without any prior failure. With N parallel steps completing, up to N concurrent continuations may race to start the same step, each incrementing attempt. The first completion wins (step_completed idempotency), but premature "exceeded max retries" failures must be prevented. Also documents all integration challenges in the changelog. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When a step has pending background operations (e.g., WritableStream data being piped to S3), the V2 inline execution loop must not continue to the next replay iteration. Instead, queue a continuation and return to give waitUntil time to flush the ops. In V1, each step ran in a separate function invocation. After the step completed, the function returned and waitUntil flushed the stream writes to S3 before the test could read the data. In V2, the inline loop continues processing, keeping the function alive and preventing waitUntil from flushing. The test's stream reader blocks forever because S3 data never arrives. Fix: executeStep now returns hasPendingOps when the step had background ops. The V2 handler checks this and breaks the loop, queueing a continuation instead of looping inline. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add changelog entries for: - Concurrent step_started inflating attempt counter (promiseRaceStressTest fix) - Inline step execution with pending stream operations (outputStream fix) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Peter Wielander <mittgfu@gmail.com>
See changelog / architecture doc https://workflow-docs-git-peter-v2-flow.vercel.sh/docs/changelog/eager-processing