Skip to content

feat(core): opt-in QuickJS WASM workflow runtime - #2505

Closed
pranaygp wants to merge 9 commits into
mainfrom
pgp/cloudflare-world-testing
Closed

feat(core): opt-in QuickJS WASM workflow runtime#2505
pranaygp wants to merge 9 commits into
mainfrom
pgp/cloudflare-world-testing

Conversation

@pranaygp

Copy link
Copy Markdown
Contributor

Context

Running Workflow on Cloudflare Workers (e.g. with the community @fantasticfour/world-cloudflare world) is blocked by @workflow/core's use of node:vm to execute workflow orchestrator code. I verified empirically (minimal workerd smoke test) that this is not fixable by the enable_nodejs_vm_module compat flag:

  • With enable_nodejs_vm_module: vm.createContext() works, but vm.runInContext() / compileFunction() throw "not implemented" — the flag only adds a non-functional module stub.
  • Root cause: workerd bans all runtime code-generation-from-strings (eval / new Function both throw "Code generation from strings disallowed for this context"). vm.runInContext is the same class of operation. This is a deliberate, platform-wide constraint.

The unblock is a runtime that doesn't use node:vm. This PR lifts the QuickJS WASM engine from @TooTallNate's snapshot-runtime PR (#1300) — running workflow bytecode inside a QuickJS WASM VM via quickjs-wasi (a first-party vercel-labs package), where dynamic eval is fine because it happens in the VM's own WASM linear memory, outside the host isolate's eval ban. I confirmed quickjs-wasi runs cleanly inside workerd.

Tracker: #2028. Engine source: #1300.

What this PR does

Adds an opt-in QuickJS runtime to @workflow/core. node:vm remains the default — nothing changes unless you opt in.

  • Selection: WORKFLOW_RUNTIME=quickjs env var, or per-run executionContext.workflowRuntime = 'quickjs' (propagated by start()), so one deployment can serve both runtimes.
  • No snapshots. Unlike Snapshot Runtime: QuickJS WASM VM with snapshot/restore for workflow execution #1300, the snapshot/restore machinery is dropped entirely — this is pure event replay (the same model as node:vm), with no dependency on world.snapshots.*. None of the world packages are touched.
  • Lazy-loaded: the QuickJS entrypoint (and its multi-MB embedded WASM assets) is dynamically imported only when the QuickJS runtime is selected, so node:vm runs never pay the cost.

Key files

  • runtime/runtime-mode.tsWORKFLOW_RUNTIME flag (node-vm default | quickjs)
  • runtime/quickjs-runtime.ts — VM execution: create VM → eval serde bundle + bootstrap + workflow → replay events → complete/suspend/fail (ported from snapshot-runtime.ts, snapshots stripped)
  • runtime/quickjs-entrypoint.ts — run lifecycle: load events → run VM → write run_completed / per-pending-op step_created/hook_created/wait_created + queue steps / run_failed (ported from snapshot-entrypoint.ts, snapshots stripped)
  • serialization/{workflow-vm,codec-devalue-vm,reducers/common-vm,vm-bundle-entry}.ts — devalue serde bundle that runs inside the VM (no Node deps; btoa/atob instead of Buffer)
  • scripts/build-{quickjs-assets,vm-serde-bundle}.js — base64-embed quickjs.wasm + C extensions and bundle the serde IIFE (no filesystem access at runtime → Workers-safe)
  • runtime.ts / runtime/start.ts — dispatch + flag propagation

Notable fixes made while porting (vs. #1300)

  • Targets quickjs-wasi@3.x (btoa/atob are now default intrinsics, not a separate extension).
  • Wrap the VM workflow invocation in Promise.resolve(...) so synchronous workflow returns behave like node:vm.
  • Host-side decrypt + decompress of event payloads before they cross the WASM boundary (the VM only understands the devl format; step results are zstd-compressed by the shared executor).
  • Deterministic VM clock follows the event timeline (Date.now() advances per consumed event's createdAt), matching node:vm — required now that there are no snapshots.

Verification

  • quickjs-wasi runs inside workerd (create / evalCode / host callbacks / async drain) — the Cloudflare unblock.
  • ✅ New isolated integration tests (quickjs-runtime.test.ts) + flag tests — completion, args, structured-value serde round-trip, suspension w/ pending step, failure, not-registered, and Date-timeline determinism.
  • ✅ Full existing @workflow/core unit suite passes (no node:vm regression).
  • ✅ Workbench builds and runs with the QuickJS runtime integrated; sleepingWorkflow + promiseAnyWorkflow pass end-to-end under WORKFLOW_RUNTIME=quickjs.
  • 🔄 Full local e2e suite (nextjs-turbopack, world-local) under QuickJS is running; results to follow in a comment.

Follow-ups (not in this PR)

  • Cloudflare workbench app + bridging @fantasticfour/world-cloudflare to the current world interface, deployed to Workers.
  • CI: a Workers-deploy e2e/benchmark variant for the Cloudflare community world.

🤖 Generated with Claude Code

Adds an alternative workflow runtime that executes orchestrator code in a
QuickJS WASM VM (via quickjs-wasi) instead of node:vm, enabling execution on
runtimes that disallow node:vm / code-generation-from-strings (e.g. Cloudflare
Workers). Opt-in via WORKFLOW_RUNTIME=quickjs or executionContext.workflowRuntime;
node:vm remains the default.

Ported from the QuickJS engine in #1300 with the snapshot/restore machinery
removed: pure event-replay, no dependency on world.snapshots.*. The workflow
bundle + a devalue serde bundle run inside the VM; host-written event payloads
are decrypted and decompressed before crossing the WASM boundary, and the VM's
deterministic clock follows the event timeline.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@pranaygp
pranaygp requested a review from a team as a code owner June 18, 2026 06:32
Copilot AI review requested due to automatic review settings June 18, 2026 06:32
@changeset-bot

changeset-bot Bot commented Jun 18, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 543f481

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 16 packages
Name Type
@workflow/core Minor
@workflow/builders Patch
@workflow/cli Patch
@workflow/next Patch
@workflow/nitro Patch
@workflow/vitest Patch
@workflow/web-shared Patch
@workflow/web Patch
workflow Minor
@workflow/world-testing Patch
@workflow/astro Patch
@workflow/nest Patch
@workflow/rollup Patch
@workflow/sveltekit Patch
@workflow/vite Patch
@workflow/nuxt Patch

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

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@vercel

vercel Bot commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
example-nextjs-workflow-turbopack Ready Ready Preview, Comment Jun 23, 2026 9:38pm
example-nextjs-workflow-webpack Ready Ready Preview, Comment Jun 23, 2026 9:38pm
example-workflow Ready Ready Preview, Comment Jun 23, 2026 9:38pm
workbench-astro-workflow Ready Ready Preview, Comment Jun 23, 2026 9:38pm
workbench-express-workflow Ready Ready Preview, Comment Jun 23, 2026 9:38pm
workbench-fastify-workflow Ready Ready Preview, Comment Jun 23, 2026 9:38pm
workbench-hono-workflow Ready Ready Preview, Comment Jun 23, 2026 9:38pm
workbench-nitro-workflow Ready Ready Preview, Comment Jun 23, 2026 9:38pm
workbench-nuxt-workflow Ready Ready Preview, Comment Jun 23, 2026 9:38pm
workbench-sveltekit-workflow Ready Ready Preview, Comment Jun 23, 2026 9:38pm
workbench-tanstack-start-workflow Ready Ready Preview, Comment Jun 23, 2026 9:38pm
workbench-vite-workflow Ready Ready Preview, Comment Jun 23, 2026 9:38pm
workflow-docs Ready Ready Preview, Comment, Open in v0 Jun 23, 2026 9:38pm
workflow-swc-playground Ready Ready Preview, Comment Jun 23, 2026 9:38pm
workflow-tarballs Ready Ready Preview, Comment Jun 23, 2026 9:38pm
workflow-web Ready Ready Preview, Comment Jun 23, 2026 9:38pm

@github-actions

github-actions Bot commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

Passed Failed Skipped Total
❌ ▲ Vercel Production 1437 5 230 1672
✅ 💻 Local Development 1605 0 219 1824
✅ 📦 Local Production 1605 0 219 1824
✅ 🐘 Local Postgres 1593 0 231 1824
✅ 🪟 Windows 152 0 0 152
✅ 📋 Other 885 0 179 1064
Total 7277 5 1078 8360

❌ Failed Tests

▲ Vercel Production (5 failed)

astro (1 failed):

  • error handling retry behavior RetryableError respects custom retryAfter delay

example (1 failed):

express (1 failed):

  • error handling retry behavior RetryableError respects custom retryAfter delay

fastify (1 failed):

  • error handling retry behavior RetryableError respects custom retryAfter delay

nextjs-webpack (1 failed):

Details by Category

❌ ▲ Vercel Production
App Passed Failed Skipped
❌ astro 124 1 27
❌ example 124 1 27
❌ express 124 1 27
❌ fastify 124 1 27
✅ hono 125 0 27
✅ nextjs-turbopack 149 0 3
❌ nextjs-webpack 148 1 3
✅ nitro 125 0 27
✅ nuxt 125 0 27
✅ sveltekit 144 0 8
✅ vite 125 0 27
✅ 💻 Local Development
App Passed Failed Skipped
✅ astro-stable 127 0 25
✅ express-stable 127 0 25
✅ fastify-stable 127 0 25
✅ hono-stable 127 0 25
✅ nextjs-turbopack-canary 133 0 19
✅ nextjs-turbopack-stable 152 0 0
✅ nextjs-webpack-canary 133 0 19
✅ nextjs-webpack-stable 152 0 0
✅ nitro-stable 127 0 25
✅ nuxt-stable 127 0 25
✅ sveltekit-stable 146 0 6
✅ vite-stable 127 0 25
✅ 📦 Local Production
App Passed Failed Skipped
✅ astro-stable 127 0 25
✅ express-stable 127 0 25
✅ fastify-stable 127 0 25
✅ hono-stable 127 0 25
✅ nextjs-turbopack-canary 133 0 19
✅ nextjs-turbopack-stable 152 0 0
✅ nextjs-webpack-canary 133 0 19
✅ nextjs-webpack-stable 152 0 0
✅ nitro-stable 127 0 25
✅ nuxt-stable 127 0 25
✅ sveltekit-stable 146 0 6
✅ vite-stable 127 0 25
✅ 🐘 Local Postgres
App Passed Failed Skipped
✅ astro-stable 126 0 26
✅ express-stable 126 0 26
✅ fastify-stable 126 0 26
✅ hono-stable 126 0 26
✅ nextjs-turbopack-canary 132 0 20
✅ nextjs-turbopack-stable 151 0 1
✅ nextjs-webpack-canary 132 0 20
✅ nextjs-webpack-stable 151 0 1
✅ nitro-stable 126 0 26
✅ nuxt-stable 126 0 26
✅ sveltekit-stable 145 0 7
✅ vite-stable 126 0 26
✅ 🪟 Windows
App Passed Failed Skipped
✅ nextjs-turbopack 152 0 0
✅ 📋 Other
App Passed Failed Skipped
✅ e2e-local-dev-nest-stable 127 0 25
✅ e2e-local-dev-tanstack-start- 127 0 25
✅ e2e-local-postgres-nest-stable 126 0 26
✅ e2e-local-postgres-tanstack-start- 126 0 26
✅ e2e-local-prod-nest-stable 127 0 25
✅ e2e-local-prod-tanstack-start- 127 0 25
✅ e2e-vercel-prod-tanstack-start 125 0 27

📋 View full workflow run


Some E2E test jobs failed:

  • Vercel Prod: failure
  • Local Dev: success
  • Local Prod: success
  • Local Postgres: success
  • Windows: success

Check the workflow run for details.

@github-actions

github-actions Bot commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

📈 Comparing against baseline from main branch. Green 🟢 = faster, Red 🔺 = slower.

workflow with no steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Nitro 0.037s (-20.4% 🟢) 1.005s (~) 0.968s 10 1.00x
💻 Local Express 0.043s (-8.8% 🟢) 1.006s (~) 0.962s 10 1.16x
💻 Local Next.js (Turbopack) 0.050s (-5.5% 🟢) 1.005s (~) 0.956s 10 1.33x
🐘 Postgres Next.js (Turbopack) 0.058s (+5.4% 🔺) 1.013s (~) 0.954s 10 1.56x
🐘 Postgres Nitro 0.066s (+1.4%) 1.012s (~) 0.946s 10 1.77x
🐘 Postgres Express 0.072s (+3.6%) 1.013s (~) 0.941s 10 1.92x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Next.js (Turbopack) 0.306s (+13.3% 🔺) 2.414s (+18.1% 🔺) 2.108s 10 1.00x
▲ Vercel Express 0.425s (+83.9% 🔺) 2.134s (+1.4%) 1.709s 10 1.39x
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Next.js (Turbopack) | Express

workflow with 1 step

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Nitro 1.062s (-2.2%) 2.005s (~) 0.943s 10 1.00x
💻 Local Express 1.077s (-0.9%) 2.007s (~) 0.930s 10 1.01x
💻 Local Next.js (Turbopack) 1.086s (~) 2.008s (~) 0.921s 10 1.02x
🐘 Postgres Express 1.088s (~) 2.010s (~) 0.922s 10 1.02x
🐘 Postgres Next.js (Turbopack) 1.092s (+0.8%) 2.009s (~) 0.917s 10 1.03x
🐘 Postgres Nitro 1.093s (-0.9%) 2.009s (~) 0.916s 10 1.03x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Next.js (Turbopack) 1.530s (+6.7% 🔺) 3.429s (+2.3%) 1.899s 10 1.00x
▲ Vercel Express 1.543s (+11.5% 🔺) 3.489s (+23.3% 🔺) 1.946s 10 1.01x
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Next.js (Turbopack) | Express

workflow with 10 sequential steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Nitro 10.348s (-1.1%) 11.020s (~) 0.673s 3 1.00x
💻 Local Express 10.414s (~) 11.022s (~) 0.608s 3 1.01x
🐘 Postgres Nitro 10.425s (-0.7%) 11.015s (~) 0.590s 3 1.01x
🐘 Postgres Express 10.459s (~) 11.016s (~) 0.558s 3 1.01x
💻 Local Next.js (Turbopack) 10.475s (~) 11.023s (~) 0.548s 3 1.01x
🐘 Postgres Next.js (Turbopack) 10.536s (+0.8%) 11.018s (~) 0.482s 3 1.02x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 13.751s (+11.7% 🔺) 15.776s (+13.7% 🔺) 2.025s 2 1.00x
▲ Vercel Next.js (Turbopack) 14.155s (+19.9% 🔺) 16.427s (+17.8% 🔺) 2.273s 2 1.03x
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Express | Next.js (Turbopack)

workflow with 25 sequential steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Nitro 13.374s (-1.9%) 14.027s (~) 0.653s 5 1.00x
💻 Local Express 13.562s (~) 14.027s (~) 0.465s 5 1.01x
🐘 Postgres Nitro 13.603s (-0.8%) 14.016s (~) 0.413s 5 1.02x
🐘 Postgres Express 13.628s (~) 14.018s (~) 0.391s 5 1.02x
💻 Local Next.js (Turbopack) 13.630s (~) 14.028s (~) 0.399s 5 1.02x
🐘 Postgres Next.js (Turbopack) 13.822s (+1.1%) 14.019s (~) 0.197s 5 1.03x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 20.079s (+22.1% 🔺) 22.293s (+23.9% 🔺) 2.214s 3 1.00x
▲ Vercel Next.js (Turbopack) 22.775s (+31.3% 🔺) 25.269s (+31.9% 🔺) 2.495s 3 1.13x
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Express | Next.js (Turbopack)

workflow with 50 sequential steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Nitro 11.672s (-4.2%) 12.022s (-7.7% 🟢) 0.350s 8 1.00x
💻 Local Express 12.005s (-2.7%) 12.398s (-4.8%) 0.393s 8 1.03x
🐘 Postgres Nitro 12.008s (-2.2%) 12.141s (-6.8% 🟢) 0.133s 8 1.03x
🐘 Postgres Express 12.101s (~) 13.021s (~) 0.919s 7 1.04x
💻 Local Next.js (Turbopack) 12.266s (~) 13.027s (~) 0.760s 7 1.05x
🐘 Postgres Next.js (Turbopack) 12.290s (+1.6%) 13.016s (~) 0.726s 7 1.05x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 27.116s (+47.6% 🔺) 29.394s (+45.8% 🔺) 2.278s 4 1.00x
▲ Vercel Next.js (Turbopack) 29.370s (+51.8% 🔺) 31.552s (+48.6% 🔺) 2.182s 3 1.08x
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Express | Next.js (Turbopack)

Promise.all with 10 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 1.167s (-1.3%) 2.008s (~) 0.841s 15 1.00x
🐘 Postgres Express 1.169s (~) 2.008s (~) 0.839s 15 1.00x
🐘 Postgres Next.js (Turbopack) 1.197s (+1.8%) 2.008s (~) 0.811s 15 1.03x
💻 Local Nitro 1.323s (-8.0% 🟢) 2.005s (~) 0.683s 15 1.13x
💻 Local Express 1.390s (~) 2.006s (~) 0.617s 15 1.19x
💻 Local Next.js (Turbopack) 1.449s (-3.2%) 2.006s (~) 0.557s 15 1.24x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 3.133s (+54.4% 🔺) 5.195s (+48.4% 🔺) 2.062s 6 1.00x
▲ Vercel Next.js (Turbopack) 3.890s (+81.0% 🔺) 6.252s (+65.0% 🔺) 2.362s 5 1.24x
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Express | Next.js (Turbopack)

Promise.all with 25 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Next.js (Turbopack) 1.312s (~) 3.009s (+3.1%) 1.696s 10 1.00x
🐘 Postgres Express 1.327s (-4.7%) 2.394s (-10.5% 🟢) 1.067s 13 1.01x
🐘 Postgres Nitro 1.369s (+2.2%) 2.470s (-1.5%) 1.101s 13 1.04x
💻 Local Nitro 2.353s (-2.3%) 3.007s (+3.0%) 0.655s 10 1.79x
💻 Local Express 2.527s (+7.2% 🔺) 3.109s (+6.6% 🔺) 0.582s 10 1.93x
💻 Local Next.js (Turbopack) 2.587s (-5.4% 🟢) 3.210s (+3.2%) 0.623s 10 1.97x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 4.182s (+15.1% 🔺) 6.786s (+31.4% 🔺) 2.604s 5 1.00x
▲ Vercel Next.js (Turbopack) 4.668s (+39.7% 🔺) 6.649s (+31.8% 🔺) 1.981s 5 1.12x
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Express | Next.js (Turbopack)

Promise.all with 50 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 1.593s (-2.8%) 4.011s (-5.9% 🟢) 2.418s 8 1.00x
🐘 Postgres Express 1.649s (+4.0%) 4.140s (-3.8%) 2.490s 8 1.04x
🐘 Postgres Next.js (Turbopack) 2.726s (-3.1%) 6.195s (+12.3% 🔺) 3.469s 6 1.71x
💻 Local Nitro 4.793s (-25.6% 🟢) 5.678s (-21.3% 🟢) 0.885s 6 3.01x
💻 Local Express 5.528s (-12.6% 🟢) 6.216s (-11.4% 🟢) 0.688s 5 3.47x
💻 Local Next.js (Turbopack) 6.311s (+15.3% 🔺) 7.015s (+16.6% 🔺) 0.704s 5 3.96x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 5.450s (+77.8% 🔺) 7.757s (+53.8% 🔺) 2.306s 4 1.00x
▲ Vercel Next.js (Turbopack) 5.911s (+69.8% 🔺) 7.598s (+46.1% 🔺) 1.687s 4 1.08x
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Express | Next.js (Turbopack)

Promise.race with 10 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 1.171s (-0.6%) 2.007s (~) 0.836s 15 1.00x
🐘 Postgres Nitro 1.171s (-2.9%) 2.007s (~) 0.836s 15 1.00x
🐘 Postgres Next.js (Turbopack) 1.184s (+1.3%) 2.009s (~) 0.824s 15 1.01x
💻 Local Nitro 1.329s (-6.5% 🟢) 2.006s (~) 0.677s 15 1.14x
💻 Local Express 1.353s (-5.9% 🟢) 2.006s (~) 0.653s 15 1.16x
💻 Local Next.js (Turbopack) 1.406s (~) 2.007s (~) 0.601s 15 1.20x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 3.618s (+75.3% 🔺) 5.678s (+62.3% 🔺) 2.061s 6 1.00x
▲ Vercel Next.js (Turbopack) 3.867s (-33.2% 🟢) 6.227s (-16.3% 🟢) 2.361s 5 1.07x
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Express | Next.js (Turbopack)

Promise.race with 25 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Next.js (Turbopack) 1.294s (~) 2.919s (~) 1.625s 11 1.00x
🐘 Postgres Nitro 1.316s (-4.1%) 2.151s (-13.0% 🟢) 0.835s 14 1.02x
🐘 Postgres Express 1.344s (~) 2.152s (-12.9% 🟢) 0.808s 14 1.04x
💻 Local Nitro 2.297s (-11.0% 🟢) 2.917s (-6.2% 🟢) 0.619s 11 1.78x
💻 Local Express 2.474s (+2.2%) 3.008s (-3.2%) 0.534s 10 1.91x
💻 Local Next.js (Turbopack) 2.808s (+6.5% 🔺) 3.310s (+10.0% 🔺) 0.502s 10 2.17x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 3.466s (-27.7% 🟢) 5.295s (-20.3% 🟢) 1.829s 6 1.00x
▲ Vercel Next.js (Turbopack) 4.410s (-7.1% 🟢) 6.470s (-6.9% 🟢) 2.061s 5 1.27x
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Express | Next.js (Turbopack)

Promise.race with 50 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 1.554s (-2.1%) 4.011s (-3.0%) 2.457s 8 1.00x
🐘 Postgres Nitro 1.562s (-5.9% 🟢) 4.141s (+3.2%) 2.580s 8 1.01x
🐘 Postgres Next.js (Turbopack) 3.221s (+12.7% 🔺) 6.013s (+2.8%) 2.792s 5 2.07x
💻 Local Nitro 5.370s (-20.8% 🟢) 6.015s (-16.7% 🟢) 0.645s 5 3.46x
💻 Local Express 6.604s (-1.4%) 7.516s (+4.1%) 0.912s 4 4.25x
💻 Local Next.js (Turbopack) 6.931s (+21.1% 🔺) 7.516s (+24.9% 🔺) 0.585s 4 4.46x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 3.862s (-17.6% 🟢) 5.905s (-6.2% 🟢) 2.043s 6 1.00x
▲ Vercel Next.js (Turbopack) 4.665s (+49.3% 🔺) 6.826s (+32.2% 🔺) 2.161s 5 1.21x
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Express | Next.js (Turbopack)

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 0.498s (-16.2% 🟢) 1.006s (-3.4%) 0.508s 60 1.00x
🐘 Postgres Express 0.512s (-4.0%) 1.006s (-1.7%) 0.494s 60 1.03x
🐘 Postgres Next.js (Turbopack) 0.548s (+7.2% 🔺) 1.007s (~) 0.459s 60 1.10x
💻 Local Nitro 0.562s (-4.2%) 1.021s (+1.6%) 0.460s 59 1.13x
💻 Local Express 0.610s (+6.5% 🔺) 1.039s (+3.4%) 0.429s 58 1.22x
💻 Local Next.js (Turbopack) 0.615s (+1.8%) 1.005s (-1.7%) 0.390s 60 1.23x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 3.538s (+26.4% 🔺) 5.458s (+21.7% 🔺) 1.920s 11 1.00x
▲ Vercel Next.js (Turbopack) 4.056s (+33.4% 🔺) 5.961s (+19.3% 🔺) 1.905s 11 1.15x
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Express | Next.js (Turbopack)

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 1.186s (-16.2% 🟢) 2.008s (-2.3%) 0.822s 45 1.00x
💻 Local Nitro 1.259s (-15.1% 🟢) 2.027s (+1.1%) 0.769s 45 1.06x
🐘 Postgres Next.js (Turbopack) 1.349s (+8.4% 🔺) 2.030s (~) 0.681s 45 1.14x
🐘 Postgres Express 1.364s (+9.1% 🔺) 2.053s (+2.3%) 0.689s 44 1.15x
💻 Local Express 1.375s (-5.5% 🟢) 2.006s (~) 0.631s 45 1.16x
💻 Local Next.js (Turbopack) 1.542s (+5.1% 🔺) 2.007s (~) 0.465s 45 1.30x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 8.812s (+36.3% 🔺) 10.826s (+34.7% 🔺) 2.013s 9 1.00x
▲ Vercel Next.js (Turbopack) 9.961s (+39.4% 🔺) 12.097s (+36.1% 🔺) 2.136s 8 1.13x
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Express | Next.js (Turbopack)

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 2.397s (-11.7% 🟢) 3.033s (-0.8%) 0.636s 40 1.00x
🐘 Postgres Next.js (Turbopack) 2.619s (+5.8% 🔺) 3.009s (-1.6%) 0.390s 40 1.09x
🐘 Postgres Express 2.660s (+2.5%) 3.137s (+1.7%) 0.477s 39 1.11x
💻 Local Nitro 2.737s (-15.0% 🟢) 3.136s (-21.8% 🟢) 0.399s 39 1.14x
💻 Local Express 3.020s (-7.0% 🟢) 3.465s (-12.9% 🟢) 0.445s 35 1.26x
💻 Local Next.js (Turbopack) 3.271s (~) 4.010s (-0.8%) 0.739s 30 1.36x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 19.671s (+42.7% 🔺) 22.203s (+43.6% 🔺) 2.532s 6 1.00x
▲ Vercel Next.js (Turbopack) 22.008s (+34.1% 🔺) 24.468s (+32.4% 🔺) 2.460s 5 1.12x
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Express | Next.js (Turbopack)

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Next.js (Turbopack) 0.172s (+2.4%) 1.006s (~) 0.835s 60 1.00x
🐘 Postgres Express 0.210s (~) 1.007s (~) 0.796s 60 1.23x
🐘 Postgres Nitro 0.228s (~) 1.006s (~) 0.778s 60 1.32x
💻 Local Nitro 0.418s (-10.7% 🟢) 1.004s (~) 0.585s 60 2.44x
💻 Local Express 0.433s (~) 1.004s (~) 0.571s 60 2.52x
💻 Local Next.js (Turbopack) 0.634s (+8.4% 🔺) 1.005s (~) 0.371s 60 3.69x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 1.951s (-5.9% 🟢) 3.850s (~) 1.898s 16 1.00x
▲ Vercel Next.js (Turbopack) 2.139s (+17.6% 🔺) 4.027s (+13.5% 🔺) 1.888s 16 1.10x
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Express | Next.js (Turbopack)

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Next.js (Turbopack) 0.275s (+2.7%) 1.018s (+1.2%) 0.742s 89 1.00x
🐘 Postgres Nitro 0.306s (-9.3% 🟢) 1.006s (~) 0.700s 90 1.11x
🐘 Postgres Express 0.316s (-2.1%) 1.006s (-1.1%) 0.690s 90 1.15x
💻 Local Nitro 1.945s (-8.8% 🟢) 2.469s (-8.1% 🟢) 0.523s 37 7.06x
💻 Local Express 2.133s (-1.3%) 2.766s (+1.1%) 0.633s 33 7.74x
💻 Local Next.js (Turbopack) 2.763s (-1.0%) 3.258s (~) 0.496s 28 10.03x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 1.927s (-21.8% 🟢) 3.826s (-5.7% 🟢) 1.899s 24 1.00x
▲ Vercel Next.js (Turbopack) 2.562s (+18.1% 🔺) 4.427s (+12.7% 🔺) 1.865s 21 1.33x
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Express | Next.js (Turbopack)

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 0.484s (-8.4% 🟢) 1.023s (-0.9%) 0.539s 118 1.00x
🐘 Postgres Next.js (Turbopack) 0.495s (+4.9%) 3.035s (+0.9%) 2.540s 40 1.02x
🐘 Postgres Express 0.526s (+2.0%) 1.068s (~) 0.542s 113 1.09x
💻 Local Nitro 8.436s (-14.9% 🟢) 9.485s (-14.0% 🟢) 1.049s 13 17.44x
💻 Local Express 9.236s (-8.4% 🟢) 10.192s (-8.4% 🟢) 0.956s 12 19.10x
💻 Local Next.js (Turbopack) 9.916s (+1.4%) 10.938s (+1.5%) 1.022s 11 20.50x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Next.js (Turbopack) 4.329s (-7.6% 🟢) 6.772s (+3.5%) 2.442s 19 1.00x
▲ Vercel Express 22.867s (+856.7% 🔺) 25.261s (+523.1% 🔺) 2.394s 15 5.28x
▲ Vercel Nitro ⚠️ missing - - - -

🔍 Observability: Next.js (Turbopack) | Express

Stream Benchmarks (includes TTFB metrics)
workflow with stream

💻 Local Development

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Nitro 1.123s (-3.0%) 2.004s (~) 0.008s (-33.1% 🟢) 2.014s (~) 0.892s 10 1.00x
🐘 Postgres Nitro 1.145s (-1.8%) 1.997s (~) 0.001s (-9.1% 🟢) 2.010s (~) 0.865s 10 1.02x
🐘 Postgres Next.js (Turbopack) 1.149s (~) 2.001s (~) 0.001s (+18.2% 🔺) 2.011s (~) 0.862s 10 1.02x
🐘 Postgres Express 1.159s (+0.8%) 1.998s (~) 0.001s (+20.0% 🔺) 2.010s (~) 0.851s 10 1.03x
💻 Local Express 1.163s (+0.9%) 2.004s (~) 0.010s (-14.2% 🟢) 2.017s (~) 0.855s 10 1.04x
💻 Local Next.js (Turbopack) 1.163s (-0.5%) 1.962s (~) 0.012s (-2.4%) 2.020s (~) 0.857s 10 1.04x

▲ Production (Vercel)

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Next.js (Turbopack) 2.270s (+9.9% 🔺) 3.623s (+5.0% 🔺) 3.615s (+156.4% 🔺) 7.846s (+45.1% 🔺) 5.576s 10 1.00x
▲ Vercel Express 2.383s (+16.4% 🔺) 3.756s (+22.4% 🔺) 3.236s (+84.7% 🔺) 7.544s (+39.6% 🔺) 5.162s 10 1.05x
▲ Vercel Nitro ⚠️ missing - - - - -

🔍 Observability: Next.js (Turbopack) | Express

stream pipeline with 5 transform steps (1MB)

💻 Local Development

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Nitro 1.450s (-6.6% 🟢) 2.007s (~) 0.012s (-0.8%) 2.022s (~) 0.571s 30 1.00x
💻 Local Express 1.518s (-2.0%) 2.009s (~) 0.011s (-17.8% 🟢) 2.023s (~) 0.505s 30 1.05x
🐘 Postgres Nitro 1.520s (-5.3% 🟢) 2.001s (~) 0.005s (+0.7%) 2.025s (~) 0.506s 30 1.05x
🐘 Postgres Express 1.544s (~) 2.003s (~) 0.005s (-3.6%) 2.026s (~) 0.482s 30 1.06x
💻 Local Next.js (Turbopack) 1.585s (~) 1.970s (~) 0.012s (-7.9% 🟢) 2.026s (~) 0.440s 30 1.09x
🐘 Postgres Next.js (Turbopack) 1.603s (+4.1%) 2.010s (~) 0.005s (-5.1% 🟢) 2.027s (~) 0.424s 30 1.11x

▲ Production (Vercel)

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Next.js (Turbopack) 5.754s (-3.1%) 7.307s (-0.9%) 0.381s (+19.6% 🔺) 8.245s (~) 2.492s 8 1.00x
▲ Vercel Express 5.951s (+1.1%) 7.324s (+1.6%) 0.236s (+9.2% 🔺) 8.180s (+3.8%) 2.229s 8 1.03x
▲ Vercel Nitro ⚠️ missing - - - - -

🔍 Observability: Next.js (Turbopack) | Express

10 parallel streams (1MB each)

💻 Local Development

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 0.778s (-5.3% 🟢) 1.046s (-4.8%) 0.000s (+42.1% 🔺) 1.059s (-5.3% 🟢) 0.281s 57 1.00x
🐘 Postgres Express 0.786s (+1.6%) 1.030s (-1.7%) 0.000s (+Infinity% 🔺) 1.057s (-0.7%) 0.270s 57 1.01x
🐘 Postgres Next.js (Turbopack) 0.966s (+5.3% 🔺) 1.327s (+4.0%) 0.000s (-100.0% 🟢) 1.347s (+4.9%) 0.381s 46 1.24x
💻 Local Nitro 1.103s (-16.3% 🟢) 1.806s (-8.9% 🟢) 0.000s (-54.4% 🟢) 1.809s (-8.9% 🟢) 0.705s 34 1.42x
💻 Local Express 1.228s (-3.1%) 2.013s (~) 0.000s (+50.0% 🔺) 2.016s (~) 0.787s 30 1.58x
💻 Local Next.js (Turbopack) 1.398s (+4.7%) 1.978s (~) 0.000s (+100.0% 🔺) 2.016s (~) 0.618s 30 1.80x

▲ Production (Vercel)

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 3.124s (-20.1% 🟢) 4.576s (-9.0% 🟢) 0.000s (-8.3% 🟢) 5.138s (-7.3% 🟢) 2.014s 12 1.00x
▲ Vercel Next.js (Turbopack) 3.312s (+0.6%) 4.719s (~) 0.000s (-100.0% 🟢) 5.299s (+0.9%) 1.988s 12 1.06x
▲ Vercel Nitro ⚠️ missing - - - - -

🔍 Observability: Express | Next.js (Turbopack)

fan-out fan-in 10 streams (1MB each)

💻 Local Development

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 1.559s (-18.7% 🟢) 2.172s (-13.0% 🟢) 0.000s (-14.3% 🟢) 2.187s (-13.3% 🟢) 0.628s 28 1.00x
🐘 Postgres Express 1.657s (-2.2%) 2.254s (~) 0.000s (-66.7% 🟢) 2.268s (~) 0.611s 27 1.06x
🐘 Postgres Next.js (Turbopack) 2.332s (+14.8% 🔺) 2.857s (+9.4% 🔺) 0.000s (-100.0% 🟢) 2.866s (+8.5% 🔺) 0.534s 21 1.50x
💻 Local Nitro 3.217s (-9.8% 🟢) 3.897s (-3.2%) 0.000s (-83.5% 🟢) 3.902s (-3.2%) 0.685s 16 2.06x
💻 Local Express 3.453s (-5.0%) 4.093s (-3.2%) 0.000s (-50.0% 🟢) 4.099s (-3.1%) 0.646s 15 2.21x
💻 Local Next.js (Turbopack) 3.770s (+2.9%) 4.191s (~) 0.000s (-56.3% 🟢) 4.236s (~) 0.466s 15 2.42x

▲ Production (Vercel)

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 4.735s (~) 6.306s (+9.8% 🔺) 0.000s (+11.1% 🔺) 6.954s (+11.2% 🔺) 2.219s 9 1.00x
▲ Vercel Next.js (Turbopack) 5.101s (+10.1% 🔺) 6.574s (+9.2% 🔺) 0.000s (-44.4% 🟢) 7.161s (+8.9% 🔺) 2.061s 9 1.08x
▲ Vercel Nitro ⚠️ missing - - - - -

🔍 Observability: Express | Next.js (Turbopack)

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World 🥇 Fastest Framework Wins
💻 Local Nitro 21/21
🐘 Postgres Nitro 13/21
▲ Vercel Express 16/21
Fastest World by Framework

Winner determined by most benchmark wins

Framework 🥇 Fastest World Wins
Express 🐘 Postgres 15/21
Next.js (Turbopack) 🐘 Postgres 15/21
Nitro 🐘 Postgres 14/21
Column Definitions
  • Workflow Time: Runtime reported by workflow (completedAt - createdAt) - primary metric
  • TTFB: Time to First Byte - time from workflow start until first stream byte received (stream benchmarks only)
  • Slurp: Time from first byte to complete stream consumption (stream benchmarks only)
  • Wall Time: Total testbench time (trigger workflow + poll for result)
  • Overhead: Testbench overhead (Wall Time - Workflow Time)
  • Samples: Number of benchmark iterations run
  • vs Fastest: How much slower compared to the fastest configuration for this benchmark

Worlds:

  • 💻 Local: In-memory filesystem world (local development)
  • 🐘 Postgres: PostgreSQL database world (local development)
  • ▲ Vercel: Vercel production/preview deployment
  • 🌐 Turso: Community world (local development)
  • 🌐 MongoDB: Community world (local development)
  • 🌐 Redis: Community world (local development)
  • 🌐 Jazz: Community world (local development)
  • 🌐 Redis: Community world (local development)
  • 🌐 Redis + BullMQ: Community world (local development)
  • 🌐 Cloudflare: Community world (local development)
  • 🌐 MySQL: Community world (local development)
  • 🌐 Azure: Community world (local development)
  • 🌐 NATS JetStream: Community world (local development)
  • 🌐 Upstash: Community world (local development)
  • 🌐 Platformatic: Community world (local development)

📋 View full workflow run

@socket-security

socket-security Bot commented Jun 18, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addednpm/​quickjs-wasi@​3.0.07810010094100

View full report

Comment thread packages/core/src/runtime/quickjs-entrypoint.ts Outdated
Comment thread packages/core/src/serialization/reducers/common-vm.ts Outdated
@pranaygp

Copy link
Copy Markdown
Contributor Author

e2e results (local, nextjs-turbopack + world-local, WORKFLOW_RUNTIME=quickjs)

Ran a representative subset across feature categories. 16 passed / 4 failed. Core functionality works; the failures are specific serialization / edge-case gaps in the QuickJS path, not the runtime fundamentals.

Passing: addTenWorkflow (app + pages router), promiseAll / promiseRace / promiseAny, hookWorkflow (+ public-webhook-not-resumable guard), sleepingWorkflow (app + pages), nullByteWorkflow, workflowAndStepMetadataWorkflow, customSerializationWorkflow (WORKFLOW_SERIALIZE/DESERIALIZE), errorSubclassRoundTripWorkflow, resilient start.

Failing (known follow-ups):

Test Symptom Diagnosis
webhookWorkflow a 4-byte length-frame prefix leaks into the deserialized payload hook/webhook payload byte-framing isn't stripped before the VM deserializes — same class of layering gap as the zstd-decompress fix, but for framed byte streams
parallelStepsThenWebhookWorkflow body mismatch likely the same framing issue
instanceMethodStepWorkflow Cannot stringify a function instance-method step this / closure serialization gap in the VM serde
stepWinsRaceWorkflow assertion step/sleep race ordering — possibly flaky, under review

Not yet exercised (likely need work, deferred): streams (readableStream / utf8Stream / writable forwarding — the VM has only a minimal ReadableStream stub), the abort / AbortController suite, advanced hook-conflict variants, experimental_setAttributes, and distributedAbortController.

Notes

  • Runtime correctness itself is covered by isolated unit/integration tests (quickjs-runtime.test.ts): completion, args, structured-value serde round-trip, suspension, failure, not-registered, and event-timeline Date.now() determinism.
  • Two real bugs were found and fixed during this e2e pass: sync-return workflows (Promise.resolve wrap) and compressed step-result payloads (host-side decodeForVm = decrypt + decompress).

pranaygp and others added 2 commits June 18, 2026 15:34
Review feedback:
- common-vm: use the native proposal-arraybuffer-base64 API
  (Uint8Array.toBase64 / fromBase64), which quickjs-wasi implements natively,
  instead of the btoa/atob dance (TooTallNate).
- quickjs-entrypoint: thread the builder-configured `namespace` into every
  getWorkflowQueueName() call (step queue + hook_conflict re-queue) so a
  namespaced deployment actually consumes them instead of hanging (VADE).

CI:
- Add .github/workflows/quickjs.yml with two jobs: `node` runs a curated e2e
  subset with WORKFLOW_RUNTIME=quickjs, `workerd` runs a committed smoke worker
  (packages/core/test/workerd-smoke) under wrangler/workerd that instantiates
  quickjs-wasi + native extensions (as pre-compiled WebAssembly.Modules) and
  evaluates core's VM serde bundle, proving the QuickJS VM runs on Workers.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…testing

* origin/main:
  perf(core): decouple workflow VM seed/clock from startedAt (#2525)
  [world-local] [core] Cache local dev server port per process (#2522)
  Show pending runs as gray animated stripes in trace viewer (#2520)
  [world-vercel] Route v4 event requests through global fetch (#2514)
  [core] Send workflowName with step events (#2511)
  Stamp run IDs on world spans (#2508)
  Reject empty-string hook tokens in createHook() (#2490)
  perf(core): cache compiled workflow-bundle vm.Script across replays (#2471)
  perf(core): drain consumable replay events synchronously (#2473)
  perf(core): lazy inline step start (save one world round-trip per step) (#2478)
`hookWorkflow` is a substring of `webhookWorkflow` /
`parallelStepsThenWebhookWorkflow`, so the -t pattern accidentally pulled in
the webhook tests whose byte-stream framing is a known QuickJS gap. Use
`\bhookWorkflow` so only the real hook tests run.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@skorfmann

Copy link
Copy Markdown

Being blocked by the vm thing right now as well - looking forward to this one

…testing

* origin/main:
  Version Packages (beta) (#2540)
  perf(core): memoize step return value hydration across inline replays (#2472)
  [core] Fix abort signal not reflected in subsequent step (replay-ordering flake) (#2412)
  Amend lazy discovery removal changeset (#2560)
  [docs] Document minimum SDK version for using hook.getConflict (#2423)
  Update default CODEOWNERS (#2556)
  Optimize and fix the default eager build mode (#2546)
  Revert "fix(world-vercel): cancel v4 event frame stream on early exit (#2547)" (#2554)
  [core] Turbo mode: fast-path the first invocation (#2526)
  Remove lazy discovery from workflow/next (#2545)
  fix(world-vercel): cancel v4 event frame stream on early exit (#2547)
  feat(docs): add eve and AI SDK to product switcher (#2543)
  [vitest] Fix local imports failing to load in test step bundles (#2351)
  [builders] Fix unicode-escape crash in workflow graph extraction (#2324)
  Version Packages (beta) (#2495)
  otel(world-vercel): inject trace context on v4 event requests (#2533)
  Bump undici to 7.28.0 (#2534)
  Default source maps to dev-on / prod-off (#2529)
  otel: nest linked-mode invocations under the delivery context (route + execution in one trace) (#2527)
  perf(core): parallel inline steps + optimistic lazy step start (#2516)

@VaguelySerious VaguelySerious left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI review: blocking issues found

Comment thread packages/core/src/runtime.ts Outdated
// queues steps through the same combined route — so step
// messages (incomingStepId) still hit executeStep above on
// re-entry. Return immediately after dispatch.
if (useQuickJSRuntime(workflowRun)) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI Review: Blocking

The QuickJS dispatch runs unconditionally here, but the run has already passed through the turbo branch above (isTurboEnabled() defaults to true). Turbo backgrounds run_started and synthesizes the run entity locally with startedAt: now (a fresh new Date(), line 851). QuickJS then seeds its PRNG and ULID timestamp from workflowRun.startedAt (quickjs-runtime.ts:487,533).

The consequence: the first (turbo) invocation seeds from the synthesized local now, while every resume invocation loads the run from storage and seeds from the persisted run_started timestamp. Different startedAt → different seed → different correlationIds/tokens. Reproduced in isolation (same workflow code, only startedAt differs):

expected 'step_01HK153XE84DWJH7VPQARYJ5AV'
to be    'step_01HK153X3VY6K45J95FWJN86AM'

This breaks the replay-dedup contract: a step_completed(C1) committed during the first invocation is never matched by the resume invocation (which regenerates C2), so steps re-create/duplicate or the run wedges; for hooks the regenerated token collides cross-run as hook_conflict (see the failing e2e job).

Separately, runReadyBarrier is never threaded into runWorkflowWithQuickJS, so the entrypoint's world.events.create calls can also race ahead of the backgrounded run_started — exactly what the node:vm path's awaitRunReady() exists to prevent.

Suggested fix: decide the QuickJS path before the turbo const (from the env var + runInput.executionContext.workflowRuntime) and exclude it from turbo (&& !quickjs). That makes the first invocation await a real run_started and seed from the persisted startedAt, fixing both the seed divergence and the write race.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great catch — fixed in 4dcc203 exactly as suggested. The QuickJS decision is now computed before the turbo const (from getWorkflowRuntimeFromEnv() + runInput?.executionContext?.workflowRuntime) and turbo gains && !quickjs. So a QuickJS first-invocation takes the non-turbo path: it awaits a real run_started and seeds the PRNG/ULID clock from the persisted startedAt — same seed as every resume — which also removes the run-ready write race (no backgrounded run_started for QuickJS). Verified locally with turbo enabled (the default): hookWorkflow + the curated subset pass.

# match webhookWorkflow / parallelStepsThenWebhookWorkflow, whose
# byte-stream framing is a known QuickJS gap (tracked separately).
pnpm vitest run packages/core/e2e/e2e.test.ts \
-t "addTenWorkflow|promiseAllWorkflow|promiseRaceWorkflow|promiseAnyWorkflow|\bhookWorkflow|sleepingWorkflow|nullByteWorkflow|workflowAndStepMetadataWorkflow|customSerializationWorkflow|errorSubclassRoundTripWorkflow|resilient start"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI Review: Blocking

The QuickJS Runtime (Node) job is currently failing on hookWorkflow and hookWorkflow is not resumable via public webhook endpoint with Hook token "…" is already in use by another workflow. Both are in this curated subset (\bhookWorkflow), i.e. claimed-supported. The job's recent history is flaky (1 pass / 3 fail), which is the signature of the determinism race flagged in runtime.ts — under turbo the resume invocation regenerates a different hook token/correlationId than the first invocation committed.

Shouldn't merge with this job red on a feature the curated subset asserts as working. Fixing the turbo/seed integration should resolve these; please confirm the job goes green (not just re-run-to-pass).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right — this was the visible symptom of the turbo/seed divergence (fixed in 4dcc203, see the runtime.ts thread). Reproduced locally post-merge (turbo on by default): hookWorkflow + hookWorkflow is not resumable via public webhook endpoint were failing; after excluding QuickJS from turbo they pass (8/8 in the matched subset, 0 fail) — confirmed green, not re-run-to-pass. The fresh CI run will reflect it.

// also matches because the PRNG is seeded the same way) and the
// world's EntityConflictError on `events.create` dedups one of each
// pair. Use `startedAt` (constant per-run) — distinctness across
// resumptions comes from the cursor mixed into the seedrandom seed,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI Review: Note

This comment (and the matching note in vm-bundle-entry.ts) references "the cursor mixed into the seedrandom seed," but the seed at line 487 is [runId, workflowName, startedAt] — there is no cursor (snapshots were dropped in this port). The comment misdescribes the actual determinism model; it reads like carryover from the snapshot runtime in #1300. Worth correcting since the seed contents are load-bearing for replay dedup.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 4dcc203 — that comment was carried over from the snapshot port. Rewrote it: there's no cursor in the seed (snapshots were dropped); the seed is runId:workflowName:startedAt and full-replay-from-scratch regenerates the identical correlationId sequence every invocation, which is the dedup contract.

// Process events and drain jobs in a loop. Events may resolve promises
// that unblock workflow code, which then creates NEW resolvers for
// subsequent events; re-processing matches those against delivered events.
let maxIterations = 100;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI Review: Note

If the replay loop exhausts maxIterations while madeProgress is still true, it silently falls through to checkWorkflowState and can suspend with no new ops queued — a silent wedge with no log or error. I tested a 120-step sequential chain and it drains in 1–2 passes (each processEvents pass resolves a full sequential chain), so this is low severity in practice. Still, a runtimeLogger.warn when the cap is hit would turn a silent footgun into a diagnosable one.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 4dcc203 — if the loop exhausts its iteration budget while still making progress, it now returns a failed result (and logs an error) instead of silently calling checkWorkflowState on a half-settled VM. Fails loud → queue retries / surfaces a real error rather than wedging.

* Auto-generated by scripts/build-quickjs-assets.js
* Do not edit manually.
*
* Contains base64-encoded quickjs-wasi WASM binary and native C extension

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI Review: Note

The generated module embeds ~3.3 MB of base64-encoded WASM/.so assets, which tsc compiles into dist and ships in the @workflow/core npm tarball for all consumers — even though node:vm is the default and the asset is only dynamically imported at runtime. The dynamic import defers runtime load, not package/tarball size. Worth confirming the bundle-size impact is acceptable, or splitting the assets into an optional dependency that only QuickJS users install.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI Review: Note (follow-up — corrects the framing above)

I built the vite workbench for both the plain Node and the Vercel targets and inspected the output; the bundle impact is narrower than my note implied.

It is code-split into a lazy chunk, not inlined into the entry, and not tree-shaken away (the bundler can't prove the WORKFLOW_RUNTIME / per-run flag is never set, so the chunk is always emitted — but it's loaded only via the dynamic import() at runtime.ts:966).

vercel build.vercel/output/functions/__server.func/ (Nitro, nodejs24.x):

.vc-config.json   →  "handler": "index.mjs"
index.mjs                     2.2 MB   ← no WASM inlined; references the chunk via import("./_chunks/quickjs-entrypoint.mjs")
_chunks/quickjs-entrypoint.mjs  3.3 MB   ← the blob, its own chunk (~41% of the 8.0 MB function)

So, splitting the two senses of "in the bundle":

  • Same deployed function artifact as index.mjs? Yes — it ships inside __server.func/, counts toward the function's deployed size and toward Vercel's function size limit.
  • Same JS module / parsed at cold start? No — index.mjs loads it only via dynamic import(). The launcher boots index.mjs; the 3.3 MB chunk isn't read or compiled until a run actually selects QuickJS.

Net for node:vm-only users (the default): no main-bundle bloat and no cold-start parse/CPU penalty — the cost is purely ~3.3 MB of dead weight in every deployed function artifact regardless of runtime. Recoverable only by making quickjs-wasi an optional/peer dependency of @workflow/core rather than a hard dep. Downgrading this from a bundle concern to a deploy-artifact-size one.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the detailed follow-up — agreed with the corrected framing: code-split lazy chunk, so node:vm-only users pay no main-bundle or cold-start cost, just ~3.3 MB of dead weight in the deployed function artifact (counts toward Vercel's function size limit). For this PR I'm leaving it as a hard dep since the runtime is opt-in/experimental, but you're right that the proper fix before any broad rollout is to make quickjs-wasi (and the generated assets) an optional/peer dependency that only QuickJS users install. I'll track that with the other Workers follow-ups (asset-loading variant + wasm-compression) rather than expand this PR's scope.

const cid = event.correlationId;
if (!cid) continue;

const escapedCid = cid.replace(/"/g, '\\"');

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI Review: Nit

escapedCid only escapes ", not backslashes or newlines, before interpolating into evalCode strings. Safe today because correlationIds are step_/wait_/hook_ + ULID (Crockford base32, no special chars), but it's fragile if the ID format ever changes. Consider JSON.stringify(cid) for the interpolated string literals, consistent with how event.eventId is already handled.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hardened in 4dcc203escapedCid now escapes backslashes as well as quotes (cid.replace(/[\\"]/g, '\\$&')). Agreed it's safe-by-construction today (ULID Crockford base32), but this keeps the interpolation safe if the id format ever changes.

pranaygp and others added 3 commits June 22, 2026 14:47
Blocking (AI review): the turbo fast-path synthesizes the run with a local
`startedAt: now` and backgrounds run_started, but the QuickJS runtime seeds its
PRNG/ULID clock from `startedAt`. A turbo first-invocation would seed from a
transient `now` while every resume seeds from the persisted run_started
timestamp → divergent correlationIds/hook tokens, breaking replay dedup (the
failing hookWorkflow e2e). Decide the QuickJS path before the `turbo` const
(from env + runInput.executionContext) and exclude it from turbo, so the first
invocation awaits a real run_started and seeds from the persisted startedAt.
Verified locally: hookWorkflow + the curated subset pass under quickjs+turbo.

Notes:
- escape backslashes as well as quotes when interpolating correlationId into
  VM evalCode strings (defensive).
- fail loud instead of silently wedging if event processing doesn't converge
  within the iteration budget.
- fix a stale comment referencing the (removed) snapshot events cursor in the
  PRNG seed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…testing

* origin/main:
  Trace /flow route initialization (#2592)
  Version Packages (beta) (#2591)
  feat(web): add trace step shortcut helper (#2582)
  [web-shared] reskin json viewer (no duplicates, better colours and navigation) (#2434)
  Send occurredAt with workflow events (#2580)
  docs: use actual eve logo and tidy OSS nav dropdown (#2586)
  Display occurredAt in trace details (#2581)
  fix(next): discover root entrypoints (#2564)
  [core] Turbo: skip the unused run_started event-log preload (#2569)
  fix(next): prewarm SWC plugin cache (#2538)
  [world-vercel] Use v3 stream endpoint (supports transparent reconnect on timeout) (#2424)
  [core] Retry stream reopen against the reconnect budget (#2334)
  Add Platformatic World to worlds-manifest.json (#1450)
  docs: derive section landing-page cards from the page tree + lint drift (#2567)
- start(): always record the resolved runtime (node-vm | quickjs) on the run's
  executionContext (defaulting to node-vm), so every run captures which runtime
  it uses and the choice is pinned per-run.
- runtime.ts: tag the workflow execution span with `workflow.runtime`
  (Attribute.WorkflowRuntime), resolved per-invocation via useQuickJSRuntime so
  it's authoritative on resumes — for both the node:vm and QuickJS paths.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@TooTallNate

Copy link
Copy Markdown
Member

Closing as superseded: the opt-in QuickJS runtime shipped via #3048 and successors (WORKFLOW_VM=quickjs, since made the default-track work in #3253), and the serde/bundle machinery here was replaced by host-side serde (#3263).

One genuinely uncovered piece worth extracting before this is forgotten: the workerd/Cloudflare smoke test (packages/core/test/workerd-smoke/ + the workerd CI job). Nothing on main or the remaining stack tests Workers compatibility, and "runs where node:vm doesn't (e.g. Cloudflare Workers)" is stated motivation in main's own vm-mode.ts docstring. Two details from this PR matter for any revival:

  • workerd bans runtime WebAssembly.compile(bytes) — this PR imported quickjs.wasm + the native extensions as pre-compiled WebAssembly.Modules via wrangler's CompiledWasm rules. Main's quickjs-assets.generated.ts (base64 → compile at runtime) would fail there, so asset loading needs a workerd-specific path.
  • The in-worker checks (VM boot, seeded PRNG/clock bootstrap, serde round-trip, workflow registration/invocation, microtask drain) are still the right smoke coverage, but the worker as written evals the now-removed VM serde bundle — it would need rewriting against the host-side serde.

Recommend filing an issue for workerd compatibility testing rather than keeping this PR open for it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants