Skip to content

Add CommonJS require() support for class serialization detection in SWC plugin - #1144

Merged
TooTallNate merged 3 commits into
mainfrom
02-20-add_commonjs_require_support_for_class_serialization_detection_in_swc_plugin
Mar 23, 2026
Merged

Add CommonJS require() support for class serialization detection in SWC plugin#1144
TooTallNate merged 3 commits into
mainfrom
02-20-add_commonjs_require_support_for_class_serialization_detection_in_swc_plugin

Conversation

@TooTallNate

Copy link
Copy Markdown
Member

The SWC compiler plugin now detects classes with custom serialization methods (WORKFLOW_SERIALIZE / WORKFLOW_DESERIALIZE) when symbols are obtained via CommonJS require() calls, in addition to the existing ESM import and Symbol.for() patterns.

This handles code that has been pre-compiled from ESM to CommonJS by tools like TypeScript (tsc), esbuild, or tsup, which transform:

import{WORKFLOW_SERIALIZE}from"@workflow/serde"

into either:

constserde_1=require("@workflow/serde")// namespace requireconst{WORKFLOW_SERIALIZE}=require(...)// destructured require

Both patterns are now recognized during the identifier collection phase, and classes using them are properly registered with registerSerializationClass().

@changeset-bot

changeset-botBot commented Feb 20, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 14ea197

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

This PR includes changesets to release 17 packages
NameType
@workflow/swc-pluginPatch
@workflow/astroPatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nestPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
workflowPatch
@workflow/vitePatch
@workflow/vitestPatch
@workflow/world-testingPatch
@workflow/nuxtPatch
@workflow/aiPatch
@workflow/corePatch
@workflow/web-sharedPatch

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

@vercel

vercelBot commented Feb 20, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Feb 20, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production780067847
✅ 💻 Local Development7820142924
✅ 📦 Local Production7820142924
✅ 🐘 Local Postgres7820142924
✅ 🪟 Windows720577
❌ 🌍 Community Worlds1185621195
✅ 📋 Other198033231
Total3514565524122

❌ Failed Tests

🌍 Community Worlds (56 failed)

mongodb (3 failed):

  • hookWorkflow is not resumable via public webhook endpoint | wrun_01KMDYE4RWSYQ6538DVH70M5P0
  • webhookWorkflow | wrun_01KMDYED4N9B1F4PF1X6F86J26
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously | wrun_01KMDYMVS1CFTACB4081WS8K9V

redis (2 failed):

  • hookWorkflow is not resumable via public webhook endpoint | wrun_01KMDYE4RWSYQ6538DVH70M5P0
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously | wrun_01KMDYMVS1CFTACB4081WS8K9V

turso (51 failed):

  • addTenWorkflow | wrun_01KMDYCYXATZBJYFAPPKVRZKDZ
  • addTenWorkflow | wrun_01KMDYCYXATZBJYFAPPKVRZKDZ
  • wellKnownAgentWorkflow (.well-known/agent) | wrun_01KMDYEKVFEVFMCXC32DMPJ3KB
  • should work with react rendering in step
  • promiseAllWorkflow | wrun_01KMDYD5F67YMHR6KTJF5YFYM3
  • promiseRaceWorkflow | wrun_01KMDYDBXWV7065WDZ655A6X2Y
  • promiseAnyWorkflow | wrun_01KMDYDEFBS29N1Y4E04XS0PHS
  • importedStepOnlyWorkflow | wrun_01KMDYF31ZMHDYKRR9870BQJFE
  • hookWorkflow | wrun_01KMDYDTDYK5ZZTVGBV7PBPJAZ
  • hookWorkflow is not resumable via public webhook endpoint | wrun_01KMDYE4RWSYQ6538DVH70M5P0
  • webhookWorkflow | wrun_01KMDYED4N9B1F4PF1X6F86J26
  • sleepingWorkflow | wrun_01KMDYEKNPTWHR8GD4VJXRHSWR
  • parallelSleepWorkflow | wrun_01KMDYF1MMFSRK8683TXYGPEFF
  • nullByteWorkflow | wrun_01KMDYF6AS6SP633VT5B7JZ0Q8
  • workflowAndStepMetadataWorkflow | wrun_01KMDYF8B1WBHNCPZKHFYCH5G0
  • fetchWorkflow | wrun_01KMDYH6MPPT3CPGBXX02XNQTP
  • promiseRaceStressTestWorkflow | wrun_01KMDYH9Y93SDT6D7TEE6CMKHT
  • error handling error propagation workflow errors nested function calls preserve message and stack trace
  • error handling error propagation workflow errors cross-file imports preserve message and stack trace
  • error handling error propagation step errors basic step error preserves message and stack trace
  • error handling error propagation step errors cross-file step error preserves message and function names in stack
  • error handling retry behavior regular Error retries until success
  • error handling retry behavior FatalError fails immediately without retries
  • error handling retry behavior RetryableError respects custom retryAfter delay
  • error handling retry behavior maxRetries=0 disables retries
  • error handling catchability FatalError can be caught and detected with FatalError.is()
  • hookCleanupTestWorkflow - hook token reuse after workflow completion | wrun_01KMDYM7FJ0MWDXPS56VM7PF4D
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously | wrun_01KMDYMVS1CFTACB4081WS8K9V
  • hookDisposeTestWorkflow - hook token reuse after explicit disposal while workflow still running | wrun_01KMDYNGM0E0ZXFK8E2CQ9CYZV
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars) | wrun_01KMDYP5F3H7NXZ9W494MXF8EQ
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument | wrun_01KMDYPFQ0MK8D4B36398E2S6A
  • closureVariableWorkflow - nested step functions with closure variables | wrun_01KMDYPN9T3F9HBR5W1TQSBAVH
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step | wrun_01KMDYPQBVNH7GG5Y8ZDS8B1P3
  • health check (queue-based) - workflow and step endpoints respond to health check messages
  • pathsAliasWorkflow - TypeScript path aliases resolve correctly | wrun_01KMDYQ7ZKSHZ1DF03RZ64NGXD
  • Calculator.calculate - static workflow method using static step methods from another class | wrun_01KMDYQEMCVB21XR91WVHJEGK4
  • AllInOneService.processNumber - static workflow method using sibling static step methods | wrun_01KMDYQP3ST1XWR4FTRNPGNYE4
  • ChainableService.processWithThis - static step methods using this to reference the class | wrun_01KMDYQWMSK6ZYMFNY7ME546A9
  • thisSerializationWorkflow - step function invoked with .call() and .apply() | wrun_01KMDYR36AW71R74TKNQEA26QC
  • customSerializationWorkflow - custom class serialization with WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE | wrun_01KMDYR9J9RVJCAWSQG76N5656
  • instanceMethodStepWorkflow - instance methods with "use step" directive | wrun_01KMDYRH7WBXH2XGDZPC5CXB49
  • crossContextSerdeWorkflow - classes defined in step code are deserializable in workflow context | wrun_01KMDYRVMSG67Q29E0JT62SCPB
  • stepFunctionAsStartArgWorkflow - step function reference passed as start() argument | wrun_01KMDYS38DF4PJE3SJHH7YV0GQ
  • cancelRun - cancelling a running workflow | wrun_01KMDYSB4MVW6TNAVS70ZDC36V
  • cancelRun via CLI - cancelling a running workflow | wrun_01KMDYSM88CM0VKBMVABG1ZRM0
  • pages router addTenWorkflow via pages router
  • pages router promiseAllWorkflow via pages router
  • pages router sleepingWorkflow via pages router
  • hookWithSleepWorkflow - hook payloads delivered correctly with concurrent sleep | wrun_01KMDYT02V9K533MN8PA31072V
  • sleepInLoopWorkflow - sleep inside loop with steps actually delays each iteration | wrun_01KMDYTKS4H2ZFNHJC3AH7D70B
  • sleepWithSequentialStepsWorkflow - sequential steps work with concurrent sleep (control) | wrun_01KMDYTZMDM1Q8370F5R6NNHJV

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro7007
✅ example7007
✅ express7007
✅ fastify7007
✅ hono7007
✅ nextjs-turbopack7502
✅ nextjs-webpack7502
✅ nitro7007
✅ nuxt7007
✅ sveltekit7007
✅ vite7007
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable66011
✅ express-stable66011
✅ fastify-stable66011
✅ hono-stable66011
✅ nextjs-turbopack-canary55022
✅ nextjs-turbopack-stable7205
✅ nextjs-webpack-canary55022
✅ nextjs-webpack-stable7205
✅ nitro-stable66011
✅ nuxt-stable66011
✅ sveltekit-stable66011
✅ vite-stable66011
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable66011
✅ express-stable66011
✅ fastify-stable66011
✅ hono-stable66011
✅ nextjs-turbopack-canary55022
✅ nextjs-turbopack-stable7205
✅ nextjs-webpack-canary55022
✅ nextjs-webpack-stable7205
✅ nitro-stable66011
✅ nuxt-stable66011
✅ sveltekit-stable66011
✅ vite-stable66011
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable66011
✅ express-stable66011
✅ fastify-stable66011
✅ hono-stable66011
✅ nextjs-turbopack-canary55022
✅ nextjs-turbopack-stable7205
✅ nextjs-webpack-canary55022
✅ nextjs-webpack-stable7205
✅ nitro-stable66011
✅ nuxt-stable66011
✅ sveltekit-stable66011
✅ vite-stable66011
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack7205
❌ 🌍 Community Worlds
AppPassedFailedSkipped
✅ mongodb-dev302
❌ mongodb5235
✅ redis-dev302
❌ redis5325
✅ turso-dev302
❌ turso4515
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable66011
✅ e2e-local-postgres-nest-stable66011
✅ e2e-local-prod-nest-stable66011

📋 View full workflow run

@github-actions

github-actionsBot commented Feb 20, 2026

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

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

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro0.037s (-21.3% 🟢)1.005s (~)0.967s101.00x
💻 LocalExpress0.043s (-4.5%)1.005s (~)0.962s101.14x
🐘 PostgresNitro0.047s (-31.9% 🟢)1.012s (~)0.965s101.26x
🐘 PostgresExpress0.048s (-32.2% 🟢)1.012s (-0.5%)0.964s101.29x
💻 LocalNext.js (Turbopack)0.051s1.006s0.956s101.35x
🌐 RedisNext.js (Turbopack)0.054s1.005s0.952s101.44x
🐘 PostgresNext.js (Turbopack)0.062s1.012s0.950s101.65x
workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.097s (-2.8%)2.005s (~)0.908s101.00x
🌐 RedisNext.js (Turbopack)1.120s2.007s0.887s101.02x
🐘 PostgresNitro1.124s (-1.6%)2.012s (~)0.888s101.02x
💻 LocalNext.js (Turbopack)1.124s2.006s0.883s101.02x
🐘 PostgresExpress1.126s (-1.1%)2.012s (~)0.886s101.03x
💻 LocalExpress1.131s (~)2.005s (~)0.874s101.03x
🐘 PostgresNext.js (Turbopack)1.143s2.013s0.869s101.04x
workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro10.635s (-2.4%)11.023s (~)0.388s31.00x
🌐 RedisNext.js (Turbopack)10.680s11.025s0.345s31.00x
🐘 PostgresNitro10.689s (-2.4%)11.036s (~)0.348s31.01x
🐘 PostgresExpress10.705s (-1.8%)11.038s (~)0.333s31.01x
💻 LocalNext.js (Turbopack)10.847s11.025s0.178s31.02x
💻 LocalExpress10.934s (~)11.023s (~)0.089s31.03x
🐘 PostgresNext.js (Turbopack)10.996s11.382s0.386s31.03x
workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)14.091s15.033s0.941s41.00x
🐘 PostgresExpress14.123s (-3.1%)15.042s (~)0.919s41.00x
🐘 PostgresNitro14.129s (-4.7%)15.039s (~)0.910s41.00x
💻 LocalNitro14.183s (-4.9%)15.029s (~)0.847s41.01x
🐘 PostgresNext.js (Turbopack)14.580s15.049s0.469s41.03x
💻 LocalNext.js (Turbopack)14.703s15.031s0.328s41.04x
💻 LocalExpress14.911s (-0.6%)15.029s (-3.2%)0.118s41.06x
workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)13.030s13.457s0.427s71.00x
🐘 PostgresExpress13.096s (-6.5% 🟢)14.038s (-3.0%)0.942s71.01x
🐘 PostgresNitro13.119s (-8.7% 🟢)14.038s (-6.7% 🟢)0.919s71.01x
🐘 PostgresNext.js (Turbopack)14.145s14.759s0.613s71.09x
💻 LocalNitro14.789s (-10.1% 🟢)15.026s (-11.8% 🟢)0.237s61.14x
💻 LocalNext.js (Turbopack)16.157s17.033s0.875s61.24x
💻 LocalExpress16.514s (-1.0%)17.029s (~)0.516s61.27x
Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.228s (-3.0%)2.011s (~)0.783s151.00x
🐘 PostgresNitro1.231s (-4.8%)2.011s (~)0.780s151.00x
🌐 RedisNext.js (Turbopack)1.266s2.007s0.741s151.03x
🐘 PostgresNext.js (Turbopack)1.286s2.011s0.725s151.05x
💻 LocalNitro1.472s (-0.8%)2.005s (~)0.534s151.20x
💻 LocalExpress1.503s (~)2.006s (~)0.503s151.22x
💻 LocalNext.js (Turbopack)1.524s2.006s0.482s151.24x
Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express2.341s (-4.9%)3.012s (~)0.671s101.00x
🐘 PostgresNitro2.353s (-4.8%)3.013s (~)0.660s101.01x
🌐 RedisNext.js (Turbopack)2.478s3.009s0.531s101.06x
🐘 PostgresNext.js (Turbopack)2.484s3.015s0.531s101.06x
💻 LocalNitro2.551s (-11.2% 🟢)3.007s (-3.2%)0.456s101.09x
💻 LocalExpress2.970s (-0.9%)3.308s (-7.2% 🟢)0.338s101.27x
💻 LocalNext.js (Turbopack)2.995s3.309s0.314s101.28x
Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express3.519s (-1.9%)4.014s (~)0.495s81.00x
🐘 PostgresNitro3.523s (-3.0%)4.015s (~)0.492s81.00x
🐘 PostgresNext.js (Turbopack)3.844s4.017s0.173s81.09x
🌐 RedisNext.js (Turbopack)4.117s4.727s0.610s71.17x
💻 LocalNitro7.025s (-13.5% 🟢)7.514s (-14.3% 🟢)0.489s42.00x
💻 LocalExpress8.144s (-3.9%)8.769s (-2.8%)0.625s42.31x
💻 LocalNext.js (Turbopack)8.563s9.019s0.456s42.43x
Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.207s (-4.1%)2.011s (~)0.803s151.00x
🐘 PostgresNitro1.216s (-4.7%)2.010s (~)0.794s151.01x
🌐 RedisNext.js (Turbopack)1.252s2.007s0.755s151.04x
🐘 PostgresNext.js (Turbopack)1.254s2.011s0.757s151.04x
💻 LocalNitro1.484s (-4.9%)2.006s (~)0.522s151.23x
💻 LocalNext.js (Turbopack)1.515s2.005s0.490s151.25x
💻 LocalExpress1.550s (+0.9%)2.005s (~)0.455s151.28x
Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro2.342s (-4.7%)3.012s (~)0.670s101.00x
🐘 PostgresExpress2.353s (-4.0%)3.012s (~)0.659s101.00x
🐘 PostgresNext.js (Turbopack)2.474s3.013s0.539s101.06x
🌐 RedisNext.js (Turbopack)2.498s3.009s0.511s101.07x
💻 LocalNitro2.924s (-3.4%)3.208s (-12.7% 🟢)0.283s101.25x
💻 LocalNext.js (Turbopack)3.012s3.675s0.663s91.29x
💻 LocalExpress3.090s (+1.9%)3.885s (+3.4%)0.795s81.32x
Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express3.515s (-2.3%)4.013s (~)0.497s81.00x
🐘 PostgresNitro3.518s (-2.7%)4.014s (~)0.496s81.00x
🐘 PostgresNext.js (Turbopack)3.841s4.018s0.177s81.09x
🌐 RedisNext.js (Turbopack)4.031s4.583s0.553s71.15x
💻 LocalNitro7.511s (-13.1% 🟢)8.016s (-11.2% 🟢)0.506s42.14x
💻 LocalExpress8.608s (-5.0%)9.020s (-7.7% 🟢)0.412s42.45x
💻 LocalNext.js (Turbopack)9.214s9.772s0.557s42.62x
workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)0.601s1.005s0.404s601.00x
🐘 PostgresExpress0.642s (-25.8% 🟢)1.009s (-1.7%)0.366s601.07x
🐘 PostgresNitro0.657s (-26.8% 🟢)1.008s (-3.4%)0.351s601.09x
💻 LocalNitro0.672s (-29.7% 🟢)1.004s (-1.7%)0.332s601.12x
🐘 PostgresNext.js (Turbopack)0.870s1.009s0.140s601.45x
💻 LocalNext.js (Turbopack)0.878s1.057s0.179s571.46x
💻 LocalExpress0.973s (-1.0%)1.114s (+1.7%)0.140s551.62x
workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)1.460s2.006s0.546s451.00x
🐘 PostgresExpress1.553s (-26.6% 🟢)2.010s (-33.3% 🟢)0.457s451.06x
🐘 PostgresNitro1.563s (-27.7% 🟢)2.032s (-31.8% 🟢)0.470s451.07x
🐘 PostgresNext.js (Turbopack)2.127s2.948s0.821s311.46x
💻 LocalNitro2.193s (-25.8% 🟢)3.007s (-3.3%)0.815s301.50x
💻 LocalNext.js (Turbopack)2.736s3.008s0.272s301.87x
💻 LocalExpress2.966s (-1.6%)3.222s (-9.2% 🟢)0.255s282.03x
workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)3.077s3.437s0.360s351.00x
🐘 PostgresNitro3.153s (-27.3% 🟢)4.012s (-20.0% 🟢)0.859s301.02x
🐘 PostgresExpress3.188s (-25.2% 🟢)4.012s (-19.3% 🟢)0.824s301.04x
🐘 PostgresNext.js (Turbopack)4.345s5.015s0.670s241.41x
💻 LocalNitro7.218s (-20.1% 🟢)7.827s (-17.4% 🟢)0.610s162.35x
💻 LocalNext.js (Turbopack)8.797s9.232s0.434s142.86x
💻 LocalExpress8.995s (-1.7%)9.479s (-3.1%)0.484s132.92x
workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.243s (-19.7% 🟢)1.008s (~)0.766s601.00x
🐘 PostgresNitro0.245s (-16.9% 🟢)1.009s (~)0.764s601.01x
🌐 RedisNext.js (Turbopack)0.260s1.005s0.744s601.07x
🐘 PostgresNext.js (Turbopack)0.287s1.010s0.723s601.18x
💻 LocalNitro0.576s (-3.7%)1.004s (~)0.428s602.37x
💻 LocalExpress0.581s (-3.7%)1.004s (~)0.424s602.39x
💻 LocalNext.js (Turbopack)0.585s1.004s0.420s602.41x
workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.419s (-20.5% 🟢)1.009s (~)0.589s901.00x
🐘 PostgresNitro0.424s (-21.4% 🟢)1.009s (~)0.585s901.01x
🐘 PostgresNext.js (Turbopack)0.590s1.033s0.443s881.41x
🌐 RedisNext.js (Turbopack)1.194s2.006s0.812s452.85x
💻 LocalNitro2.345s (-5.2% 🟢)3.007s (~)0.662s305.59x
💻 LocalExpress2.498s (-3.2%)3.009s (~)0.510s305.96x
💻 LocalNext.js (Turbopack)2.664s3.009s0.345s306.36x
workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.606s (-35.5% 🟢)1.009s (-15.4% 🟢)0.403s1191.00x
🐘 PostgresExpress0.620s (-30.9% 🟢)1.009s (-3.1%)0.389s1191.02x
🐘 PostgresNext.js (Turbopack)1.001s1.477s0.476s821.65x
🌐 RedisNext.js (Turbopack)2.883s3.085s0.202s394.76x
💻 LocalNitro10.189s (-8.3% 🟢)10.855s (-6.9% 🟢)0.666s1216.83x
💻 LocalExpress11.127s (-1.4%)11.845s (-1.5%)0.718s1118.38x
💻 LocalNext.js (Turbopack)11.287s11.753s0.465s1118.64x
Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro0.140s (-28.6% 🟢)1.003s (~)0.009s (-26.3% 🟢)1.014s (~)0.874s101.00x
🐘 PostgresExpress0.164s (-24.1% 🟢)0.999s (+0.5%)0.001s (-8.3% 🟢)1.013s (~)0.848s101.17x
🐘 PostgresNitro0.167s (-30.3% 🟢)0.999s (+0.7%)0.001s (-21.4% 🟢)1.013s (~)0.846s101.19x
🌐 RedisNext.js (Turbopack)0.169s1.000s0.001s1.008s0.838s101.21x
💻 LocalNext.js (Turbopack)0.178s1.001s0.012s1.018s0.840s101.27x
💻 LocalExpress0.199s (-2.4%)1.003s (~)0.012s (-2.5%)1.017s (~)0.818s101.42x
🐘 PostgresNext.js (Turbopack)0.224s1.001s0.001s1.013s0.789s101.60x
stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)0.447s0.999s0.003s1.012s0.565s601.00x
🐘 PostgresNitro0.563s (-22.1% 🟢)1.005s (~)0.003s (-13.6% 🟢)1.023s (~)0.460s591.26x
💻 LocalNitro0.569s (-21.7% 🟢)1.008s (~)0.009s (-6.2% 🟢)1.021s (~)0.452s591.27x
🐘 PostgresExpress0.610s (-12.0% 🟢)1.006s (~)0.005s (+20.4% 🔺)1.028s (~)0.417s591.37x
💻 LocalNext.js (Turbopack)0.672s1.008s0.010s1.024s0.352s591.50x
💻 LocalExpress0.723s (-6.9% 🟢)1.009s (-1.7%)0.009s (-12.1% 🟢)1.022s (-1.8%)0.300s591.62x
🐘 PostgresNext.js (Turbopack)0.760s1.026s0.008s1.050s0.290s591.70x
10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)0.916s1.016s0.000s1.022s0.106s591.00x
🐘 PostgresNitro0.988s (+7.3% 🔺)1.384s (+30.2% 🔺)0.000s (-36.4% 🟢)1.398s (+29.3% 🔺)0.410s441.08x
🐘 PostgresExpress1.066s (+19.3% 🔺)1.664s (+53.7% 🔺)0.000s (+205.6% 🔺)1.677s (+51.8% 🔺)0.612s361.16x
💻 LocalNitro1.140s (-6.9% 🟢)2.017s (~)0.000s (-38.5% 🟢)2.020s (~)0.880s301.24x
🐘 PostgresNext.js (Turbopack)1.175s1.909s0.000s1.930s0.755s321.28x
💻 LocalExpress1.235s (-1.8%)2.020s (~)0.000s (-35.3% 🟢)2.023s (~)0.788s301.35x
💻 LocalNext.js (Turbopack)1.295s2.019s0.000s2.024s0.729s301.41x
fan-out fan-in 10 streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.886s (~)2.223s (~)0.000s (-100.0% 🟢)2.233s (~)0.347s271.00x
🌐 RedisNext.js (Turbopack)1.963s2.180s0.000s2.186s0.223s281.04x
🐘 PostgresExpress2.034s (+12.7% 🔺)2.441s (+16.3% 🔺)0.000s (+Infinity% 🔺)2.453s (+15.6% 🔺)0.418s251.08x
🐘 PostgresNext.js (Turbopack)2.302s3.005s0.000s3.015s0.713s201.22x
💻 LocalNitro3.350s (+0.6%)3.967s (~)0.000s (-60.0% 🟢)3.971s (~)0.621s161.78x
💻 LocalExpress3.508s (+1.9%)4.030s (-1.6%)0.001s (+37.5% 🔺)4.035s (-1.6%)0.527s151.86x
💻 LocalNext.js (Turbopack)3.784s4.387s0.000s4.392s0.608s142.01x

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNitro21/21
🐘 PostgresExpress12/21
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express🐘 Postgres20/21
Next.js (Turbopack)🌐 Redis15/21
Nitro🐘 Postgres17/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)

📋 View full workflow run


Some benchmark jobs failed:

  • Local: success
  • Postgres: success
  • Vercel: failure

Check the workflow run for details.

@TooTallNateGraphite App

Copy link
Copy Markdown
MemberAuthor

This stack of pull requests is managed by Graphite. Learn more about stacking.

@TooTallNate
TooTallNate marked this pull request as ready for review February 20, 2026 22:47
CopilotAI review requested due to automatic review settings February 20, 2026 22:47

CopilotAI 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.

Pull request overview

This PR extends the @workflow/swc-plugin transform to detect custom class serialization methods when the WORKFLOW_SERIALIZE / WORKFLOW_DESERIALIZE symbols are accessed via CommonJS require() patterns, aligning behavior with codebases that have been transpiled from ESM to CJS.

Changes:

  • Track CommonJS require() bindings during identifier collection (namespace const x = require(...) and destructured const { ... } = require(...)) and recognize x.WORKFLOW_SERIALIZE / x.WORKFLOW_DESERIALIZE as serialization symbols.
  • Add fixture coverage for both namespace-require and destructured-require inputs across workflow/step/client outputs.
  • Update the plugin spec and add a changeset entry documenting the new behavior.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 2 comments.

Show a summary per file
FileDescription
packages/swc-plugin-workflow/transform/src/lib.rsAdds require-binding tracking and member-expression symbol recognition for serialization detection.
packages/swc-plugin-workflow/transform/tests/fixture/custom-serialization-require-namespace/input.jsNew fixture input for namespace require() pattern.
packages/swc-plugin-workflow/transform/tests/fixture/custom-serialization-require-namespace/output-workflow.jsExpected workflow-mode output for namespace require() fixture.
packages/swc-plugin-workflow/transform/tests/fixture/custom-serialization-require-namespace/output-step.jsExpected step-mode output for namespace require() fixture.
packages/swc-plugin-workflow/transform/tests/fixture/custom-serialization-require-namespace/output-client.jsExpected client-mode output for namespace require() fixture.
packages/swc-plugin-workflow/transform/tests/fixture/custom-serialization-require-destructured/input.jsNew fixture input for destructured require() pattern.
packages/swc-plugin-workflow/transform/tests/fixture/custom-serialization-require-destructured/output-workflow.jsExpected workflow-mode output for destructured require() fixture.
packages/swc-plugin-workflow/transform/tests/fixture/custom-serialization-require-destructured/output-step.jsExpected step-mode output for destructured require() fixture.
packages/swc-plugin-workflow/transform/tests/fixture/custom-serialization-require-destructured/output-client.jsExpected client-mode output for destructured require() fixture.
packages/swc-plugin-workflow/spec.mdDocuments supported CommonJS require() symbol patterns.
.changeset/social-forks-tease.mdDeclares a patch release for the new detection behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment threadpackages/swc-plugin-workflow/transform/src/lib.rs Outdated
Comment threadpackages/swc-plugin-workflow/transform/src/lib.rs Outdated
…WC plugin
The SWC compiler plugin now detects classes with custom serialization methods (`WORKFLOW_SERIALIZE` / `WORKFLOW_DESERIALIZE`) when symbols are obtained via CommonJS `require()` calls, in addition to the existing ESM import and `Symbol.for()` patterns.
This handles code that has been pre-compiled from ESM to CommonJS by tools like TypeScript (tsc), esbuild, or tsup, which transform:
```ts
import { WORKFLOW_SERIALIZE } from "@workflow/serde"
```
into either:
```ts
const serde_1 = require("@workflow/serde") // namespace require
const { WORKFLOW_SERIALIZE } = require(...) // destructured require
```
Both patterns are now recognized during the identifier collection phase, and classes using them are properly registered with registerSerializationClass().
@TooTallNateTooTallNate changed the title Add CommonJS require() support for class serialization detection in SWC pluginAdd CommonJS require() support for class serialization detection in SWC pluginMar 23, 2026
…on_bindings helper
- Tighten is_require_call to require exactly one string literal argument,
rejecting dynamic requires and multi-arg calls.
- Extract track_serialization_bindings helper to deduplicate the require
tracking logic between Decl::Var and ExportDecl(Decl::Var) branches.
@TooTallNate
TooTallNateforce-pushed the 02-20-add_commonjs_require_support_for_class_serialization_detection_in_swc_plugin branch from 3648e46 to f461c59CompareMarch 23, 2026 14:01
@TooTallNate
TooTallNate requested a review from a team as a code ownerMarch 23, 2026 14:01
@TooTallNate
TooTallNate merged commit 992d768 into mainMar 23, 2026
98 of 103 checks passed
@TooTallNate
TooTallNate deleted the 02-20-add_commonjs_require_support_for_class_serialization_detection_in_swc_plugin branch March 23, 2026 18:30
@ghostghost mentioned this pull request Mar 23, 2026

@pranaygppranaygp 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.

Post-merge review — overall this is a clean, well-tested addition. A few observations below.

// Track CommonJS namespace require: const serde_1 = require("...")
if self.is_require_call(init) {
self.require_namespace_identifiers
.insert(ident.id.sym.to_string());

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.

Nit: This tracks everyrequire() call as a namespace identifier, regardless of the module being required. So const fs = require("fs") would cause fs.WORKFLOW_SERIALIZE to be treated as a serialization symbol if it ever appeared. This is unlikely to cause real bugs since nobody would name a property WORKFLOW_SERIALIZE on an unrelated module, but it is semantically imprecise.

Consider either:

  1. Restricting to known serde modules (e.g. checking the string arg contains @workflow/serde or similar), or
  2. Documenting this as an intentional trade-off (broad tracking is simpler and the false-positive risk is negligible).

match prop {
ObjectPatProp::Assign(assign) => {
// const { WORKFLOW_SERIALIZE } = require("...")
let name = assign.key.sym.to_string();

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.

Nit: The ObjectPatProp::Assign arm handles const { WORKFLOW_SERIALIZE } = require(...), but in the SWC AST this node actually represents const { WORKFLOW_SERIALIZE = defaultValue } = require(...) (a destructured binding with a default value). A plain shorthand const { WORKFLOW_SERIALIZE } = require(...) is typically parsed as ObjectPatProp::KeyValue where key and value ident are the same.

In practice this still works because SWC may represent the no-default shorthand either way depending on version, and the logic is correct for both paths. But worth noting that the Assign arm would also match const { WORKFLOW_SERIALIZE = someDefault } = require(...), which is an unusual but technically valid pattern.

@@ -0,0 +1,25 @@
// Test custom serialization with CommonJS destructured require
const { WORKFLOW_SERIALIZE, WORKFLOW_DESERIALIZE } = require("@workflow/serde");

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.

The spec documents renamed destructured require (const { WORKFLOW_SERIALIZE: WS } = require(...)) but there's no test fixture covering this case. Adding a custom-serialization-require-destructured-renamed fixture would ensure the ObjectPatProp::KeyValue branch stays correct.

pranaygp added a commit that referenced this pull request Mar 23, 2026
* origin/main:
[ai] Add experimental_context to DurableAgentOptions (#1489)
[ai] Expose configured tools on DurableAgent instances (#1488)
fix(builders): catch node builtin usage when entry fields diverge (#1455)
[web-shared] Fix timeline duration format and precision (#1482)
[cli] Add bulk cancel, --status filter, fix step JSON hydration (#1467)
[utils] Re-export parseName utilities and add workflow/observability module (#1453)
[o11y] Polish display when run data has expired (#1438)
Add CommonJS `require()` support for class serialization detection in SWC plugin (#1144)
fix(next): stabilize deferred canary e2e in nextjs workbenches (#1468)
[web] Support legacy newline-delimited stream format in `useStreamReader` (#1473)
Revert "Add support for calling start() inside workflow functions (#1133)" (#1475)
pranaygp added a commit that referenced this pull request Mar 24, 2026
…naygp-db9e68c1
* 'main' of https://github.com/vercel/workflow: (32 commits)
chore: bump @nestjs/* to ^11.1.17 (#1497)
chore: bump hono to ^4.12.8 (#1495)
Revert "Inline class serialization registration to fix 3rd-party package supp…" (#1493)
[world] Add stream pagination and metadata endpoints (#1470)
[cli] [world-local] Ensure update checks don't suggest upgrading from stable release to pre-releases (#1490)
Remove NestJS Vercel integration while in experimental phase (#1485)
feat: export semantic error types and add API reference docs (#1447)
feat: enforce max queue deliveries in handlers with graceful failure (#1344)
[world-postgres] Migrate client from `postgres.js` to `pg` (#1484)
Inline class serialization registration to fix 3rd-party package support (#1480)
[ai] Add experimental_context to DurableAgentOptions (#1489)
[ai] Expose configured tools on DurableAgent instances (#1488)
fix(builders): catch node builtin usage when entry fields diverge (#1455)
[web-shared] Fix timeline duration format and precision (#1482)
[cli] Add bulk cancel, --status filter, fix step JSON hydration (#1467)
[utils] Re-export parseName utilities and add workflow/observability module (#1453)
[o11y] Polish display when run data has expired (#1438)
Add CommonJS `require()` support for class serialization detection in SWC plugin (#1144)
fix(next): stabilize deferred canary e2e in nextjs workbenches (#1468)
[web] Support legacy newline-delimited stream format in `useStreamReader` (#1473)
...
TooTallNate added a commit that referenced this pull request Mar 24, 2026
The original PR #1480 was merged but reverted because it didn't include
updated fixtures for the CJS require patterns added by PR #1144
(custom-serialization-require-destructured and
custom-serialization-require-namespace). These fixtures still had the
old 'import { registerSerializationClass }' pattern instead of the
new inline IIFE.
TooTallNate added a commit that referenced this pull request Mar 24, 2026
…ort (v2) (#1503)
* Inline class serialization registration to fix 3rd-party package support (#1480)
* Inline class serialization registration to fix 3rd-party package support
The SWC plugin previously generated:
import { registerSerializationClass } from "workflow/internal/class-serialization";
registerSerializationClass("class//...", ClassName);
This broke for 3rd-party packages (e.g. @vercel/sandbox) that define
serializable classes but don't depend on the 'workflow' package. The
bare 'workflow' specifier is unresolvable from within node_modules of
a package that doesn't list it as a dependency.
Now the plugin generates a self-contained IIFE that uses
Symbol.for('workflow-class-registry') on globalThis directly, with
zero module dependencies:
(function(__wf_cls, __wf_id) {
var __wf_sym = Symbol.for("workflow-class-registry"),
__wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map());
__wf_reg.set(__wf_id, __wf_cls);
Object.defineProperty(__wf_cls, "classId", { ... });
})(ClassName, "class//...");
This is fully compatible with the existing deserialization side in
@workflow/core which reads from the same globalThis registry.
* Address review feedback: fix comment and update docstring
- Fix inaccurate IIFE comment in lib.rs: the second arg is the
generated class ID string, not the literal "classId"
- Update registerSerializationClass docstring to reflect that the
SWC plugin now inlines equivalent logic rather than importing it
* Update CJS require fixture outputs for inline class serialization
The original PR #1480 was merged but reverted because it didn't include
updated fixtures for the CJS require patterns added by PR #1144
(custom-serialization-require-destructured and
custom-serialization-require-namespace). These fixtures still had the
old 'import { registerSerializationClass }' pattern instead of the
new inline IIFE.
pranaygp pushed a commit to vercel/sandbox that referenced this pull request Mar 27, 2026
…ibility (#109)
## Summary
Makes `@vercel/sandbox` fully compatible with the Workflow DevKit
compiler so that `Sandbox`, `Command`, and `CommandFinished` instances
can be used directly inside `"use workflow"` functions — no wrapper step
functions needed.
Supersedes #58.
## Problem
When `@vercel/sandbox` is imported in a workflow context, the workflow
builder tries to bundle it into the workflow VM bundle (because it has
`WORKFLOW_SERIALIZE`/`WORKFLOW_DESERIALIZE` on its classes). This fails
because:
1. The SDK's public methods use Node.js APIs (`fs`, `stream`, `zlib`,
`undici`, etc.) which are forbidden in the workflow VM
2. The compiled `dist/index.js` was a single bundled file that hoisted
all Node.js imports to the top, making them impossible to tree-shake
3. The `Sandbox` and `Command` classes had a sync `client` getter that
directly referenced `APIClient`, pulling the entire HTTP client stack
into the module scope
## Solution
### 1. `"use step"` annotations on all public async methods
Added `"use step"` to all 22 public async methods across `Sandbox` (14),
`Command` (5), and `Snapshot` (3). The SWC plugin strips these method
bodies in workflow mode, replacing them with durable step proxies. This
eliminates all Node.js API references from the workflow bundle.
### 2. Async `ensureClient()` replaces sync `client` getter
The sync `get client()` getter directly referenced `APIClient`, which
pulls in `undici`, `zlib`, `tar-stream`, `jsonlines`, etc. Replaced
with:
```typescript
private async ensureClient(): Promise<APIClient> {
"use step";
if (this._client) return this._client;
const credentials = getSandboxCredentials();
this._client = new APIClient({ ... });
return this._client;
}
```
Since `ensureClient()` is itself `"use step"`, its body (including `new
APIClient(...)`) gets stripped in workflow mode. All instance methods
now call `const client = await this.ensureClient();` instead of
`this.client`.
### 3. `bundle: false` in tsdown config
Changed from single-file bundling to per-file output. This keeps Node.js
imports local to the files that use them, so after the SWC plugin strips
step method bodies, the now-unused Node.js imports can be eliminated by
esbuild's tree-shaking.
### 4. Workflow-code-runner example updated
- Removed `serverExternalPackages: ["@vercel/sandbox"]` from
`next.config.ts` (no longer needed)
- Updated `workflow` dependency to use a tarball that includes the
inline class serialization registration fix (vercel/workflow#1480)
## Testing
- `pnpm build` succeeds for the full monorepo (all 8 tasks)
- The `workflow-code-runner` example app builds successfully with all
workflow routes generated
- 22 `"use step"` directives survive compilation in both ESM and CJS
dist output
## Related
- vercel/workflow#1480 — Inline class serialization registration (fixes
SWC import resolution for 3rd-party packages)
- vercel/workflow#1481 — Build-time warning when
`serverExternalPackages` hides workflow-enabled packages
- vercel/workflow#1144 — CJS detection for serde symbols (pending
review)
- #58 — Previous attempt (superseded by this PR)
---------
Signed-off-by: Peter Wielander <mittgfu@gmail.com>
Co-authored-by: Peter Wielander <mittgfu@gmail.com>
Sign up for freeto 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.

4 participants

@TooTallNate@pranaygp@VaguelySerious
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Add CommonJS `require()` support for class serialization detection in SWC plugin by TooTallNate · Pull Request #1144 · vercel/workflow · GitHub
Skip to content

Add CommonJS require() support for class serialization detection in SWC plugin - #1144

Merged
TooTallNate merged 3 commits into
mainfrom
02-20-add_commonjs_require_support_for_class_serialization_detection_in_swc_plugin
Mar 23, 2026
Merged

Add CommonJS require() support for class serialization detection in SWC plugin#1144
TooTallNate merged 3 commits into
mainfrom
02-20-add_commonjs_require_support_for_class_serialization_detection_in_swc_plugin

Conversation

@TooTallNate

Copy link
Copy Markdown
Member

The SWC compiler plugin now detects classes with custom serialization methods (WORKFLOW_SERIALIZE / WORKFLOW_DESERIALIZE) when symbols are obtained via CommonJS require() calls, in addition to the existing ESM import and Symbol.for() patterns.

This handles code that has been pre-compiled from ESM to CommonJS by tools like TypeScript (tsc), esbuild, or tsup, which transform:

import{WORKFLOW_SERIALIZE}from"@workflow/serde"

into either:

constserde_1=require("@workflow/serde")// namespace requireconst{WORKFLOW_SERIALIZE}=require(...)// destructured require

Both patterns are now recognized during the identifier collection phase, and classes using them are properly registered with registerSerializationClass().

@changeset-bot

changeset-botBot commented Feb 20, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 14ea197

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

This PR includes changesets to release 17 packages
NameType
@workflow/swc-pluginPatch
@workflow/astroPatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nestPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
workflowPatch
@workflow/vitePatch
@workflow/vitestPatch
@workflow/world-testingPatch
@workflow/nuxtPatch
@workflow/aiPatch
@workflow/corePatch
@workflow/web-sharedPatch

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

@vercel

vercelBot commented Feb 20, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Feb 20, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production780067847
✅ 💻 Local Development7820142924
✅ 📦 Local Production7820142924
✅ 🐘 Local Postgres7820142924
✅ 🪟 Windows720577
❌ 🌍 Community Worlds1185621195
✅ 📋 Other198033231
Total3514565524122

❌ Failed Tests

🌍 Community Worlds (56 failed)

mongodb (3 failed):

  • hookWorkflow is not resumable via public webhook endpoint | wrun_01KMDYE4RWSYQ6538DVH70M5P0
  • webhookWorkflow | wrun_01KMDYED4N9B1F4PF1X6F86J26
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously | wrun_01KMDYMVS1CFTACB4081WS8K9V

redis (2 failed):

  • hookWorkflow is not resumable via public webhook endpoint | wrun_01KMDYE4RWSYQ6538DVH70M5P0
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously | wrun_01KMDYMVS1CFTACB4081WS8K9V

turso (51 failed):

  • addTenWorkflow | wrun_01KMDYCYXATZBJYFAPPKVRZKDZ
  • addTenWorkflow | wrun_01KMDYCYXATZBJYFAPPKVRZKDZ
  • wellKnownAgentWorkflow (.well-known/agent) | wrun_01KMDYEKVFEVFMCXC32DMPJ3KB
  • should work with react rendering in step
  • promiseAllWorkflow | wrun_01KMDYD5F67YMHR6KTJF5YFYM3
  • promiseRaceWorkflow | wrun_01KMDYDBXWV7065WDZ655A6X2Y
  • promiseAnyWorkflow | wrun_01KMDYDEFBS29N1Y4E04XS0PHS
  • importedStepOnlyWorkflow | wrun_01KMDYF31ZMHDYKRR9870BQJFE
  • hookWorkflow | wrun_01KMDYDTDYK5ZZTVGBV7PBPJAZ
  • hookWorkflow is not resumable via public webhook endpoint | wrun_01KMDYE4RWSYQ6538DVH70M5P0
  • webhookWorkflow | wrun_01KMDYED4N9B1F4PF1X6F86J26
  • sleepingWorkflow | wrun_01KMDYEKNPTWHR8GD4VJXRHSWR
  • parallelSleepWorkflow | wrun_01KMDYF1MMFSRK8683TXYGPEFF
  • nullByteWorkflow | wrun_01KMDYF6AS6SP633VT5B7JZ0Q8
  • workflowAndStepMetadataWorkflow | wrun_01KMDYF8B1WBHNCPZKHFYCH5G0
  • fetchWorkflow | wrun_01KMDYH6MPPT3CPGBXX02XNQTP
  • promiseRaceStressTestWorkflow | wrun_01KMDYH9Y93SDT6D7TEE6CMKHT
  • error handling error propagation workflow errors nested function calls preserve message and stack trace
  • error handling error propagation workflow errors cross-file imports preserve message and stack trace
  • error handling error propagation step errors basic step error preserves message and stack trace
  • error handling error propagation step errors cross-file step error preserves message and function names in stack
  • error handling retry behavior regular Error retries until success
  • error handling retry behavior FatalError fails immediately without retries
  • error handling retry behavior RetryableError respects custom retryAfter delay
  • error handling retry behavior maxRetries=0 disables retries
  • error handling catchability FatalError can be caught and detected with FatalError.is()
  • hookCleanupTestWorkflow - hook token reuse after workflow completion | wrun_01KMDYM7FJ0MWDXPS56VM7PF4D
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously | wrun_01KMDYMVS1CFTACB4081WS8K9V
  • hookDisposeTestWorkflow - hook token reuse after explicit disposal while workflow still running | wrun_01KMDYNGM0E0ZXFK8E2CQ9CYZV
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars) | wrun_01KMDYP5F3H7NXZ9W494MXF8EQ
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument | wrun_01KMDYPFQ0MK8D4B36398E2S6A
  • closureVariableWorkflow - nested step functions with closure variables | wrun_01KMDYPN9T3F9HBR5W1TQSBAVH
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step | wrun_01KMDYPQBVNH7GG5Y8ZDS8B1P3
  • health check (queue-based) - workflow and step endpoints respond to health check messages
  • pathsAliasWorkflow - TypeScript path aliases resolve correctly | wrun_01KMDYQ7ZKSHZ1DF03RZ64NGXD
  • Calculator.calculate - static workflow method using static step methods from another class | wrun_01KMDYQEMCVB21XR91WVHJEGK4
  • AllInOneService.processNumber - static workflow method using sibling static step methods | wrun_01KMDYQP3ST1XWR4FTRNPGNYE4
  • ChainableService.processWithThis - static step methods using this to reference the class | wrun_01KMDYQWMSK6ZYMFNY7ME546A9
  • thisSerializationWorkflow - step function invoked with .call() and .apply() | wrun_01KMDYR36AW71R74TKNQEA26QC
  • customSerializationWorkflow - custom class serialization with WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE | wrun_01KMDYR9J9RVJCAWSQG76N5656
  • instanceMethodStepWorkflow - instance methods with "use step" directive | wrun_01KMDYRH7WBXH2XGDZPC5CXB49
  • crossContextSerdeWorkflow - classes defined in step code are deserializable in workflow context | wrun_01KMDYRVMSG67Q29E0JT62SCPB
  • stepFunctionAsStartArgWorkflow - step function reference passed as start() argument | wrun_01KMDYS38DF4PJE3SJHH7YV0GQ
  • cancelRun - cancelling a running workflow | wrun_01KMDYSB4MVW6TNAVS70ZDC36V
  • cancelRun via CLI - cancelling a running workflow | wrun_01KMDYSM88CM0VKBMVABG1ZRM0
  • pages router addTenWorkflow via pages router
  • pages router promiseAllWorkflow via pages router
  • pages router sleepingWorkflow via pages router
  • hookWithSleepWorkflow - hook payloads delivered correctly with concurrent sleep | wrun_01KMDYT02V9K533MN8PA31072V
  • sleepInLoopWorkflow - sleep inside loop with steps actually delays each iteration | wrun_01KMDYTKS4H2ZFNHJC3AH7D70B
  • sleepWithSequentialStepsWorkflow - sequential steps work with concurrent sleep (control) | wrun_01KMDYTZMDM1Q8370F5R6NNHJV

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro7007
✅ example7007
✅ express7007
✅ fastify7007
✅ hono7007
✅ nextjs-turbopack7502
✅ nextjs-webpack7502
✅ nitro7007
✅ nuxt7007
✅ sveltekit7007
✅ vite7007
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable66011
✅ express-stable66011
✅ fastify-stable66011
✅ hono-stable66011
✅ nextjs-turbopack-canary55022
✅ nextjs-turbopack-stable7205
✅ nextjs-webpack-canary55022
✅ nextjs-webpack-stable7205
✅ nitro-stable66011
✅ nuxt-stable66011
✅ sveltekit-stable66011
✅ vite-stable66011
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable66011
✅ express-stable66011
✅ fastify-stable66011
✅ hono-stable66011
✅ nextjs-turbopack-canary55022
✅ nextjs-turbopack-stable7205
✅ nextjs-webpack-canary55022
✅ nextjs-webpack-stable7205
✅ nitro-stable66011
✅ nuxt-stable66011
✅ sveltekit-stable66011
✅ vite-stable66011
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable66011
✅ express-stable66011
✅ fastify-stable66011
✅ hono-stable66011
✅ nextjs-turbopack-canary55022
✅ nextjs-turbopack-stable7205
✅ nextjs-webpack-canary55022
✅ nextjs-webpack-stable7205
✅ nitro-stable66011
✅ nuxt-stable66011
✅ sveltekit-stable66011
✅ vite-stable66011
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack7205
❌ 🌍 Community Worlds
AppPassedFailedSkipped
✅ mongodb-dev302
❌ mongodb5235
✅ redis-dev302
❌ redis5325
✅ turso-dev302
❌ turso4515
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable66011
✅ e2e-local-postgres-nest-stable66011
✅ e2e-local-prod-nest-stable66011

📋 View full workflow run

@github-actions

github-actionsBot commented Feb 20, 2026

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

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

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro0.037s (-21.3% 🟢)1.005s (~)0.967s101.00x
💻 LocalExpress0.043s (-4.5%)1.005s (~)0.962s101.14x
🐘 PostgresNitro0.047s (-31.9% 🟢)1.012s (~)0.965s101.26x
🐘 PostgresExpress0.048s (-32.2% 🟢)1.012s (-0.5%)0.964s101.29x
💻 LocalNext.js (Turbopack)0.051s1.006s0.956s101.35x
🌐 RedisNext.js (Turbopack)0.054s1.005s0.952s101.44x
🐘 PostgresNext.js (Turbopack)0.062s1.012s0.950s101.65x
workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.097s (-2.8%)2.005s (~)0.908s101.00x
🌐 RedisNext.js (Turbopack)1.120s2.007s0.887s101.02x
🐘 PostgresNitro1.124s (-1.6%)2.012s (~)0.888s101.02x
💻 LocalNext.js (Turbopack)1.124s2.006s0.883s101.02x
🐘 PostgresExpress1.126s (-1.1%)2.012s (~)0.886s101.03x
💻 LocalExpress1.131s (~)2.005s (~)0.874s101.03x
🐘 PostgresNext.js (Turbopack)1.143s2.013s0.869s101.04x
workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro10.635s (-2.4%)11.023s (~)0.388s31.00x
🌐 RedisNext.js (Turbopack)10.680s11.025s0.345s31.00x
🐘 PostgresNitro10.689s (-2.4%)11.036s (~)0.348s31.01x
🐘 PostgresExpress10.705s (-1.8%)11.038s (~)0.333s31.01x
💻 LocalNext.js (Turbopack)10.847s11.025s0.178s31.02x
💻 LocalExpress10.934s (~)11.023s (~)0.089s31.03x
🐘 PostgresNext.js (Turbopack)10.996s11.382s0.386s31.03x
workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)14.091s15.033s0.941s41.00x
🐘 PostgresExpress14.123s (-3.1%)15.042s (~)0.919s41.00x
🐘 PostgresNitro14.129s (-4.7%)15.039s (~)0.910s41.00x
💻 LocalNitro14.183s (-4.9%)15.029s (~)0.847s41.01x
🐘 PostgresNext.js (Turbopack)14.580s15.049s0.469s41.03x
💻 LocalNext.js (Turbopack)14.703s15.031s0.328s41.04x
💻 LocalExpress14.911s (-0.6%)15.029s (-3.2%)0.118s41.06x
workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)13.030s13.457s0.427s71.00x
🐘 PostgresExpress13.096s (-6.5% 🟢)14.038s (-3.0%)0.942s71.01x
🐘 PostgresNitro13.119s (-8.7% 🟢)14.038s (-6.7% 🟢)0.919s71.01x
🐘 PostgresNext.js (Turbopack)14.145s14.759s0.613s71.09x
💻 LocalNitro14.789s (-10.1% 🟢)15.026s (-11.8% 🟢)0.237s61.14x
💻 LocalNext.js (Turbopack)16.157s17.033s0.875s61.24x
💻 LocalExpress16.514s (-1.0%)17.029s (~)0.516s61.27x
Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.228s (-3.0%)2.011s (~)0.783s151.00x
🐘 PostgresNitro1.231s (-4.8%)2.011s (~)0.780s151.00x
🌐 RedisNext.js (Turbopack)1.266s2.007s0.741s151.03x
🐘 PostgresNext.js (Turbopack)1.286s2.011s0.725s151.05x
💻 LocalNitro1.472s (-0.8%)2.005s (~)0.534s151.20x
💻 LocalExpress1.503s (~)2.006s (~)0.503s151.22x
💻 LocalNext.js (Turbopack)1.524s2.006s0.482s151.24x
Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express2.341s (-4.9%)3.012s (~)0.671s101.00x
🐘 PostgresNitro2.353s (-4.8%)3.013s (~)0.660s101.01x
🌐 RedisNext.js (Turbopack)2.478s3.009s0.531s101.06x
🐘 PostgresNext.js (Turbopack)2.484s3.015s0.531s101.06x
💻 LocalNitro2.551s (-11.2% 🟢)3.007s (-3.2%)0.456s101.09x
💻 LocalExpress2.970s (-0.9%)3.308s (-7.2% 🟢)0.338s101.27x
💻 LocalNext.js (Turbopack)2.995s3.309s0.314s101.28x
Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express3.519s (-1.9%)4.014s (~)0.495s81.00x
🐘 PostgresNitro3.523s (-3.0%)4.015s (~)0.492s81.00x
🐘 PostgresNext.js (Turbopack)3.844s4.017s0.173s81.09x
🌐 RedisNext.js (Turbopack)4.117s4.727s0.610s71.17x
💻 LocalNitro7.025s (-13.5% 🟢)7.514s (-14.3% 🟢)0.489s42.00x
💻 LocalExpress8.144s (-3.9%)8.769s (-2.8%)0.625s42.31x
💻 LocalNext.js (Turbopack)8.563s9.019s0.456s42.43x
Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.207s (-4.1%)2.011s (~)0.803s151.00x
🐘 PostgresNitro1.216s (-4.7%)2.010s (~)0.794s151.01x
🌐 RedisNext.js (Turbopack)1.252s2.007s0.755s151.04x
🐘 PostgresNext.js (Turbopack)1.254s2.011s0.757s151.04x
💻 LocalNitro1.484s (-4.9%)2.006s (~)0.522s151.23x
💻 LocalNext.js (Turbopack)1.515s2.005s0.490s151.25x
💻 LocalExpress1.550s (+0.9%)2.005s (~)0.455s151.28x
Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro2.342s (-4.7%)3.012s (~)0.670s101.00x
🐘 PostgresExpress2.353s (-4.0%)3.012s (~)0.659s101.00x
🐘 PostgresNext.js (Turbopack)2.474s3.013s0.539s101.06x
🌐 RedisNext.js (Turbopack)2.498s3.009s0.511s101.07x
💻 LocalNitro2.924s (-3.4%)3.208s (-12.7% 🟢)0.283s101.25x
💻 LocalNext.js (Turbopack)3.012s3.675s0.663s91.29x
💻 LocalExpress3.090s (+1.9%)3.885s (+3.4%)0.795s81.32x
Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express3.515s (-2.3%)4.013s (~)0.497s81.00x
🐘 PostgresNitro3.518s (-2.7%)4.014s (~)0.496s81.00x
🐘 PostgresNext.js (Turbopack)3.841s4.018s0.177s81.09x
🌐 RedisNext.js (Turbopack)4.031s4.583s0.553s71.15x
💻 LocalNitro7.511s (-13.1% 🟢)8.016s (-11.2% 🟢)0.506s42.14x
💻 LocalExpress8.608s (-5.0%)9.020s (-7.7% 🟢)0.412s42.45x
💻 LocalNext.js (Turbopack)9.214s9.772s0.557s42.62x
workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)0.601s1.005s0.404s601.00x
🐘 PostgresExpress0.642s (-25.8% 🟢)1.009s (-1.7%)0.366s601.07x
🐘 PostgresNitro0.657s (-26.8% 🟢)1.008s (-3.4%)0.351s601.09x
💻 LocalNitro0.672s (-29.7% 🟢)1.004s (-1.7%)0.332s601.12x
🐘 PostgresNext.js (Turbopack)0.870s1.009s0.140s601.45x
💻 LocalNext.js (Turbopack)0.878s1.057s0.179s571.46x
💻 LocalExpress0.973s (-1.0%)1.114s (+1.7%)0.140s551.62x
workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)1.460s2.006s0.546s451.00x
🐘 PostgresExpress1.553s (-26.6% 🟢)2.010s (-33.3% 🟢)0.457s451.06x
🐘 PostgresNitro1.563s (-27.7% 🟢)2.032s (-31.8% 🟢)0.470s451.07x
🐘 PostgresNext.js (Turbopack)2.127s2.948s0.821s311.46x
💻 LocalNitro2.193s (-25.8% 🟢)3.007s (-3.3%)0.815s301.50x
💻 LocalNext.js (Turbopack)2.736s3.008s0.272s301.87x
💻 LocalExpress2.966s (-1.6%)3.222s (-9.2% 🟢)0.255s282.03x
workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)3.077s3.437s0.360s351.00x
🐘 PostgresNitro3.153s (-27.3% 🟢)4.012s (-20.0% 🟢)0.859s301.02x
🐘 PostgresExpress3.188s (-25.2% 🟢)4.012s (-19.3% 🟢)0.824s301.04x
🐘 PostgresNext.js (Turbopack)4.345s5.015s0.670s241.41x
💻 LocalNitro7.218s (-20.1% 🟢)7.827s (-17.4% 🟢)0.610s162.35x
💻 LocalNext.js (Turbopack)8.797s9.232s0.434s142.86x
💻 LocalExpress8.995s (-1.7%)9.479s (-3.1%)0.484s132.92x
workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.243s (-19.7% 🟢)1.008s (~)0.766s601.00x
🐘 PostgresNitro0.245s (-16.9% 🟢)1.009s (~)0.764s601.01x
🌐 RedisNext.js (Turbopack)0.260s1.005s0.744s601.07x
🐘 PostgresNext.js (Turbopack)0.287s1.010s0.723s601.18x
💻 LocalNitro0.576s (-3.7%)1.004s (~)0.428s602.37x
💻 LocalExpress0.581s (-3.7%)1.004s (~)0.424s602.39x
💻 LocalNext.js (Turbopack)0.585s1.004s0.420s602.41x
workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.419s (-20.5% 🟢)1.009s (~)0.589s901.00x
🐘 PostgresNitro0.424s (-21.4% 🟢)1.009s (~)0.585s901.01x
🐘 PostgresNext.js (Turbopack)0.590s1.033s0.443s881.41x
🌐 RedisNext.js (Turbopack)1.194s2.006s0.812s452.85x
💻 LocalNitro2.345s (-5.2% 🟢)3.007s (~)0.662s305.59x
💻 LocalExpress2.498s (-3.2%)3.009s (~)0.510s305.96x
💻 LocalNext.js (Turbopack)2.664s3.009s0.345s306.36x
workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.606s (-35.5% 🟢)1.009s (-15.4% 🟢)0.403s1191.00x
🐘 PostgresExpress0.620s (-30.9% 🟢)1.009s (-3.1%)0.389s1191.02x
🐘 PostgresNext.js (Turbopack)1.001s1.477s0.476s821.65x
🌐 RedisNext.js (Turbopack)2.883s3.085s0.202s394.76x
💻 LocalNitro10.189s (-8.3% 🟢)10.855s (-6.9% 🟢)0.666s1216.83x
💻 LocalExpress11.127s (-1.4%)11.845s (-1.5%)0.718s1118.38x
💻 LocalNext.js (Turbopack)11.287s11.753s0.465s1118.64x
Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro0.140s (-28.6% 🟢)1.003s (~)0.009s (-26.3% 🟢)1.014s (~)0.874s101.00x
🐘 PostgresExpress0.164s (-24.1% 🟢)0.999s (+0.5%)0.001s (-8.3% 🟢)1.013s (~)0.848s101.17x
🐘 PostgresNitro0.167s (-30.3% 🟢)0.999s (+0.7%)0.001s (-21.4% 🟢)1.013s (~)0.846s101.19x
🌐 RedisNext.js (Turbopack)0.169s1.000s0.001s1.008s0.838s101.21x
💻 LocalNext.js (Turbopack)0.178s1.001s0.012s1.018s0.840s101.27x
💻 LocalExpress0.199s (-2.4%)1.003s (~)0.012s (-2.5%)1.017s (~)0.818s101.42x
🐘 PostgresNext.js (Turbopack)0.224s1.001s0.001s1.013s0.789s101.60x
stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)0.447s0.999s0.003s1.012s0.565s601.00x
🐘 PostgresNitro0.563s (-22.1% 🟢)1.005s (~)0.003s (-13.6% 🟢)1.023s (~)0.460s591.26x
💻 LocalNitro0.569s (-21.7% 🟢)1.008s (~)0.009s (-6.2% 🟢)1.021s (~)0.452s591.27x
🐘 PostgresExpress0.610s (-12.0% 🟢)1.006s (~)0.005s (+20.4% 🔺)1.028s (~)0.417s591.37x
💻 LocalNext.js (Turbopack)0.672s1.008s0.010s1.024s0.352s591.50x
💻 LocalExpress0.723s (-6.9% 🟢)1.009s (-1.7%)0.009s (-12.1% 🟢)1.022s (-1.8%)0.300s591.62x
🐘 PostgresNext.js (Turbopack)0.760s1.026s0.008s1.050s0.290s591.70x
10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)0.916s1.016s0.000s1.022s0.106s591.00x
🐘 PostgresNitro0.988s (+7.3% 🔺)1.384s (+30.2% 🔺)0.000s (-36.4% 🟢)1.398s (+29.3% 🔺)0.410s441.08x
🐘 PostgresExpress1.066s (+19.3% 🔺)1.664s (+53.7% 🔺)0.000s (+205.6% 🔺)1.677s (+51.8% 🔺)0.612s361.16x
💻 LocalNitro1.140s (-6.9% 🟢)2.017s (~)0.000s (-38.5% 🟢)2.020s (~)0.880s301.24x
🐘 PostgresNext.js (Turbopack)1.175s1.909s0.000s1.930s0.755s321.28x
💻 LocalExpress1.235s (-1.8%)2.020s (~)0.000s (-35.3% 🟢)2.023s (~)0.788s301.35x
💻 LocalNext.js (Turbopack)1.295s2.019s0.000s2.024s0.729s301.41x
fan-out fan-in 10 streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.886s (~)2.223s (~)0.000s (-100.0% 🟢)2.233s (~)0.347s271.00x
🌐 RedisNext.js (Turbopack)1.963s2.180s0.000s2.186s0.223s281.04x
🐘 PostgresExpress2.034s (+12.7% 🔺)2.441s (+16.3% 🔺)0.000s (+Infinity% 🔺)2.453s (+15.6% 🔺)0.418s251.08x
🐘 PostgresNext.js (Turbopack)2.302s3.005s0.000s3.015s0.713s201.22x
💻 LocalNitro3.350s (+0.6%)3.967s (~)0.000s (-60.0% 🟢)3.971s (~)0.621s161.78x
💻 LocalExpress3.508s (+1.9%)4.030s (-1.6%)0.001s (+37.5% 🔺)4.035s (-1.6%)0.527s151.86x
💻 LocalNext.js (Turbopack)3.784s4.387s0.000s4.392s0.608s142.01x

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNitro21/21
🐘 PostgresExpress12/21
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express🐘 Postgres20/21
Next.js (Turbopack)🌐 Redis15/21
Nitro🐘 Postgres17/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)

📋 View full workflow run


Some benchmark jobs failed:

  • Local: success
  • Postgres: success
  • Vercel: failure

Check the workflow run for details.

@TooTallNateGraphite App

Copy link
Copy Markdown
MemberAuthor

This stack of pull requests is managed by Graphite. Learn more about stacking.

@TooTallNate
TooTallNate marked this pull request as ready for review February 20, 2026 22:47
CopilotAI review requested due to automatic review settings February 20, 2026 22:47

CopilotAI 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.

Pull request overview

This PR extends the @workflow/swc-plugin transform to detect custom class serialization methods when the WORKFLOW_SERIALIZE / WORKFLOW_DESERIALIZE symbols are accessed via CommonJS require() patterns, aligning behavior with codebases that have been transpiled from ESM to CJS.

Changes:

  • Track CommonJS require() bindings during identifier collection (namespace const x = require(...) and destructured const { ... } = require(...)) and recognize x.WORKFLOW_SERIALIZE / x.WORKFLOW_DESERIALIZE as serialization symbols.
  • Add fixture coverage for both namespace-require and destructured-require inputs across workflow/step/client outputs.
  • Update the plugin spec and add a changeset entry documenting the new behavior.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 2 comments.

Show a summary per file
FileDescription
packages/swc-plugin-workflow/transform/src/lib.rsAdds require-binding tracking and member-expression symbol recognition for serialization detection.
packages/swc-plugin-workflow/transform/tests/fixture/custom-serialization-require-namespace/input.jsNew fixture input for namespace require() pattern.
packages/swc-plugin-workflow/transform/tests/fixture/custom-serialization-require-namespace/output-workflow.jsExpected workflow-mode output for namespace require() fixture.
packages/swc-plugin-workflow/transform/tests/fixture/custom-serialization-require-namespace/output-step.jsExpected step-mode output for namespace require() fixture.
packages/swc-plugin-workflow/transform/tests/fixture/custom-serialization-require-namespace/output-client.jsExpected client-mode output for namespace require() fixture.
packages/swc-plugin-workflow/transform/tests/fixture/custom-serialization-require-destructured/input.jsNew fixture input for destructured require() pattern.
packages/swc-plugin-workflow/transform/tests/fixture/custom-serialization-require-destructured/output-workflow.jsExpected workflow-mode output for destructured require() fixture.
packages/swc-plugin-workflow/transform/tests/fixture/custom-serialization-require-destructured/output-step.jsExpected step-mode output for destructured require() fixture.
packages/swc-plugin-workflow/transform/tests/fixture/custom-serialization-require-destructured/output-client.jsExpected client-mode output for destructured require() fixture.
packages/swc-plugin-workflow/spec.mdDocuments supported CommonJS require() symbol patterns.
.changeset/social-forks-tease.mdDeclares a patch release for the new detection behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment threadpackages/swc-plugin-workflow/transform/src/lib.rs Outdated
Comment threadpackages/swc-plugin-workflow/transform/src/lib.rs Outdated
…WC plugin
The SWC compiler plugin now detects classes with custom serialization methods (`WORKFLOW_SERIALIZE` / `WORKFLOW_DESERIALIZE`) when symbols are obtained via CommonJS `require()` calls, in addition to the existing ESM import and `Symbol.for()` patterns.
This handles code that has been pre-compiled from ESM to CommonJS by tools like TypeScript (tsc), esbuild, or tsup, which transform:
```ts
import { WORKFLOW_SERIALIZE } from "@workflow/serde"
```
into either:
```ts
const serde_1 = require("@workflow/serde") // namespace require
const { WORKFLOW_SERIALIZE } = require(...) // destructured require
```
Both patterns are now recognized during the identifier collection phase, and classes using them are properly registered with registerSerializationClass().
@TooTallNateTooTallNate changed the title Add CommonJS require() support for class serialization detection in SWC pluginAdd CommonJS require() support for class serialization detection in SWC pluginMar 23, 2026
…on_bindings helper
- Tighten is_require_call to require exactly one string literal argument,
rejecting dynamic requires and multi-arg calls.
- Extract track_serialization_bindings helper to deduplicate the require
tracking logic between Decl::Var and ExportDecl(Decl::Var) branches.
@TooTallNate
TooTallNateforce-pushed the 02-20-add_commonjs_require_support_for_class_serialization_detection_in_swc_plugin branch from 3648e46 to f461c59CompareMarch 23, 2026 14:01
@TooTallNate
TooTallNate requested a review from a team as a code ownerMarch 23, 2026 14:01
@TooTallNate
TooTallNate merged commit 992d768 into mainMar 23, 2026
98 of 103 checks passed
@TooTallNate
TooTallNate deleted the 02-20-add_commonjs_require_support_for_class_serialization_detection_in_swc_plugin branch March 23, 2026 18:30
@ghostghost mentioned this pull request Mar 23, 2026

@pranaygppranaygp 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.

Post-merge review — overall this is a clean, well-tested addition. A few observations below.

// Track CommonJS namespace require: const serde_1 = require("...")
if self.is_require_call(init) {
self.require_namespace_identifiers
.insert(ident.id.sym.to_string());

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.

Nit: This tracks everyrequire() call as a namespace identifier, regardless of the module being required. So const fs = require("fs") would cause fs.WORKFLOW_SERIALIZE to be treated as a serialization symbol if it ever appeared. This is unlikely to cause real bugs since nobody would name a property WORKFLOW_SERIALIZE on an unrelated module, but it is semantically imprecise.

Consider either:

  1. Restricting to known serde modules (e.g. checking the string arg contains @workflow/serde or similar), or
  2. Documenting this as an intentional trade-off (broad tracking is simpler and the false-positive risk is negligible).

match prop {
ObjectPatProp::Assign(assign) => {
// const { WORKFLOW_SERIALIZE } = require("...")
let name = assign.key.sym.to_string();

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.

Nit: The ObjectPatProp::Assign arm handles const { WORKFLOW_SERIALIZE } = require(...), but in the SWC AST this node actually represents const { WORKFLOW_SERIALIZE = defaultValue } = require(...) (a destructured binding with a default value). A plain shorthand const { WORKFLOW_SERIALIZE } = require(...) is typically parsed as ObjectPatProp::KeyValue where key and value ident are the same.

In practice this still works because SWC may represent the no-default shorthand either way depending on version, and the logic is correct for both paths. But worth noting that the Assign arm would also match const { WORKFLOW_SERIALIZE = someDefault } = require(...), which is an unusual but technically valid pattern.

@@ -0,0 +1,25 @@
// Test custom serialization with CommonJS destructured require
const { WORKFLOW_SERIALIZE, WORKFLOW_DESERIALIZE } = require("@workflow/serde");

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.

The spec documents renamed destructured require (const { WORKFLOW_SERIALIZE: WS } = require(...)) but there's no test fixture covering this case. Adding a custom-serialization-require-destructured-renamed fixture would ensure the ObjectPatProp::KeyValue branch stays correct.

pranaygp added a commit that referenced this pull request Mar 23, 2026
* origin/main:
[ai] Add experimental_context to DurableAgentOptions (#1489)
[ai] Expose configured tools on DurableAgent instances (#1488)
fix(builders): catch node builtin usage when entry fields diverge (#1455)
[web-shared] Fix timeline duration format and precision (#1482)
[cli] Add bulk cancel, --status filter, fix step JSON hydration (#1467)
[utils] Re-export parseName utilities and add workflow/observability module (#1453)
[o11y] Polish display when run data has expired (#1438)
Add CommonJS `require()` support for class serialization detection in SWC plugin (#1144)
fix(next): stabilize deferred canary e2e in nextjs workbenches (#1468)
[web] Support legacy newline-delimited stream format in `useStreamReader` (#1473)
Revert "Add support for calling start() inside workflow functions (#1133)" (#1475)
pranaygp added a commit that referenced this pull request Mar 24, 2026
…naygp-db9e68c1
* 'main' of https://github.com/vercel/workflow: (32 commits)
chore: bump @nestjs/* to ^11.1.17 (#1497)
chore: bump hono to ^4.12.8 (#1495)
Revert "Inline class serialization registration to fix 3rd-party package supp…" (#1493)
[world] Add stream pagination and metadata endpoints (#1470)
[cli] [world-local] Ensure update checks don't suggest upgrading from stable release to pre-releases (#1490)
Remove NestJS Vercel integration while in experimental phase (#1485)
feat: export semantic error types and add API reference docs (#1447)
feat: enforce max queue deliveries in handlers with graceful failure (#1344)
[world-postgres] Migrate client from `postgres.js` to `pg` (#1484)
Inline class serialization registration to fix 3rd-party package support (#1480)
[ai] Add experimental_context to DurableAgentOptions (#1489)
[ai] Expose configured tools on DurableAgent instances (#1488)
fix(builders): catch node builtin usage when entry fields diverge (#1455)
[web-shared] Fix timeline duration format and precision (#1482)
[cli] Add bulk cancel, --status filter, fix step JSON hydration (#1467)
[utils] Re-export parseName utilities and add workflow/observability module (#1453)
[o11y] Polish display when run data has expired (#1438)
Add CommonJS `require()` support for class serialization detection in SWC plugin (#1144)
fix(next): stabilize deferred canary e2e in nextjs workbenches (#1468)
[web] Support legacy newline-delimited stream format in `useStreamReader` (#1473)
...
TooTallNate added a commit that referenced this pull request Mar 24, 2026
The original PR #1480 was merged but reverted because it didn't include
updated fixtures for the CJS require patterns added by PR #1144
(custom-serialization-require-destructured and
custom-serialization-require-namespace). These fixtures still had the
old 'import { registerSerializationClass }' pattern instead of the
new inline IIFE.
TooTallNate added a commit that referenced this pull request Mar 24, 2026
…ort (v2) (#1503)
* Inline class serialization registration to fix 3rd-party package support (#1480)
* Inline class serialization registration to fix 3rd-party package support
The SWC plugin previously generated:
import { registerSerializationClass } from "workflow/internal/class-serialization";
registerSerializationClass("class//...", ClassName);
This broke for 3rd-party packages (e.g. @vercel/sandbox) that define
serializable classes but don't depend on the 'workflow' package. The
bare 'workflow' specifier is unresolvable from within node_modules of
a package that doesn't list it as a dependency.
Now the plugin generates a self-contained IIFE that uses
Symbol.for('workflow-class-registry') on globalThis directly, with
zero module dependencies:
(function(__wf_cls, __wf_id) {
var __wf_sym = Symbol.for("workflow-class-registry"),
__wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map());
__wf_reg.set(__wf_id, __wf_cls);
Object.defineProperty(__wf_cls, "classId", { ... });
})(ClassName, "class//...");
This is fully compatible with the existing deserialization side in
@workflow/core which reads from the same globalThis registry.
* Address review feedback: fix comment and update docstring
- Fix inaccurate IIFE comment in lib.rs: the second arg is the
generated class ID string, not the literal "classId"
- Update registerSerializationClass docstring to reflect that the
SWC plugin now inlines equivalent logic rather than importing it
* Update CJS require fixture outputs for inline class serialization
The original PR #1480 was merged but reverted because it didn't include
updated fixtures for the CJS require patterns added by PR #1144
(custom-serialization-require-destructured and
custom-serialization-require-namespace). These fixtures still had the
old 'import { registerSerializationClass }' pattern instead of the
new inline IIFE.
pranaygp pushed a commit to vercel/sandbox that referenced this pull request Mar 27, 2026
…ibility (#109)
## Summary
Makes `@vercel/sandbox` fully compatible with the Workflow DevKit
compiler so that `Sandbox`, `Command`, and `CommandFinished` instances
can be used directly inside `"use workflow"` functions — no wrapper step
functions needed.
Supersedes #58.
## Problem
When `@vercel/sandbox` is imported in a workflow context, the workflow
builder tries to bundle it into the workflow VM bundle (because it has
`WORKFLOW_SERIALIZE`/`WORKFLOW_DESERIALIZE` on its classes). This fails
because:
1. The SDK's public methods use Node.js APIs (`fs`, `stream`, `zlib`,
`undici`, etc.) which are forbidden in the workflow VM
2. The compiled `dist/index.js` was a single bundled file that hoisted
all Node.js imports to the top, making them impossible to tree-shake
3. The `Sandbox` and `Command` classes had a sync `client` getter that
directly referenced `APIClient`, pulling the entire HTTP client stack
into the module scope
## Solution
### 1. `"use step"` annotations on all public async methods
Added `"use step"` to all 22 public async methods across `Sandbox` (14),
`Command` (5), and `Snapshot` (3). The SWC plugin strips these method
bodies in workflow mode, replacing them with durable step proxies. This
eliminates all Node.js API references from the workflow bundle.
### 2. Async `ensureClient()` replaces sync `client` getter
The sync `get client()` getter directly referenced `APIClient`, which
pulls in `undici`, `zlib`, `tar-stream`, `jsonlines`, etc. Replaced
with:
```typescript
private async ensureClient(): Promise<APIClient> {
"use step";
if (this._client) return this._client;
const credentials = getSandboxCredentials();
this._client = new APIClient({ ... });
return this._client;
}
```
Since `ensureClient()` is itself `"use step"`, its body (including `new
APIClient(...)`) gets stripped in workflow mode. All instance methods
now call `const client = await this.ensureClient();` instead of
`this.client`.
### 3. `bundle: false` in tsdown config
Changed from single-file bundling to per-file output. This keeps Node.js
imports local to the files that use them, so after the SWC plugin strips
step method bodies, the now-unused Node.js imports can be eliminated by
esbuild's tree-shaking.
### 4. Workflow-code-runner example updated
- Removed `serverExternalPackages: ["@vercel/sandbox"]` from
`next.config.ts` (no longer needed)
- Updated `workflow` dependency to use a tarball that includes the
inline class serialization registration fix (vercel/workflow#1480)
## Testing
- `pnpm build` succeeds for the full monorepo (all 8 tasks)
- The `workflow-code-runner` example app builds successfully with all
workflow routes generated
- 22 `"use step"` directives survive compilation in both ESM and CJS
dist output
## Related
- vercel/workflow#1480 — Inline class serialization registration (fixes
SWC import resolution for 3rd-party packages)
- vercel/workflow#1481 — Build-time warning when
`serverExternalPackages` hides workflow-enabled packages
- vercel/workflow#1144 — CJS detection for serde symbols (pending
review)
- #58 — Previous attempt (superseded by this PR)
---------
Signed-off-by: Peter Wielander <mittgfu@gmail.com>
Co-authored-by: Peter Wielander <mittgfu@gmail.com>
Sign up for freeto 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.

4 participants

@TooTallNate@pranaygp@VaguelySerious
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Add CommonJS `require()` support for class serialization detection in SWC plugin by TooTallNate · Pull Request #1144 · vercel/workflow · GitHub
Skip to content

Add CommonJS require() support for class serialization detection in SWC plugin - #1144

Merged
TooTallNate merged 3 commits into
mainfrom
02-20-add_commonjs_require_support_for_class_serialization_detection_in_swc_plugin
Mar 23, 2026
Merged

Add CommonJS require() support for class serialization detection in SWC plugin#1144
TooTallNate merged 3 commits into
mainfrom
02-20-add_commonjs_require_support_for_class_serialization_detection_in_swc_plugin

Conversation

@TooTallNate

Copy link
Copy Markdown
Member

The SWC compiler plugin now detects classes with custom serialization methods (WORKFLOW_SERIALIZE / WORKFLOW_DESERIALIZE) when symbols are obtained via CommonJS require() calls, in addition to the existing ESM import and Symbol.for() patterns.

This handles code that has been pre-compiled from ESM to CommonJS by tools like TypeScript (tsc), esbuild, or tsup, which transform:

import{WORKFLOW_SERIALIZE}from"@workflow/serde"

into either:

constserde_1=require("@workflow/serde")// namespace requireconst{WORKFLOW_SERIALIZE}=require(...)// destructured require

Both patterns are now recognized during the identifier collection phase, and classes using them are properly registered with registerSerializationClass().

@changeset-bot

changeset-botBot commented Feb 20, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 14ea197

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

This PR includes changesets to release 17 packages
NameType
@workflow/swc-pluginPatch
@workflow/astroPatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nestPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
workflowPatch
@workflow/vitePatch
@workflow/vitestPatch
@workflow/world-testingPatch
@workflow/nuxtPatch
@workflow/aiPatch
@workflow/corePatch
@workflow/web-sharedPatch

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

@vercel

vercelBot commented Feb 20, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Feb 20, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production780067847
✅ 💻 Local Development7820142924
✅ 📦 Local Production7820142924
✅ 🐘 Local Postgres7820142924
✅ 🪟 Windows720577
❌ 🌍 Community Worlds1185621195
✅ 📋 Other198033231
Total3514565524122

❌ Failed Tests

🌍 Community Worlds (56 failed)

mongodb (3 failed):

  • hookWorkflow is not resumable via public webhook endpoint | wrun_01KMDYE4RWSYQ6538DVH70M5P0
  • webhookWorkflow | wrun_01KMDYED4N9B1F4PF1X6F86J26
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously | wrun_01KMDYMVS1CFTACB4081WS8K9V

redis (2 failed):

  • hookWorkflow is not resumable via public webhook endpoint | wrun_01KMDYE4RWSYQ6538DVH70M5P0
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously | wrun_01KMDYMVS1CFTACB4081WS8K9V

turso (51 failed):

  • addTenWorkflow | wrun_01KMDYCYXATZBJYFAPPKVRZKDZ
  • addTenWorkflow | wrun_01KMDYCYXATZBJYFAPPKVRZKDZ
  • wellKnownAgentWorkflow (.well-known/agent) | wrun_01KMDYEKVFEVFMCXC32DMPJ3KB
  • should work with react rendering in step
  • promiseAllWorkflow | wrun_01KMDYD5F67YMHR6KTJF5YFYM3
  • promiseRaceWorkflow | wrun_01KMDYDBXWV7065WDZ655A6X2Y
  • promiseAnyWorkflow | wrun_01KMDYDEFBS29N1Y4E04XS0PHS
  • importedStepOnlyWorkflow | wrun_01KMDYF31ZMHDYKRR9870BQJFE
  • hookWorkflow | wrun_01KMDYDTDYK5ZZTVGBV7PBPJAZ
  • hookWorkflow is not resumable via public webhook endpoint | wrun_01KMDYE4RWSYQ6538DVH70M5P0
  • webhookWorkflow | wrun_01KMDYED4N9B1F4PF1X6F86J26
  • sleepingWorkflow | wrun_01KMDYEKNPTWHR8GD4VJXRHSWR
  • parallelSleepWorkflow | wrun_01KMDYF1MMFSRK8683TXYGPEFF
  • nullByteWorkflow | wrun_01KMDYF6AS6SP633VT5B7JZ0Q8
  • workflowAndStepMetadataWorkflow | wrun_01KMDYF8B1WBHNCPZKHFYCH5G0
  • fetchWorkflow | wrun_01KMDYH6MPPT3CPGBXX02XNQTP
  • promiseRaceStressTestWorkflow | wrun_01KMDYH9Y93SDT6D7TEE6CMKHT
  • error handling error propagation workflow errors nested function calls preserve message and stack trace
  • error handling error propagation workflow errors cross-file imports preserve message and stack trace
  • error handling error propagation step errors basic step error preserves message and stack trace
  • error handling error propagation step errors cross-file step error preserves message and function names in stack
  • error handling retry behavior regular Error retries until success
  • error handling retry behavior FatalError fails immediately without retries
  • error handling retry behavior RetryableError respects custom retryAfter delay
  • error handling retry behavior maxRetries=0 disables retries
  • error handling catchability FatalError can be caught and detected with FatalError.is()
  • hookCleanupTestWorkflow - hook token reuse after workflow completion | wrun_01KMDYM7FJ0MWDXPS56VM7PF4D
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously | wrun_01KMDYMVS1CFTACB4081WS8K9V
  • hookDisposeTestWorkflow - hook token reuse after explicit disposal while workflow still running | wrun_01KMDYNGM0E0ZXFK8E2CQ9CYZV
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars) | wrun_01KMDYP5F3H7NXZ9W494MXF8EQ
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument | wrun_01KMDYPFQ0MK8D4B36398E2S6A
  • closureVariableWorkflow - nested step functions with closure variables | wrun_01KMDYPN9T3F9HBR5W1TQSBAVH
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step | wrun_01KMDYPQBVNH7GG5Y8ZDS8B1P3
  • health check (queue-based) - workflow and step endpoints respond to health check messages
  • pathsAliasWorkflow - TypeScript path aliases resolve correctly | wrun_01KMDYQ7ZKSHZ1DF03RZ64NGXD
  • Calculator.calculate - static workflow method using static step methods from another class | wrun_01KMDYQEMCVB21XR91WVHJEGK4
  • AllInOneService.processNumber - static workflow method using sibling static step methods | wrun_01KMDYQP3ST1XWR4FTRNPGNYE4
  • ChainableService.processWithThis - static step methods using this to reference the class | wrun_01KMDYQWMSK6ZYMFNY7ME546A9
  • thisSerializationWorkflow - step function invoked with .call() and .apply() | wrun_01KMDYR36AW71R74TKNQEA26QC
  • customSerializationWorkflow - custom class serialization with WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE | wrun_01KMDYR9J9RVJCAWSQG76N5656
  • instanceMethodStepWorkflow - instance methods with "use step" directive | wrun_01KMDYRH7WBXH2XGDZPC5CXB49
  • crossContextSerdeWorkflow - classes defined in step code are deserializable in workflow context | wrun_01KMDYRVMSG67Q29E0JT62SCPB
  • stepFunctionAsStartArgWorkflow - step function reference passed as start() argument | wrun_01KMDYS38DF4PJE3SJHH7YV0GQ
  • cancelRun - cancelling a running workflow | wrun_01KMDYSB4MVW6TNAVS70ZDC36V
  • cancelRun via CLI - cancelling a running workflow | wrun_01KMDYSM88CM0VKBMVABG1ZRM0
  • pages router addTenWorkflow via pages router
  • pages router promiseAllWorkflow via pages router
  • pages router sleepingWorkflow via pages router
  • hookWithSleepWorkflow - hook payloads delivered correctly with concurrent sleep | wrun_01KMDYT02V9K533MN8PA31072V
  • sleepInLoopWorkflow - sleep inside loop with steps actually delays each iteration | wrun_01KMDYTKS4H2ZFNHJC3AH7D70B
  • sleepWithSequentialStepsWorkflow - sequential steps work with concurrent sleep (control) | wrun_01KMDYTZMDM1Q8370F5R6NNHJV

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro7007
✅ example7007
✅ express7007
✅ fastify7007
✅ hono7007
✅ nextjs-turbopack7502
✅ nextjs-webpack7502
✅ nitro7007
✅ nuxt7007
✅ sveltekit7007
✅ vite7007
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable66011
✅ express-stable66011
✅ fastify-stable66011
✅ hono-stable66011
✅ nextjs-turbopack-canary55022
✅ nextjs-turbopack-stable7205
✅ nextjs-webpack-canary55022
✅ nextjs-webpack-stable7205
✅ nitro-stable66011
✅ nuxt-stable66011
✅ sveltekit-stable66011
✅ vite-stable66011
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable66011
✅ express-stable66011
✅ fastify-stable66011
✅ hono-stable66011
✅ nextjs-turbopack-canary55022
✅ nextjs-turbopack-stable7205
✅ nextjs-webpack-canary55022
✅ nextjs-webpack-stable7205
✅ nitro-stable66011
✅ nuxt-stable66011
✅ sveltekit-stable66011
✅ vite-stable66011
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable66011
✅ express-stable66011
✅ fastify-stable66011
✅ hono-stable66011
✅ nextjs-turbopack-canary55022
✅ nextjs-turbopack-stable7205
✅ nextjs-webpack-canary55022
✅ nextjs-webpack-stable7205
✅ nitro-stable66011
✅ nuxt-stable66011
✅ sveltekit-stable66011
✅ vite-stable66011
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack7205
❌ 🌍 Community Worlds
AppPassedFailedSkipped
✅ mongodb-dev302
❌ mongodb5235
✅ redis-dev302
❌ redis5325
✅ turso-dev302
❌ turso4515
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable66011
✅ e2e-local-postgres-nest-stable66011
✅ e2e-local-prod-nest-stable66011

📋 View full workflow run

@github-actions

github-actionsBot commented Feb 20, 2026

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

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

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro0.037s (-21.3% 🟢)1.005s (~)0.967s101.00x
💻 LocalExpress0.043s (-4.5%)1.005s (~)0.962s101.14x
🐘 PostgresNitro0.047s (-31.9% 🟢)1.012s (~)0.965s101.26x
🐘 PostgresExpress0.048s (-32.2% 🟢)1.012s (-0.5%)0.964s101.29x
💻 LocalNext.js (Turbopack)0.051s1.006s0.956s101.35x
🌐 RedisNext.js (Turbopack)0.054s1.005s0.952s101.44x
🐘 PostgresNext.js (Turbopack)0.062s1.012s0.950s101.65x
workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.097s (-2.8%)2.005s (~)0.908s101.00x
🌐 RedisNext.js (Turbopack)1.120s2.007s0.887s101.02x
🐘 PostgresNitro1.124s (-1.6%)2.012s (~)0.888s101.02x
💻 LocalNext.js (Turbopack)1.124s2.006s0.883s101.02x
🐘 PostgresExpress1.126s (-1.1%)2.012s (~)0.886s101.03x
💻 LocalExpress1.131s (~)2.005s (~)0.874s101.03x
🐘 PostgresNext.js (Turbopack)1.143s2.013s0.869s101.04x
workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro10.635s (-2.4%)11.023s (~)0.388s31.00x
🌐 RedisNext.js (Turbopack)10.680s11.025s0.345s31.00x
🐘 PostgresNitro10.689s (-2.4%)11.036s (~)0.348s31.01x
🐘 PostgresExpress10.705s (-1.8%)11.038s (~)0.333s31.01x
💻 LocalNext.js (Turbopack)10.847s11.025s0.178s31.02x
💻 LocalExpress10.934s (~)11.023s (~)0.089s31.03x
🐘 PostgresNext.js (Turbopack)10.996s11.382s0.386s31.03x
workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)14.091s15.033s0.941s41.00x
🐘 PostgresExpress14.123s (-3.1%)15.042s (~)0.919s41.00x
🐘 PostgresNitro14.129s (-4.7%)15.039s (~)0.910s41.00x
💻 LocalNitro14.183s (-4.9%)15.029s (~)0.847s41.01x
🐘 PostgresNext.js (Turbopack)14.580s15.049s0.469s41.03x
💻 LocalNext.js (Turbopack)14.703s15.031s0.328s41.04x
💻 LocalExpress14.911s (-0.6%)15.029s (-3.2%)0.118s41.06x
workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)13.030s13.457s0.427s71.00x
🐘 PostgresExpress13.096s (-6.5% 🟢)14.038s (-3.0%)0.942s71.01x
🐘 PostgresNitro13.119s (-8.7% 🟢)14.038s (-6.7% 🟢)0.919s71.01x
🐘 PostgresNext.js (Turbopack)14.145s14.759s0.613s71.09x
💻 LocalNitro14.789s (-10.1% 🟢)15.026s (-11.8% 🟢)0.237s61.14x
💻 LocalNext.js (Turbopack)16.157s17.033s0.875s61.24x
💻 LocalExpress16.514s (-1.0%)17.029s (~)0.516s61.27x
Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.228s (-3.0%)2.011s (~)0.783s151.00x
🐘 PostgresNitro1.231s (-4.8%)2.011s (~)0.780s151.00x
🌐 RedisNext.js (Turbopack)1.266s2.007s0.741s151.03x
🐘 PostgresNext.js (Turbopack)1.286s2.011s0.725s151.05x
💻 LocalNitro1.472s (-0.8%)2.005s (~)0.534s151.20x
💻 LocalExpress1.503s (~)2.006s (~)0.503s151.22x
💻 LocalNext.js (Turbopack)1.524s2.006s0.482s151.24x
Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express2.341s (-4.9%)3.012s (~)0.671s101.00x
🐘 PostgresNitro2.353s (-4.8%)3.013s (~)0.660s101.01x
🌐 RedisNext.js (Turbopack)2.478s3.009s0.531s101.06x
🐘 PostgresNext.js (Turbopack)2.484s3.015s0.531s101.06x
💻 LocalNitro2.551s (-11.2% 🟢)3.007s (-3.2%)0.456s101.09x
💻 LocalExpress2.970s (-0.9%)3.308s (-7.2% 🟢)0.338s101.27x
💻 LocalNext.js (Turbopack)2.995s3.309s0.314s101.28x
Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express3.519s (-1.9%)4.014s (~)0.495s81.00x
🐘 PostgresNitro3.523s (-3.0%)4.015s (~)0.492s81.00x
🐘 PostgresNext.js (Turbopack)3.844s4.017s0.173s81.09x
🌐 RedisNext.js (Turbopack)4.117s4.727s0.610s71.17x
💻 LocalNitro7.025s (-13.5% 🟢)7.514s (-14.3% 🟢)0.489s42.00x
💻 LocalExpress8.144s (-3.9%)8.769s (-2.8%)0.625s42.31x
💻 LocalNext.js (Turbopack)8.563s9.019s0.456s42.43x
Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.207s (-4.1%)2.011s (~)0.803s151.00x
🐘 PostgresNitro1.216s (-4.7%)2.010s (~)0.794s151.01x
🌐 RedisNext.js (Turbopack)1.252s2.007s0.755s151.04x
🐘 PostgresNext.js (Turbopack)1.254s2.011s0.757s151.04x
💻 LocalNitro1.484s (-4.9%)2.006s (~)0.522s151.23x
💻 LocalNext.js (Turbopack)1.515s2.005s0.490s151.25x
💻 LocalExpress1.550s (+0.9%)2.005s (~)0.455s151.28x
Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro2.342s (-4.7%)3.012s (~)0.670s101.00x
🐘 PostgresExpress2.353s (-4.0%)3.012s (~)0.659s101.00x
🐘 PostgresNext.js (Turbopack)2.474s3.013s0.539s101.06x
🌐 RedisNext.js (Turbopack)2.498s3.009s0.511s101.07x
💻 LocalNitro2.924s (-3.4%)3.208s (-12.7% 🟢)0.283s101.25x
💻 LocalNext.js (Turbopack)3.012s3.675s0.663s91.29x
💻 LocalExpress3.090s (+1.9%)3.885s (+3.4%)0.795s81.32x
Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express3.515s (-2.3%)4.013s (~)0.497s81.00x
🐘 PostgresNitro3.518s (-2.7%)4.014s (~)0.496s81.00x
🐘 PostgresNext.js (Turbopack)3.841s4.018s0.177s81.09x
🌐 RedisNext.js (Turbopack)4.031s4.583s0.553s71.15x
💻 LocalNitro7.511s (-13.1% 🟢)8.016s (-11.2% 🟢)0.506s42.14x
💻 LocalExpress8.608s (-5.0%)9.020s (-7.7% 🟢)0.412s42.45x
💻 LocalNext.js (Turbopack)9.214s9.772s0.557s42.62x
workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)0.601s1.005s0.404s601.00x
🐘 PostgresExpress0.642s (-25.8% 🟢)1.009s (-1.7%)0.366s601.07x
🐘 PostgresNitro0.657s (-26.8% 🟢)1.008s (-3.4%)0.351s601.09x
💻 LocalNitro0.672s (-29.7% 🟢)1.004s (-1.7%)0.332s601.12x
🐘 PostgresNext.js (Turbopack)0.870s1.009s0.140s601.45x
💻 LocalNext.js (Turbopack)0.878s1.057s0.179s571.46x
💻 LocalExpress0.973s (-1.0%)1.114s (+1.7%)0.140s551.62x
workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)1.460s2.006s0.546s451.00x
🐘 PostgresExpress1.553s (-26.6% 🟢)2.010s (-33.3% 🟢)0.457s451.06x
🐘 PostgresNitro1.563s (-27.7% 🟢)2.032s (-31.8% 🟢)0.470s451.07x
🐘 PostgresNext.js (Turbopack)2.127s2.948s0.821s311.46x
💻 LocalNitro2.193s (-25.8% 🟢)3.007s (-3.3%)0.815s301.50x
💻 LocalNext.js (Turbopack)2.736s3.008s0.272s301.87x
💻 LocalExpress2.966s (-1.6%)3.222s (-9.2% 🟢)0.255s282.03x
workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)3.077s3.437s0.360s351.00x
🐘 PostgresNitro3.153s (-27.3% 🟢)4.012s (-20.0% 🟢)0.859s301.02x
🐘 PostgresExpress3.188s (-25.2% 🟢)4.012s (-19.3% 🟢)0.824s301.04x
🐘 PostgresNext.js (Turbopack)4.345s5.015s0.670s241.41x
💻 LocalNitro7.218s (-20.1% 🟢)7.827s (-17.4% 🟢)0.610s162.35x
💻 LocalNext.js (Turbopack)8.797s9.232s0.434s142.86x
💻 LocalExpress8.995s (-1.7%)9.479s (-3.1%)0.484s132.92x
workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.243s (-19.7% 🟢)1.008s (~)0.766s601.00x
🐘 PostgresNitro0.245s (-16.9% 🟢)1.009s (~)0.764s601.01x
🌐 RedisNext.js (Turbopack)0.260s1.005s0.744s601.07x
🐘 PostgresNext.js (Turbopack)0.287s1.010s0.723s601.18x
💻 LocalNitro0.576s (-3.7%)1.004s (~)0.428s602.37x
💻 LocalExpress0.581s (-3.7%)1.004s (~)0.424s602.39x
💻 LocalNext.js (Turbopack)0.585s1.004s0.420s602.41x
workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.419s (-20.5% 🟢)1.009s (~)0.589s901.00x
🐘 PostgresNitro0.424s (-21.4% 🟢)1.009s (~)0.585s901.01x
🐘 PostgresNext.js (Turbopack)0.590s1.033s0.443s881.41x
🌐 RedisNext.js (Turbopack)1.194s2.006s0.812s452.85x
💻 LocalNitro2.345s (-5.2% 🟢)3.007s (~)0.662s305.59x
💻 LocalExpress2.498s (-3.2%)3.009s (~)0.510s305.96x
💻 LocalNext.js (Turbopack)2.664s3.009s0.345s306.36x
workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.606s (-35.5% 🟢)1.009s (-15.4% 🟢)0.403s1191.00x
🐘 PostgresExpress0.620s (-30.9% 🟢)1.009s (-3.1%)0.389s1191.02x
🐘 PostgresNext.js (Turbopack)1.001s1.477s0.476s821.65x
🌐 RedisNext.js (Turbopack)2.883s3.085s0.202s394.76x
💻 LocalNitro10.189s (-8.3% 🟢)10.855s (-6.9% 🟢)0.666s1216.83x
💻 LocalExpress11.127s (-1.4%)11.845s (-1.5%)0.718s1118.38x
💻 LocalNext.js (Turbopack)11.287s11.753s0.465s1118.64x
Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro0.140s (-28.6% 🟢)1.003s (~)0.009s (-26.3% 🟢)1.014s (~)0.874s101.00x
🐘 PostgresExpress0.164s (-24.1% 🟢)0.999s (+0.5%)0.001s (-8.3% 🟢)1.013s (~)0.848s101.17x
🐘 PostgresNitro0.167s (-30.3% 🟢)0.999s (+0.7%)0.001s (-21.4% 🟢)1.013s (~)0.846s101.19x
🌐 RedisNext.js (Turbopack)0.169s1.000s0.001s1.008s0.838s101.21x
💻 LocalNext.js (Turbopack)0.178s1.001s0.012s1.018s0.840s101.27x
💻 LocalExpress0.199s (-2.4%)1.003s (~)0.012s (-2.5%)1.017s (~)0.818s101.42x
🐘 PostgresNext.js (Turbopack)0.224s1.001s0.001s1.013s0.789s101.60x
stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)0.447s0.999s0.003s1.012s0.565s601.00x
🐘 PostgresNitro0.563s (-22.1% 🟢)1.005s (~)0.003s (-13.6% 🟢)1.023s (~)0.460s591.26x
💻 LocalNitro0.569s (-21.7% 🟢)1.008s (~)0.009s (-6.2% 🟢)1.021s (~)0.452s591.27x
🐘 PostgresExpress0.610s (-12.0% 🟢)1.006s (~)0.005s (+20.4% 🔺)1.028s (~)0.417s591.37x
💻 LocalNext.js (Turbopack)0.672s1.008s0.010s1.024s0.352s591.50x
💻 LocalExpress0.723s (-6.9% 🟢)1.009s (-1.7%)0.009s (-12.1% 🟢)1.022s (-1.8%)0.300s591.62x
🐘 PostgresNext.js (Turbopack)0.760s1.026s0.008s1.050s0.290s591.70x
10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)0.916s1.016s0.000s1.022s0.106s591.00x
🐘 PostgresNitro0.988s (+7.3% 🔺)1.384s (+30.2% 🔺)0.000s (-36.4% 🟢)1.398s (+29.3% 🔺)0.410s441.08x
🐘 PostgresExpress1.066s (+19.3% 🔺)1.664s (+53.7% 🔺)0.000s (+205.6% 🔺)1.677s (+51.8% 🔺)0.612s361.16x
💻 LocalNitro1.140s (-6.9% 🟢)2.017s (~)0.000s (-38.5% 🟢)2.020s (~)0.880s301.24x
🐘 PostgresNext.js (Turbopack)1.175s1.909s0.000s1.930s0.755s321.28x
💻 LocalExpress1.235s (-1.8%)2.020s (~)0.000s (-35.3% 🟢)2.023s (~)0.788s301.35x
💻 LocalNext.js (Turbopack)1.295s2.019s0.000s2.024s0.729s301.41x
fan-out fan-in 10 streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.886s (~)2.223s (~)0.000s (-100.0% 🟢)2.233s (~)0.347s271.00x
🌐 RedisNext.js (Turbopack)1.963s2.180s0.000s2.186s0.223s281.04x
🐘 PostgresExpress2.034s (+12.7% 🔺)2.441s (+16.3% 🔺)0.000s (+Infinity% 🔺)2.453s (+15.6% 🔺)0.418s251.08x
🐘 PostgresNext.js (Turbopack)2.302s3.005s0.000s3.015s0.713s201.22x
💻 LocalNitro3.350s (+0.6%)3.967s (~)0.000s (-60.0% 🟢)3.971s (~)0.621s161.78x
💻 LocalExpress3.508s (+1.9%)4.030s (-1.6%)0.001s (+37.5% 🔺)4.035s (-1.6%)0.527s151.86x
💻 LocalNext.js (Turbopack)3.784s4.387s0.000s4.392s0.608s142.01x

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNitro21/21
🐘 PostgresExpress12/21
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express🐘 Postgres20/21
Next.js (Turbopack)🌐 Redis15/21
Nitro🐘 Postgres17/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)

📋 View full workflow run


Some benchmark jobs failed:

  • Local: success
  • Postgres: success
  • Vercel: failure

Check the workflow run for details.

@TooTallNateGraphite App

Copy link
Copy Markdown
MemberAuthor

This stack of pull requests is managed by Graphite. Learn more about stacking.

@TooTallNate
TooTallNate marked this pull request as ready for review February 20, 2026 22:47
CopilotAI review requested due to automatic review settings February 20, 2026 22:47

CopilotAI 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.

Pull request overview

This PR extends the @workflow/swc-plugin transform to detect custom class serialization methods when the WORKFLOW_SERIALIZE / WORKFLOW_DESERIALIZE symbols are accessed via CommonJS require() patterns, aligning behavior with codebases that have been transpiled from ESM to CJS.

Changes:

  • Track CommonJS require() bindings during identifier collection (namespace const x = require(...) and destructured const { ... } = require(...)) and recognize x.WORKFLOW_SERIALIZE / x.WORKFLOW_DESERIALIZE as serialization symbols.
  • Add fixture coverage for both namespace-require and destructured-require inputs across workflow/step/client outputs.
  • Update the plugin spec and add a changeset entry documenting the new behavior.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 2 comments.

Show a summary per file
FileDescription
packages/swc-plugin-workflow/transform/src/lib.rsAdds require-binding tracking and member-expression symbol recognition for serialization detection.
packages/swc-plugin-workflow/transform/tests/fixture/custom-serialization-require-namespace/input.jsNew fixture input for namespace require() pattern.
packages/swc-plugin-workflow/transform/tests/fixture/custom-serialization-require-namespace/output-workflow.jsExpected workflow-mode output for namespace require() fixture.
packages/swc-plugin-workflow/transform/tests/fixture/custom-serialization-require-namespace/output-step.jsExpected step-mode output for namespace require() fixture.
packages/swc-plugin-workflow/transform/tests/fixture/custom-serialization-require-namespace/output-client.jsExpected client-mode output for namespace require() fixture.
packages/swc-plugin-workflow/transform/tests/fixture/custom-serialization-require-destructured/input.jsNew fixture input for destructured require() pattern.
packages/swc-plugin-workflow/transform/tests/fixture/custom-serialization-require-destructured/output-workflow.jsExpected workflow-mode output for destructured require() fixture.
packages/swc-plugin-workflow/transform/tests/fixture/custom-serialization-require-destructured/output-step.jsExpected step-mode output for destructured require() fixture.
packages/swc-plugin-workflow/transform/tests/fixture/custom-serialization-require-destructured/output-client.jsExpected client-mode output for destructured require() fixture.
packages/swc-plugin-workflow/spec.mdDocuments supported CommonJS require() symbol patterns.
.changeset/social-forks-tease.mdDeclares a patch release for the new detection behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment threadpackages/swc-plugin-workflow/transform/src/lib.rs Outdated
Comment threadpackages/swc-plugin-workflow/transform/src/lib.rs Outdated
…WC plugin
The SWC compiler plugin now detects classes with custom serialization methods (`WORKFLOW_SERIALIZE` / `WORKFLOW_DESERIALIZE`) when symbols are obtained via CommonJS `require()` calls, in addition to the existing ESM import and `Symbol.for()` patterns.
This handles code that has been pre-compiled from ESM to CommonJS by tools like TypeScript (tsc), esbuild, or tsup, which transform:
```ts
import { WORKFLOW_SERIALIZE } from "@workflow/serde"
```
into either:
```ts
const serde_1 = require("@workflow/serde") // namespace require
const { WORKFLOW_SERIALIZE } = require(...) // destructured require
```
Both patterns are now recognized during the identifier collection phase, and classes using them are properly registered with registerSerializationClass().
@TooTallNateTooTallNate changed the title Add CommonJS require() support for class serialization detection in SWC pluginAdd CommonJS require() support for class serialization detection in SWC pluginMar 23, 2026
…on_bindings helper
- Tighten is_require_call to require exactly one string literal argument,
rejecting dynamic requires and multi-arg calls.
- Extract track_serialization_bindings helper to deduplicate the require
tracking logic between Decl::Var and ExportDecl(Decl::Var) branches.
@TooTallNate
TooTallNateforce-pushed the 02-20-add_commonjs_require_support_for_class_serialization_detection_in_swc_plugin branch from 3648e46 to f461c59CompareMarch 23, 2026 14:01
@TooTallNate
TooTallNate requested a review from a team as a code ownerMarch 23, 2026 14:01
@TooTallNate
TooTallNate merged commit 992d768 into mainMar 23, 2026
98 of 103 checks passed
@TooTallNate
TooTallNate deleted the 02-20-add_commonjs_require_support_for_class_serialization_detection_in_swc_plugin branch March 23, 2026 18:30
@ghostghost mentioned this pull request Mar 23, 2026

@pranaygppranaygp 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.

Post-merge review — overall this is a clean, well-tested addition. A few observations below.

// Track CommonJS namespace require: const serde_1 = require("...")
if self.is_require_call(init) {
self.require_namespace_identifiers
.insert(ident.id.sym.to_string());

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.

Nit: This tracks everyrequire() call as a namespace identifier, regardless of the module being required. So const fs = require("fs") would cause fs.WORKFLOW_SERIALIZE to be treated as a serialization symbol if it ever appeared. This is unlikely to cause real bugs since nobody would name a property WORKFLOW_SERIALIZE on an unrelated module, but it is semantically imprecise.

Consider either:

  1. Restricting to known serde modules (e.g. checking the string arg contains @workflow/serde or similar), or
  2. Documenting this as an intentional trade-off (broad tracking is simpler and the false-positive risk is negligible).

match prop {
ObjectPatProp::Assign(assign) => {
// const { WORKFLOW_SERIALIZE } = require("...")
let name = assign.key.sym.to_string();

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.

Nit: The ObjectPatProp::Assign arm handles const { WORKFLOW_SERIALIZE } = require(...), but in the SWC AST this node actually represents const { WORKFLOW_SERIALIZE = defaultValue } = require(...) (a destructured binding with a default value). A plain shorthand const { WORKFLOW_SERIALIZE } = require(...) is typically parsed as ObjectPatProp::KeyValue where key and value ident are the same.

In practice this still works because SWC may represent the no-default shorthand either way depending on version, and the logic is correct for both paths. But worth noting that the Assign arm would also match const { WORKFLOW_SERIALIZE = someDefault } = require(...), which is an unusual but technically valid pattern.

@@ -0,0 +1,25 @@
// Test custom serialization with CommonJS destructured require
const { WORKFLOW_SERIALIZE, WORKFLOW_DESERIALIZE } = require("@workflow/serde");

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.

The spec documents renamed destructured require (const { WORKFLOW_SERIALIZE: WS } = require(...)) but there's no test fixture covering this case. Adding a custom-serialization-require-destructured-renamed fixture would ensure the ObjectPatProp::KeyValue branch stays correct.

pranaygp added a commit that referenced this pull request Mar 23, 2026
* origin/main:
[ai] Add experimental_context to DurableAgentOptions (#1489)
[ai] Expose configured tools on DurableAgent instances (#1488)
fix(builders): catch node builtin usage when entry fields diverge (#1455)
[web-shared] Fix timeline duration format and precision (#1482)
[cli] Add bulk cancel, --status filter, fix step JSON hydration (#1467)
[utils] Re-export parseName utilities and add workflow/observability module (#1453)
[o11y] Polish display when run data has expired (#1438)
Add CommonJS `require()` support for class serialization detection in SWC plugin (#1144)
fix(next): stabilize deferred canary e2e in nextjs workbenches (#1468)
[web] Support legacy newline-delimited stream format in `useStreamReader` (#1473)
Revert "Add support for calling start() inside workflow functions (#1133)" (#1475)
pranaygp added a commit that referenced this pull request Mar 24, 2026
…naygp-db9e68c1
* 'main' of https://github.com/vercel/workflow: (32 commits)
chore: bump @nestjs/* to ^11.1.17 (#1497)
chore: bump hono to ^4.12.8 (#1495)
Revert "Inline class serialization registration to fix 3rd-party package supp…" (#1493)
[world] Add stream pagination and metadata endpoints (#1470)
[cli] [world-local] Ensure update checks don't suggest upgrading from stable release to pre-releases (#1490)
Remove NestJS Vercel integration while in experimental phase (#1485)
feat: export semantic error types and add API reference docs (#1447)
feat: enforce max queue deliveries in handlers with graceful failure (#1344)
[world-postgres] Migrate client from `postgres.js` to `pg` (#1484)
Inline class serialization registration to fix 3rd-party package support (#1480)
[ai] Add experimental_context to DurableAgentOptions (#1489)
[ai] Expose configured tools on DurableAgent instances (#1488)
fix(builders): catch node builtin usage when entry fields diverge (#1455)
[web-shared] Fix timeline duration format and precision (#1482)
[cli] Add bulk cancel, --status filter, fix step JSON hydration (#1467)
[utils] Re-export parseName utilities and add workflow/observability module (#1453)
[o11y] Polish display when run data has expired (#1438)
Add CommonJS `require()` support for class serialization detection in SWC plugin (#1144)
fix(next): stabilize deferred canary e2e in nextjs workbenches (#1468)
[web] Support legacy newline-delimited stream format in `useStreamReader` (#1473)
...
TooTallNate added a commit that referenced this pull request Mar 24, 2026
The original PR #1480 was merged but reverted because it didn't include
updated fixtures for the CJS require patterns added by PR #1144
(custom-serialization-require-destructured and
custom-serialization-require-namespace). These fixtures still had the
old 'import { registerSerializationClass }' pattern instead of the
new inline IIFE.
TooTallNate added a commit that referenced this pull request Mar 24, 2026
…ort (v2) (#1503)
* Inline class serialization registration to fix 3rd-party package support (#1480)
* Inline class serialization registration to fix 3rd-party package support
The SWC plugin previously generated:
import { registerSerializationClass } from "workflow/internal/class-serialization";
registerSerializationClass("class//...", ClassName);
This broke for 3rd-party packages (e.g. @vercel/sandbox) that define
serializable classes but don't depend on the 'workflow' package. The
bare 'workflow' specifier is unresolvable from within node_modules of
a package that doesn't list it as a dependency.
Now the plugin generates a self-contained IIFE that uses
Symbol.for('workflow-class-registry') on globalThis directly, with
zero module dependencies:
(function(__wf_cls, __wf_id) {
var __wf_sym = Symbol.for("workflow-class-registry"),
__wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map());
__wf_reg.set(__wf_id, __wf_cls);
Object.defineProperty(__wf_cls, "classId", { ... });
})(ClassName, "class//...");
This is fully compatible with the existing deserialization side in
@workflow/core which reads from the same globalThis registry.
* Address review feedback: fix comment and update docstring
- Fix inaccurate IIFE comment in lib.rs: the second arg is the
generated class ID string, not the literal "classId"
- Update registerSerializationClass docstring to reflect that the
SWC plugin now inlines equivalent logic rather than importing it
* Update CJS require fixture outputs for inline class serialization
The original PR #1480 was merged but reverted because it didn't include
updated fixtures for the CJS require patterns added by PR #1144
(custom-serialization-require-destructured and
custom-serialization-require-namespace). These fixtures still had the
old 'import { registerSerializationClass }' pattern instead of the
new inline IIFE.
pranaygp pushed a commit to vercel/sandbox that referenced this pull request Mar 27, 2026
…ibility (#109)
## Summary
Makes `@vercel/sandbox` fully compatible with the Workflow DevKit
compiler so that `Sandbox`, `Command`, and `CommandFinished` instances
can be used directly inside `"use workflow"` functions — no wrapper step
functions needed.
Supersedes #58.
## Problem
When `@vercel/sandbox` is imported in a workflow context, the workflow
builder tries to bundle it into the workflow VM bundle (because it has
`WORKFLOW_SERIALIZE`/`WORKFLOW_DESERIALIZE` on its classes). This fails
because:
1. The SDK's public methods use Node.js APIs (`fs`, `stream`, `zlib`,
`undici`, etc.) which are forbidden in the workflow VM
2. The compiled `dist/index.js` was a single bundled file that hoisted
all Node.js imports to the top, making them impossible to tree-shake
3. The `Sandbox` and `Command` classes had a sync `client` getter that
directly referenced `APIClient`, pulling the entire HTTP client stack
into the module scope
## Solution
### 1. `"use step"` annotations on all public async methods
Added `"use step"` to all 22 public async methods across `Sandbox` (14),
`Command` (5), and `Snapshot` (3). The SWC plugin strips these method
bodies in workflow mode, replacing them with durable step proxies. This
eliminates all Node.js API references from the workflow bundle.
### 2. Async `ensureClient()` replaces sync `client` getter
The sync `get client()` getter directly referenced `APIClient`, which
pulls in `undici`, `zlib`, `tar-stream`, `jsonlines`, etc. Replaced
with:
```typescript
private async ensureClient(): Promise<APIClient> {
"use step";
if (this._client) return this._client;
const credentials = getSandboxCredentials();
this._client = new APIClient({ ... });
return this._client;
}
```
Since `ensureClient()` is itself `"use step"`, its body (including `new
APIClient(...)`) gets stripped in workflow mode. All instance methods
now call `const client = await this.ensureClient();` instead of
`this.client`.
### 3. `bundle: false` in tsdown config
Changed from single-file bundling to per-file output. This keeps Node.js
imports local to the files that use them, so after the SWC plugin strips
step method bodies, the now-unused Node.js imports can be eliminated by
esbuild's tree-shaking.
### 4. Workflow-code-runner example updated
- Removed `serverExternalPackages: ["@vercel/sandbox"]` from
`next.config.ts` (no longer needed)
- Updated `workflow` dependency to use a tarball that includes the
inline class serialization registration fix (vercel/workflow#1480)
## Testing
- `pnpm build` succeeds for the full monorepo (all 8 tasks)
- The `workflow-code-runner` example app builds successfully with all
workflow routes generated
- 22 `"use step"` directives survive compilation in both ESM and CJS
dist output
## Related
- vercel/workflow#1480 — Inline class serialization registration (fixes
SWC import resolution for 3rd-party packages)
- vercel/workflow#1481 — Build-time warning when
`serverExternalPackages` hides workflow-enabled packages
- vercel/workflow#1144 — CJS detection for serde symbols (pending
review)
- #58 — Previous attempt (superseded by this PR)
---------
Signed-off-by: Peter Wielander <mittgfu@gmail.com>
Co-authored-by: Peter Wielander <mittgfu@gmail.com>
Sign up for freeto 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.

4 participants

@TooTallNate@pranaygp@VaguelySerious
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Add CommonJS `require()` support for class serialization detection in SWC plugin by TooTallNate · Pull Request #1144 · vercel/workflow · GitHub
Skip to content

Add CommonJS require() support for class serialization detection in SWC plugin - #1144

Merged
TooTallNate merged 3 commits into
mainfrom
02-20-add_commonjs_require_support_for_class_serialization_detection_in_swc_plugin
Mar 23, 2026
Merged

Add CommonJS require() support for class serialization detection in SWC plugin#1144
TooTallNate merged 3 commits into
mainfrom
02-20-add_commonjs_require_support_for_class_serialization_detection_in_swc_plugin

Conversation

@TooTallNate

Copy link
Copy Markdown
Member

The SWC compiler plugin now detects classes with custom serialization methods (WORKFLOW_SERIALIZE / WORKFLOW_DESERIALIZE) when symbols are obtained via CommonJS require() calls, in addition to the existing ESM import and Symbol.for() patterns.

This handles code that has been pre-compiled from ESM to CommonJS by tools like TypeScript (tsc), esbuild, or tsup, which transform:

import{WORKFLOW_SERIALIZE}from"@workflow/serde"

into either:

constserde_1=require("@workflow/serde")// namespace requireconst{WORKFLOW_SERIALIZE}=require(...)// destructured require

Both patterns are now recognized during the identifier collection phase, and classes using them are properly registered with registerSerializationClass().

@changeset-bot

changeset-botBot commented Feb 20, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 14ea197

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

This PR includes changesets to release 17 packages
NameType
@workflow/swc-pluginPatch
@workflow/astroPatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nestPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
workflowPatch
@workflow/vitePatch
@workflow/vitestPatch
@workflow/world-testingPatch
@workflow/nuxtPatch
@workflow/aiPatch
@workflow/corePatch
@workflow/web-sharedPatch

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

@vercel

vercelBot commented Feb 20, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Feb 20, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production780067847
✅ 💻 Local Development7820142924
✅ 📦 Local Production7820142924
✅ 🐘 Local Postgres7820142924
✅ 🪟 Windows720577
❌ 🌍 Community Worlds1185621195
✅ 📋 Other198033231
Total3514565524122

❌ Failed Tests

🌍 Community Worlds (56 failed)

mongodb (3 failed):

  • hookWorkflow is not resumable via public webhook endpoint | wrun_01KMDYE4RWSYQ6538DVH70M5P0
  • webhookWorkflow | wrun_01KMDYED4N9B1F4PF1X6F86J26
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously | wrun_01KMDYMVS1CFTACB4081WS8K9V

redis (2 failed):

  • hookWorkflow is not resumable via public webhook endpoint | wrun_01KMDYE4RWSYQ6538DVH70M5P0
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously | wrun_01KMDYMVS1CFTACB4081WS8K9V

turso (51 failed):

  • addTenWorkflow | wrun_01KMDYCYXATZBJYFAPPKVRZKDZ
  • addTenWorkflow | wrun_01KMDYCYXATZBJYFAPPKVRZKDZ
  • wellKnownAgentWorkflow (.well-known/agent) | wrun_01KMDYEKVFEVFMCXC32DMPJ3KB
  • should work with react rendering in step
  • promiseAllWorkflow | wrun_01KMDYD5F67YMHR6KTJF5YFYM3
  • promiseRaceWorkflow | wrun_01KMDYDBXWV7065WDZ655A6X2Y
  • promiseAnyWorkflow | wrun_01KMDYDEFBS29N1Y4E04XS0PHS
  • importedStepOnlyWorkflow | wrun_01KMDYF31ZMHDYKRR9870BQJFE
  • hookWorkflow | wrun_01KMDYDTDYK5ZZTVGBV7PBPJAZ
  • hookWorkflow is not resumable via public webhook endpoint | wrun_01KMDYE4RWSYQ6538DVH70M5P0
  • webhookWorkflow | wrun_01KMDYED4N9B1F4PF1X6F86J26
  • sleepingWorkflow | wrun_01KMDYEKNPTWHR8GD4VJXRHSWR
  • parallelSleepWorkflow | wrun_01KMDYF1MMFSRK8683TXYGPEFF
  • nullByteWorkflow | wrun_01KMDYF6AS6SP633VT5B7JZ0Q8
  • workflowAndStepMetadataWorkflow | wrun_01KMDYF8B1WBHNCPZKHFYCH5G0
  • fetchWorkflow | wrun_01KMDYH6MPPT3CPGBXX02XNQTP
  • promiseRaceStressTestWorkflow | wrun_01KMDYH9Y93SDT6D7TEE6CMKHT
  • error handling error propagation workflow errors nested function calls preserve message and stack trace
  • error handling error propagation workflow errors cross-file imports preserve message and stack trace
  • error handling error propagation step errors basic step error preserves message and stack trace
  • error handling error propagation step errors cross-file step error preserves message and function names in stack
  • error handling retry behavior regular Error retries until success
  • error handling retry behavior FatalError fails immediately without retries
  • error handling retry behavior RetryableError respects custom retryAfter delay
  • error handling retry behavior maxRetries=0 disables retries
  • error handling catchability FatalError can be caught and detected with FatalError.is()
  • hookCleanupTestWorkflow - hook token reuse after workflow completion | wrun_01KMDYM7FJ0MWDXPS56VM7PF4D
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously | wrun_01KMDYMVS1CFTACB4081WS8K9V
  • hookDisposeTestWorkflow - hook token reuse after explicit disposal while workflow still running | wrun_01KMDYNGM0E0ZXFK8E2CQ9CYZV
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars) | wrun_01KMDYP5F3H7NXZ9W494MXF8EQ
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument | wrun_01KMDYPFQ0MK8D4B36398E2S6A
  • closureVariableWorkflow - nested step functions with closure variables | wrun_01KMDYPN9T3F9HBR5W1TQSBAVH
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step | wrun_01KMDYPQBVNH7GG5Y8ZDS8B1P3
  • health check (queue-based) - workflow and step endpoints respond to health check messages
  • pathsAliasWorkflow - TypeScript path aliases resolve correctly | wrun_01KMDYQ7ZKSHZ1DF03RZ64NGXD
  • Calculator.calculate - static workflow method using static step methods from another class | wrun_01KMDYQEMCVB21XR91WVHJEGK4
  • AllInOneService.processNumber - static workflow method using sibling static step methods | wrun_01KMDYQP3ST1XWR4FTRNPGNYE4
  • ChainableService.processWithThis - static step methods using this to reference the class | wrun_01KMDYQWMSK6ZYMFNY7ME546A9
  • thisSerializationWorkflow - step function invoked with .call() and .apply() | wrun_01KMDYR36AW71R74TKNQEA26QC
  • customSerializationWorkflow - custom class serialization with WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE | wrun_01KMDYR9J9RVJCAWSQG76N5656
  • instanceMethodStepWorkflow - instance methods with "use step" directive | wrun_01KMDYRH7WBXH2XGDZPC5CXB49
  • crossContextSerdeWorkflow - classes defined in step code are deserializable in workflow context | wrun_01KMDYRVMSG67Q29E0JT62SCPB
  • stepFunctionAsStartArgWorkflow - step function reference passed as start() argument | wrun_01KMDYS38DF4PJE3SJHH7YV0GQ
  • cancelRun - cancelling a running workflow | wrun_01KMDYSB4MVW6TNAVS70ZDC36V
  • cancelRun via CLI - cancelling a running workflow | wrun_01KMDYSM88CM0VKBMVABG1ZRM0
  • pages router addTenWorkflow via pages router
  • pages router promiseAllWorkflow via pages router
  • pages router sleepingWorkflow via pages router
  • hookWithSleepWorkflow - hook payloads delivered correctly with concurrent sleep | wrun_01KMDYT02V9K533MN8PA31072V
  • sleepInLoopWorkflow - sleep inside loop with steps actually delays each iteration | wrun_01KMDYTKS4H2ZFNHJC3AH7D70B
  • sleepWithSequentialStepsWorkflow - sequential steps work with concurrent sleep (control) | wrun_01KMDYTZMDM1Q8370F5R6NNHJV

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro7007
✅ example7007
✅ express7007
✅ fastify7007
✅ hono7007
✅ nextjs-turbopack7502
✅ nextjs-webpack7502
✅ nitro7007
✅ nuxt7007
✅ sveltekit7007
✅ vite7007
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable66011
✅ express-stable66011
✅ fastify-stable66011
✅ hono-stable66011
✅ nextjs-turbopack-canary55022
✅ nextjs-turbopack-stable7205
✅ nextjs-webpack-canary55022
✅ nextjs-webpack-stable7205
✅ nitro-stable66011
✅ nuxt-stable66011
✅ sveltekit-stable66011
✅ vite-stable66011
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable66011
✅ express-stable66011
✅ fastify-stable66011
✅ hono-stable66011
✅ nextjs-turbopack-canary55022
✅ nextjs-turbopack-stable7205
✅ nextjs-webpack-canary55022
✅ nextjs-webpack-stable7205
✅ nitro-stable66011
✅ nuxt-stable66011
✅ sveltekit-stable66011
✅ vite-stable66011
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable66011
✅ express-stable66011
✅ fastify-stable66011
✅ hono-stable66011
✅ nextjs-turbopack-canary55022
✅ nextjs-turbopack-stable7205
✅ nextjs-webpack-canary55022
✅ nextjs-webpack-stable7205
✅ nitro-stable66011
✅ nuxt-stable66011
✅ sveltekit-stable66011
✅ vite-stable66011
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack7205
❌ 🌍 Community Worlds
AppPassedFailedSkipped
✅ mongodb-dev302
❌ mongodb5235
✅ redis-dev302
❌ redis5325
✅ turso-dev302
❌ turso4515
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable66011
✅ e2e-local-postgres-nest-stable66011
✅ e2e-local-prod-nest-stable66011

📋 View full workflow run

@github-actions

github-actionsBot commented Feb 20, 2026

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

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

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro0.037s (-21.3% 🟢)1.005s (~)0.967s101.00x
💻 LocalExpress0.043s (-4.5%)1.005s (~)0.962s101.14x
🐘 PostgresNitro0.047s (-31.9% 🟢)1.012s (~)0.965s101.26x
🐘 PostgresExpress0.048s (-32.2% 🟢)1.012s (-0.5%)0.964s101.29x
💻 LocalNext.js (Turbopack)0.051s1.006s0.956s101.35x
🌐 RedisNext.js (Turbopack)0.054s1.005s0.952s101.44x
🐘 PostgresNext.js (Turbopack)0.062s1.012s0.950s101.65x
workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.097s (-2.8%)2.005s (~)0.908s101.00x
🌐 RedisNext.js (Turbopack)1.120s2.007s0.887s101.02x
🐘 PostgresNitro1.124s (-1.6%)2.012s (~)0.888s101.02x
💻 LocalNext.js (Turbopack)1.124s2.006s0.883s101.02x
🐘 PostgresExpress1.126s (-1.1%)2.012s (~)0.886s101.03x
💻 LocalExpress1.131s (~)2.005s (~)0.874s101.03x
🐘 PostgresNext.js (Turbopack)1.143s2.013s0.869s101.04x
workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro10.635s (-2.4%)11.023s (~)0.388s31.00x
🌐 RedisNext.js (Turbopack)10.680s11.025s0.345s31.00x
🐘 PostgresNitro10.689s (-2.4%)11.036s (~)0.348s31.01x
🐘 PostgresExpress10.705s (-1.8%)11.038s (~)0.333s31.01x
💻 LocalNext.js (Turbopack)10.847s11.025s0.178s31.02x
💻 LocalExpress10.934s (~)11.023s (~)0.089s31.03x
🐘 PostgresNext.js (Turbopack)10.996s11.382s0.386s31.03x
workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)14.091s15.033s0.941s41.00x
🐘 PostgresExpress14.123s (-3.1%)15.042s (~)0.919s41.00x
🐘 PostgresNitro14.129s (-4.7%)15.039s (~)0.910s41.00x
💻 LocalNitro14.183s (-4.9%)15.029s (~)0.847s41.01x
🐘 PostgresNext.js (Turbopack)14.580s15.049s0.469s41.03x
💻 LocalNext.js (Turbopack)14.703s15.031s0.328s41.04x
💻 LocalExpress14.911s (-0.6%)15.029s (-3.2%)0.118s41.06x
workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)13.030s13.457s0.427s71.00x
🐘 PostgresExpress13.096s (-6.5% 🟢)14.038s (-3.0%)0.942s71.01x
🐘 PostgresNitro13.119s (-8.7% 🟢)14.038s (-6.7% 🟢)0.919s71.01x
🐘 PostgresNext.js (Turbopack)14.145s14.759s0.613s71.09x
💻 LocalNitro14.789s (-10.1% 🟢)15.026s (-11.8% 🟢)0.237s61.14x
💻 LocalNext.js (Turbopack)16.157s17.033s0.875s61.24x
💻 LocalExpress16.514s (-1.0%)17.029s (~)0.516s61.27x
Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.228s (-3.0%)2.011s (~)0.783s151.00x
🐘 PostgresNitro1.231s (-4.8%)2.011s (~)0.780s151.00x
🌐 RedisNext.js (Turbopack)1.266s2.007s0.741s151.03x
🐘 PostgresNext.js (Turbopack)1.286s2.011s0.725s151.05x
💻 LocalNitro1.472s (-0.8%)2.005s (~)0.534s151.20x
💻 LocalExpress1.503s (~)2.006s (~)0.503s151.22x
💻 LocalNext.js (Turbopack)1.524s2.006s0.482s151.24x
Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express2.341s (-4.9%)3.012s (~)0.671s101.00x
🐘 PostgresNitro2.353s (-4.8%)3.013s (~)0.660s101.01x
🌐 RedisNext.js (Turbopack)2.478s3.009s0.531s101.06x
🐘 PostgresNext.js (Turbopack)2.484s3.015s0.531s101.06x
💻 LocalNitro2.551s (-11.2% 🟢)3.007s (-3.2%)0.456s101.09x
💻 LocalExpress2.970s (-0.9%)3.308s (-7.2% 🟢)0.338s101.27x
💻 LocalNext.js (Turbopack)2.995s3.309s0.314s101.28x
Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express3.519s (-1.9%)4.014s (~)0.495s81.00x
🐘 PostgresNitro3.523s (-3.0%)4.015s (~)0.492s81.00x
🐘 PostgresNext.js (Turbopack)3.844s4.017s0.173s81.09x
🌐 RedisNext.js (Turbopack)4.117s4.727s0.610s71.17x
💻 LocalNitro7.025s (-13.5% 🟢)7.514s (-14.3% 🟢)0.489s42.00x
💻 LocalExpress8.144s (-3.9%)8.769s (-2.8%)0.625s42.31x
💻 LocalNext.js (Turbopack)8.563s9.019s0.456s42.43x
Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.207s (-4.1%)2.011s (~)0.803s151.00x
🐘 PostgresNitro1.216s (-4.7%)2.010s (~)0.794s151.01x
🌐 RedisNext.js (Turbopack)1.252s2.007s0.755s151.04x
🐘 PostgresNext.js (Turbopack)1.254s2.011s0.757s151.04x
💻 LocalNitro1.484s (-4.9%)2.006s (~)0.522s151.23x
💻 LocalNext.js (Turbopack)1.515s2.005s0.490s151.25x
💻 LocalExpress1.550s (+0.9%)2.005s (~)0.455s151.28x
Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro2.342s (-4.7%)3.012s (~)0.670s101.00x
🐘 PostgresExpress2.353s (-4.0%)3.012s (~)0.659s101.00x
🐘 PostgresNext.js (Turbopack)2.474s3.013s0.539s101.06x
🌐 RedisNext.js (Turbopack)2.498s3.009s0.511s101.07x
💻 LocalNitro2.924s (-3.4%)3.208s (-12.7% 🟢)0.283s101.25x
💻 LocalNext.js (Turbopack)3.012s3.675s0.663s91.29x
💻 LocalExpress3.090s (+1.9%)3.885s (+3.4%)0.795s81.32x
Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express3.515s (-2.3%)4.013s (~)0.497s81.00x
🐘 PostgresNitro3.518s (-2.7%)4.014s (~)0.496s81.00x
🐘 PostgresNext.js (Turbopack)3.841s4.018s0.177s81.09x
🌐 RedisNext.js (Turbopack)4.031s4.583s0.553s71.15x
💻 LocalNitro7.511s (-13.1% 🟢)8.016s (-11.2% 🟢)0.506s42.14x
💻 LocalExpress8.608s (-5.0%)9.020s (-7.7% 🟢)0.412s42.45x
💻 LocalNext.js (Turbopack)9.214s9.772s0.557s42.62x
workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)0.601s1.005s0.404s601.00x
🐘 PostgresExpress0.642s (-25.8% 🟢)1.009s (-1.7%)0.366s601.07x
🐘 PostgresNitro0.657s (-26.8% 🟢)1.008s (-3.4%)0.351s601.09x
💻 LocalNitro0.672s (-29.7% 🟢)1.004s (-1.7%)0.332s601.12x
🐘 PostgresNext.js (Turbopack)0.870s1.009s0.140s601.45x
💻 LocalNext.js (Turbopack)0.878s1.057s0.179s571.46x
💻 LocalExpress0.973s (-1.0%)1.114s (+1.7%)0.140s551.62x
workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)1.460s2.006s0.546s451.00x
🐘 PostgresExpress1.553s (-26.6% 🟢)2.010s (-33.3% 🟢)0.457s451.06x
🐘 PostgresNitro1.563s (-27.7% 🟢)2.032s (-31.8% 🟢)0.470s451.07x
🐘 PostgresNext.js (Turbopack)2.127s2.948s0.821s311.46x
💻 LocalNitro2.193s (-25.8% 🟢)3.007s (-3.3%)0.815s301.50x
💻 LocalNext.js (Turbopack)2.736s3.008s0.272s301.87x
💻 LocalExpress2.966s (-1.6%)3.222s (-9.2% 🟢)0.255s282.03x
workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)3.077s3.437s0.360s351.00x
🐘 PostgresNitro3.153s (-27.3% 🟢)4.012s (-20.0% 🟢)0.859s301.02x
🐘 PostgresExpress3.188s (-25.2% 🟢)4.012s (-19.3% 🟢)0.824s301.04x
🐘 PostgresNext.js (Turbopack)4.345s5.015s0.670s241.41x
💻 LocalNitro7.218s (-20.1% 🟢)7.827s (-17.4% 🟢)0.610s162.35x
💻 LocalNext.js (Turbopack)8.797s9.232s0.434s142.86x
💻 LocalExpress8.995s (-1.7%)9.479s (-3.1%)0.484s132.92x
workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.243s (-19.7% 🟢)1.008s (~)0.766s601.00x
🐘 PostgresNitro0.245s (-16.9% 🟢)1.009s (~)0.764s601.01x
🌐 RedisNext.js (Turbopack)0.260s1.005s0.744s601.07x
🐘 PostgresNext.js (Turbopack)0.287s1.010s0.723s601.18x
💻 LocalNitro0.576s (-3.7%)1.004s (~)0.428s602.37x
💻 LocalExpress0.581s (-3.7%)1.004s (~)0.424s602.39x
💻 LocalNext.js (Turbopack)0.585s1.004s0.420s602.41x
workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.419s (-20.5% 🟢)1.009s (~)0.589s901.00x
🐘 PostgresNitro0.424s (-21.4% 🟢)1.009s (~)0.585s901.01x
🐘 PostgresNext.js (Turbopack)0.590s1.033s0.443s881.41x
🌐 RedisNext.js (Turbopack)1.194s2.006s0.812s452.85x
💻 LocalNitro2.345s (-5.2% 🟢)3.007s (~)0.662s305.59x
💻 LocalExpress2.498s (-3.2%)3.009s (~)0.510s305.96x
💻 LocalNext.js (Turbopack)2.664s3.009s0.345s306.36x
workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.606s (-35.5% 🟢)1.009s (-15.4% 🟢)0.403s1191.00x
🐘 PostgresExpress0.620s (-30.9% 🟢)1.009s (-3.1%)0.389s1191.02x
🐘 PostgresNext.js (Turbopack)1.001s1.477s0.476s821.65x
🌐 RedisNext.js (Turbopack)2.883s3.085s0.202s394.76x
💻 LocalNitro10.189s (-8.3% 🟢)10.855s (-6.9% 🟢)0.666s1216.83x
💻 LocalExpress11.127s (-1.4%)11.845s (-1.5%)0.718s1118.38x
💻 LocalNext.js (Turbopack)11.287s11.753s0.465s1118.64x
Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro0.140s (-28.6% 🟢)1.003s (~)0.009s (-26.3% 🟢)1.014s (~)0.874s101.00x
🐘 PostgresExpress0.164s (-24.1% 🟢)0.999s (+0.5%)0.001s (-8.3% 🟢)1.013s (~)0.848s101.17x
🐘 PostgresNitro0.167s (-30.3% 🟢)0.999s (+0.7%)0.001s (-21.4% 🟢)1.013s (~)0.846s101.19x
🌐 RedisNext.js (Turbopack)0.169s1.000s0.001s1.008s0.838s101.21x
💻 LocalNext.js (Turbopack)0.178s1.001s0.012s1.018s0.840s101.27x
💻 LocalExpress0.199s (-2.4%)1.003s (~)0.012s (-2.5%)1.017s (~)0.818s101.42x
🐘 PostgresNext.js (Turbopack)0.224s1.001s0.001s1.013s0.789s101.60x
stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)0.447s0.999s0.003s1.012s0.565s601.00x
🐘 PostgresNitro0.563s (-22.1% 🟢)1.005s (~)0.003s (-13.6% 🟢)1.023s (~)0.460s591.26x
💻 LocalNitro0.569s (-21.7% 🟢)1.008s (~)0.009s (-6.2% 🟢)1.021s (~)0.452s591.27x
🐘 PostgresExpress0.610s (-12.0% 🟢)1.006s (~)0.005s (+20.4% 🔺)1.028s (~)0.417s591.37x
💻 LocalNext.js (Turbopack)0.672s1.008s0.010s1.024s0.352s591.50x
💻 LocalExpress0.723s (-6.9% 🟢)1.009s (-1.7%)0.009s (-12.1% 🟢)1.022s (-1.8%)0.300s591.62x
🐘 PostgresNext.js (Turbopack)0.760s1.026s0.008s1.050s0.290s591.70x
10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)0.916s1.016s0.000s1.022s0.106s591.00x
🐘 PostgresNitro0.988s (+7.3% 🔺)1.384s (+30.2% 🔺)0.000s (-36.4% 🟢)1.398s (+29.3% 🔺)0.410s441.08x
🐘 PostgresExpress1.066s (+19.3% 🔺)1.664s (+53.7% 🔺)0.000s (+205.6% 🔺)1.677s (+51.8% 🔺)0.612s361.16x
💻 LocalNitro1.140s (-6.9% 🟢)2.017s (~)0.000s (-38.5% 🟢)2.020s (~)0.880s301.24x
🐘 PostgresNext.js (Turbopack)1.175s1.909s0.000s1.930s0.755s321.28x
💻 LocalExpress1.235s (-1.8%)2.020s (~)0.000s (-35.3% 🟢)2.023s (~)0.788s301.35x
💻 LocalNext.js (Turbopack)1.295s2.019s0.000s2.024s0.729s301.41x
fan-out fan-in 10 streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.886s (~)2.223s (~)0.000s (-100.0% 🟢)2.233s (~)0.347s271.00x
🌐 RedisNext.js (Turbopack)1.963s2.180s0.000s2.186s0.223s281.04x
🐘 PostgresExpress2.034s (+12.7% 🔺)2.441s (+16.3% 🔺)0.000s (+Infinity% 🔺)2.453s (+15.6% 🔺)0.418s251.08x
🐘 PostgresNext.js (Turbopack)2.302s3.005s0.000s3.015s0.713s201.22x
💻 LocalNitro3.350s (+0.6%)3.967s (~)0.000s (-60.0% 🟢)3.971s (~)0.621s161.78x
💻 LocalExpress3.508s (+1.9%)4.030s (-1.6%)0.001s (+37.5% 🔺)4.035s (-1.6%)0.527s151.86x
💻 LocalNext.js (Turbopack)3.784s4.387s0.000s4.392s0.608s142.01x

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNitro21/21
🐘 PostgresExpress12/21
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express🐘 Postgres20/21
Next.js (Turbopack)🌐 Redis15/21
Nitro🐘 Postgres17/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)

📋 View full workflow run


Some benchmark jobs failed:

  • Local: success
  • Postgres: success
  • Vercel: failure

Check the workflow run for details.

@TooTallNateGraphite App

Copy link
Copy Markdown
MemberAuthor

This stack of pull requests is managed by Graphite. Learn more about stacking.

@TooTallNate
TooTallNate marked this pull request as ready for review February 20, 2026 22:47
CopilotAI review requested due to automatic review settings February 20, 2026 22:47

CopilotAI 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.

Pull request overview

This PR extends the @workflow/swc-plugin transform to detect custom class serialization methods when the WORKFLOW_SERIALIZE / WORKFLOW_DESERIALIZE symbols are accessed via CommonJS require() patterns, aligning behavior with codebases that have been transpiled from ESM to CJS.

Changes:

  • Track CommonJS require() bindings during identifier collection (namespace const x = require(...) and destructured const { ... } = require(...)) and recognize x.WORKFLOW_SERIALIZE / x.WORKFLOW_DESERIALIZE as serialization symbols.
  • Add fixture coverage for both namespace-require and destructured-require inputs across workflow/step/client outputs.
  • Update the plugin spec and add a changeset entry documenting the new behavior.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 2 comments.

Show a summary per file
FileDescription
packages/swc-plugin-workflow/transform/src/lib.rsAdds require-binding tracking and member-expression symbol recognition for serialization detection.
packages/swc-plugin-workflow/transform/tests/fixture/custom-serialization-require-namespace/input.jsNew fixture input for namespace require() pattern.
packages/swc-plugin-workflow/transform/tests/fixture/custom-serialization-require-namespace/output-workflow.jsExpected workflow-mode output for namespace require() fixture.
packages/swc-plugin-workflow/transform/tests/fixture/custom-serialization-require-namespace/output-step.jsExpected step-mode output for namespace require() fixture.
packages/swc-plugin-workflow/transform/tests/fixture/custom-serialization-require-namespace/output-client.jsExpected client-mode output for namespace require() fixture.
packages/swc-plugin-workflow/transform/tests/fixture/custom-serialization-require-destructured/input.jsNew fixture input for destructured require() pattern.
packages/swc-plugin-workflow/transform/tests/fixture/custom-serialization-require-destructured/output-workflow.jsExpected workflow-mode output for destructured require() fixture.
packages/swc-plugin-workflow/transform/tests/fixture/custom-serialization-require-destructured/output-step.jsExpected step-mode output for destructured require() fixture.
packages/swc-plugin-workflow/transform/tests/fixture/custom-serialization-require-destructured/output-client.jsExpected client-mode output for destructured require() fixture.
packages/swc-plugin-workflow/spec.mdDocuments supported CommonJS require() symbol patterns.
.changeset/social-forks-tease.mdDeclares a patch release for the new detection behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment threadpackages/swc-plugin-workflow/transform/src/lib.rs Outdated
Comment threadpackages/swc-plugin-workflow/transform/src/lib.rs Outdated
…WC plugin
The SWC compiler plugin now detects classes with custom serialization methods (`WORKFLOW_SERIALIZE` / `WORKFLOW_DESERIALIZE`) when symbols are obtained via CommonJS `require()` calls, in addition to the existing ESM import and `Symbol.for()` patterns.
This handles code that has been pre-compiled from ESM to CommonJS by tools like TypeScript (tsc), esbuild, or tsup, which transform:
```ts
import { WORKFLOW_SERIALIZE } from "@workflow/serde"
```
into either:
```ts
const serde_1 = require("@workflow/serde") // namespace require
const { WORKFLOW_SERIALIZE } = require(...) // destructured require
```
Both patterns are now recognized during the identifier collection phase, and classes using them are properly registered with registerSerializationClass().
@TooTallNateTooTallNate changed the title Add CommonJS require() support for class serialization detection in SWC pluginAdd CommonJS require() support for class serialization detection in SWC pluginMar 23, 2026
…on_bindings helper
- Tighten is_require_call to require exactly one string literal argument,
rejecting dynamic requires and multi-arg calls.
- Extract track_serialization_bindings helper to deduplicate the require
tracking logic between Decl::Var and ExportDecl(Decl::Var) branches.
@TooTallNate
TooTallNateforce-pushed the 02-20-add_commonjs_require_support_for_class_serialization_detection_in_swc_plugin branch from 3648e46 to f461c59CompareMarch 23, 2026 14:01
@TooTallNate
TooTallNate requested a review from a team as a code ownerMarch 23, 2026 14:01
@TooTallNate
TooTallNate merged commit 992d768 into mainMar 23, 2026
98 of 103 checks passed
@TooTallNate
TooTallNate deleted the 02-20-add_commonjs_require_support_for_class_serialization_detection_in_swc_plugin branch March 23, 2026 18:30
@ghostghost mentioned this pull request Mar 23, 2026

@pranaygppranaygp 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.

Post-merge review — overall this is a clean, well-tested addition. A few observations below.

// Track CommonJS namespace require: const serde_1 = require("...")
if self.is_require_call(init) {
self.require_namespace_identifiers
.insert(ident.id.sym.to_string());

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.

Nit: This tracks everyrequire() call as a namespace identifier, regardless of the module being required. So const fs = require("fs") would cause fs.WORKFLOW_SERIALIZE to be treated as a serialization symbol if it ever appeared. This is unlikely to cause real bugs since nobody would name a property WORKFLOW_SERIALIZE on an unrelated module, but it is semantically imprecise.

Consider either:

  1. Restricting to known serde modules (e.g. checking the string arg contains @workflow/serde or similar), or
  2. Documenting this as an intentional trade-off (broad tracking is simpler and the false-positive risk is negligible).

match prop {
ObjectPatProp::Assign(assign) => {
// const { WORKFLOW_SERIALIZE } = require("...")
let name = assign.key.sym.to_string();

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.

Nit: The ObjectPatProp::Assign arm handles const { WORKFLOW_SERIALIZE } = require(...), but in the SWC AST this node actually represents const { WORKFLOW_SERIALIZE = defaultValue } = require(...) (a destructured binding with a default value). A plain shorthand const { WORKFLOW_SERIALIZE } = require(...) is typically parsed as ObjectPatProp::KeyValue where key and value ident are the same.

In practice this still works because SWC may represent the no-default shorthand either way depending on version, and the logic is correct for both paths. But worth noting that the Assign arm would also match const { WORKFLOW_SERIALIZE = someDefault } = require(...), which is an unusual but technically valid pattern.

@@ -0,0 +1,25 @@
// Test custom serialization with CommonJS destructured require
const { WORKFLOW_SERIALIZE, WORKFLOW_DESERIALIZE } = require("@workflow/serde");

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.

The spec documents renamed destructured require (const { WORKFLOW_SERIALIZE: WS } = require(...)) but there's no test fixture covering this case. Adding a custom-serialization-require-destructured-renamed fixture would ensure the ObjectPatProp::KeyValue branch stays correct.

pranaygp added a commit that referenced this pull request Mar 23, 2026
* origin/main:
[ai] Add experimental_context to DurableAgentOptions (#1489)
[ai] Expose configured tools on DurableAgent instances (#1488)
fix(builders): catch node builtin usage when entry fields diverge (#1455)
[web-shared] Fix timeline duration format and precision (#1482)
[cli] Add bulk cancel, --status filter, fix step JSON hydration (#1467)
[utils] Re-export parseName utilities and add workflow/observability module (#1453)
[o11y] Polish display when run data has expired (#1438)
Add CommonJS `require()` support for class serialization detection in SWC plugin (#1144)
fix(next): stabilize deferred canary e2e in nextjs workbenches (#1468)
[web] Support legacy newline-delimited stream format in `useStreamReader` (#1473)
Revert "Add support for calling start() inside workflow functions (#1133)" (#1475)
pranaygp added a commit that referenced this pull request Mar 24, 2026
…naygp-db9e68c1
* 'main' of https://github.com/vercel/workflow: (32 commits)
chore: bump @nestjs/* to ^11.1.17 (#1497)
chore: bump hono to ^4.12.8 (#1495)
Revert "Inline class serialization registration to fix 3rd-party package supp…" (#1493)
[world] Add stream pagination and metadata endpoints (#1470)
[cli] [world-local] Ensure update checks don't suggest upgrading from stable release to pre-releases (#1490)
Remove NestJS Vercel integration while in experimental phase (#1485)
feat: export semantic error types and add API reference docs (#1447)
feat: enforce max queue deliveries in handlers with graceful failure (#1344)
[world-postgres] Migrate client from `postgres.js` to `pg` (#1484)
Inline class serialization registration to fix 3rd-party package support (#1480)
[ai] Add experimental_context to DurableAgentOptions (#1489)
[ai] Expose configured tools on DurableAgent instances (#1488)
fix(builders): catch node builtin usage when entry fields diverge (#1455)
[web-shared] Fix timeline duration format and precision (#1482)
[cli] Add bulk cancel, --status filter, fix step JSON hydration (#1467)
[utils] Re-export parseName utilities and add workflow/observability module (#1453)
[o11y] Polish display when run data has expired (#1438)
Add CommonJS `require()` support for class serialization detection in SWC plugin (#1144)
fix(next): stabilize deferred canary e2e in nextjs workbenches (#1468)
[web] Support legacy newline-delimited stream format in `useStreamReader` (#1473)
...
TooTallNate added a commit that referenced this pull request Mar 24, 2026
The original PR #1480 was merged but reverted because it didn't include
updated fixtures for the CJS require patterns added by PR #1144
(custom-serialization-require-destructured and
custom-serialization-require-namespace). These fixtures still had the
old 'import { registerSerializationClass }' pattern instead of the
new inline IIFE.
TooTallNate added a commit that referenced this pull request Mar 24, 2026
…ort (v2) (#1503)
* Inline class serialization registration to fix 3rd-party package support (#1480)
* Inline class serialization registration to fix 3rd-party package support
The SWC plugin previously generated:
import { registerSerializationClass } from "workflow/internal/class-serialization";
registerSerializationClass("class//...", ClassName);
This broke for 3rd-party packages (e.g. @vercel/sandbox) that define
serializable classes but don't depend on the 'workflow' package. The
bare 'workflow' specifier is unresolvable from within node_modules of
a package that doesn't list it as a dependency.
Now the plugin generates a self-contained IIFE that uses
Symbol.for('workflow-class-registry') on globalThis directly, with
zero module dependencies:
(function(__wf_cls, __wf_id) {
var __wf_sym = Symbol.for("workflow-class-registry"),
__wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map());
__wf_reg.set(__wf_id, __wf_cls);
Object.defineProperty(__wf_cls, "classId", { ... });
})(ClassName, "class//...");
This is fully compatible with the existing deserialization side in
@workflow/core which reads from the same globalThis registry.
* Address review feedback: fix comment and update docstring
- Fix inaccurate IIFE comment in lib.rs: the second arg is the
generated class ID string, not the literal "classId"
- Update registerSerializationClass docstring to reflect that the
SWC plugin now inlines equivalent logic rather than importing it
* Update CJS require fixture outputs for inline class serialization
The original PR #1480 was merged but reverted because it didn't include
updated fixtures for the CJS require patterns added by PR #1144
(custom-serialization-require-destructured and
custom-serialization-require-namespace). These fixtures still had the
old 'import { registerSerializationClass }' pattern instead of the
new inline IIFE.
pranaygp pushed a commit to vercel/sandbox that referenced this pull request Mar 27, 2026
…ibility (#109)
## Summary
Makes `@vercel/sandbox` fully compatible with the Workflow DevKit
compiler so that `Sandbox`, `Command`, and `CommandFinished` instances
can be used directly inside `"use workflow"` functions — no wrapper step
functions needed.
Supersedes #58.
## Problem
When `@vercel/sandbox` is imported in a workflow context, the workflow
builder tries to bundle it into the workflow VM bundle (because it has
`WORKFLOW_SERIALIZE`/`WORKFLOW_DESERIALIZE` on its classes). This fails
because:
1. The SDK's public methods use Node.js APIs (`fs`, `stream`, `zlib`,
`undici`, etc.) which are forbidden in the workflow VM
2. The compiled `dist/index.js` was a single bundled file that hoisted
all Node.js imports to the top, making them impossible to tree-shake
3. The `Sandbox` and `Command` classes had a sync `client` getter that
directly referenced `APIClient`, pulling the entire HTTP client stack
into the module scope
## Solution
### 1. `"use step"` annotations on all public async methods
Added `"use step"` to all 22 public async methods across `Sandbox` (14),
`Command` (5), and `Snapshot` (3). The SWC plugin strips these method
bodies in workflow mode, replacing them with durable step proxies. This
eliminates all Node.js API references from the workflow bundle.
### 2. Async `ensureClient()` replaces sync `client` getter
The sync `get client()` getter directly referenced `APIClient`, which
pulls in `undici`, `zlib`, `tar-stream`, `jsonlines`, etc. Replaced
with:
```typescript
private async ensureClient(): Promise<APIClient> {
"use step";
if (this._client) return this._client;
const credentials = getSandboxCredentials();
this._client = new APIClient({ ... });
return this._client;
}
```
Since `ensureClient()` is itself `"use step"`, its body (including `new
APIClient(...)`) gets stripped in workflow mode. All instance methods
now call `const client = await this.ensureClient();` instead of
`this.client`.
### 3. `bundle: false` in tsdown config
Changed from single-file bundling to per-file output. This keeps Node.js
imports local to the files that use them, so after the SWC plugin strips
step method bodies, the now-unused Node.js imports can be eliminated by
esbuild's tree-shaking.
### 4. Workflow-code-runner example updated
- Removed `serverExternalPackages: ["@vercel/sandbox"]` from
`next.config.ts` (no longer needed)
- Updated `workflow` dependency to use a tarball that includes the
inline class serialization registration fix (vercel/workflow#1480)
## Testing
- `pnpm build` succeeds for the full monorepo (all 8 tasks)
- The `workflow-code-runner` example app builds successfully with all
workflow routes generated
- 22 `"use step"` directives survive compilation in both ESM and CJS
dist output
## Related
- vercel/workflow#1480 — Inline class serialization registration (fixes
SWC import resolution for 3rd-party packages)
- vercel/workflow#1481 — Build-time warning when
`serverExternalPackages` hides workflow-enabled packages
- vercel/workflow#1144 — CJS detection for serde symbols (pending
review)
- #58 — Previous attempt (superseded by this PR)
---------
Signed-off-by: Peter Wielander <mittgfu@gmail.com>
Co-authored-by: Peter Wielander <mittgfu@gmail.com>
Sign up for freeto 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.

4 participants

@TooTallNate@pranaygp@VaguelySerious
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' Add CommonJS `require()` support for class serialization detection in SWC plugin by TooTallNate · Pull Request #1144 · vercel/workflow · GitHub
Skip to content

Add CommonJS require() support for class serialization detection in SWC plugin - #1144

Merged
TooTallNate merged 3 commits into
mainfrom
02-20-add_commonjs_require_support_for_class_serialization_detection_in_swc_plugin
Mar 23, 2026
Merged

Add CommonJS require() support for class serialization detection in SWC plugin#1144
TooTallNate merged 3 commits into
mainfrom
02-20-add_commonjs_require_support_for_class_serialization_detection_in_swc_plugin

Conversation

@TooTallNate

Copy link
Copy Markdown
Member

The SWC compiler plugin now detects classes with custom serialization methods (WORKFLOW_SERIALIZE / WORKFLOW_DESERIALIZE) when symbols are obtained via CommonJS require() calls, in addition to the existing ESM import and Symbol.for() patterns.

This handles code that has been pre-compiled from ESM to CommonJS by tools like TypeScript (tsc), esbuild, or tsup, which transform:

import{WORKFLOW_SERIALIZE}from"@workflow/serde"

into either:

constserde_1=require("@workflow/serde")// namespace requireconst{WORKFLOW_SERIALIZE}=require(...)// destructured require

Both patterns are now recognized during the identifier collection phase, and classes using them are properly registered with registerSerializationClass().

@changeset-bot

changeset-botBot commented Feb 20, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 14ea197

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

This PR includes changesets to release 17 packages
NameType
@workflow/swc-pluginPatch
@workflow/astroPatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nestPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
workflowPatch
@workflow/vitePatch
@workflow/vitestPatch
@workflow/world-testingPatch
@workflow/nuxtPatch
@workflow/aiPatch
@workflow/corePatch
@workflow/web-sharedPatch

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

@vercel

vercelBot commented Feb 20, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Feb 20, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production780067847
✅ 💻 Local Development7820142924
✅ 📦 Local Production7820142924
✅ 🐘 Local Postgres7820142924
✅ 🪟 Windows720577
❌ 🌍 Community Worlds1185621195
✅ 📋 Other198033231
Total3514565524122

❌ Failed Tests

🌍 Community Worlds (56 failed)

mongodb (3 failed):

  • hookWorkflow is not resumable via public webhook endpoint | wrun_01KMDYE4RWSYQ6538DVH70M5P0
  • webhookWorkflow | wrun_01KMDYED4N9B1F4PF1X6F86J26
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously | wrun_01KMDYMVS1CFTACB4081WS8K9V

redis (2 failed):

  • hookWorkflow is not resumable via public webhook endpoint | wrun_01KMDYE4RWSYQ6538DVH70M5P0
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously | wrun_01KMDYMVS1CFTACB4081WS8K9V

turso (51 failed):

  • addTenWorkflow | wrun_01KMDYCYXATZBJYFAPPKVRZKDZ
  • addTenWorkflow | wrun_01KMDYCYXATZBJYFAPPKVRZKDZ
  • wellKnownAgentWorkflow (.well-known/agent) | wrun_01KMDYEKVFEVFMCXC32DMPJ3KB
  • should work with react rendering in step
  • promiseAllWorkflow | wrun_01KMDYD5F67YMHR6KTJF5YFYM3
  • promiseRaceWorkflow | wrun_01KMDYDBXWV7065WDZ655A6X2Y
  • promiseAnyWorkflow | wrun_01KMDYDEFBS29N1Y4E04XS0PHS
  • importedStepOnlyWorkflow | wrun_01KMDYF31ZMHDYKRR9870BQJFE
  • hookWorkflow | wrun_01KMDYDTDYK5ZZTVGBV7PBPJAZ
  • hookWorkflow is not resumable via public webhook endpoint | wrun_01KMDYE4RWSYQ6538DVH70M5P0
  • webhookWorkflow | wrun_01KMDYED4N9B1F4PF1X6F86J26
  • sleepingWorkflow | wrun_01KMDYEKNPTWHR8GD4VJXRHSWR
  • parallelSleepWorkflow | wrun_01KMDYF1MMFSRK8683TXYGPEFF
  • nullByteWorkflow | wrun_01KMDYF6AS6SP633VT5B7JZ0Q8
  • workflowAndStepMetadataWorkflow | wrun_01KMDYF8B1WBHNCPZKHFYCH5G0
  • fetchWorkflow | wrun_01KMDYH6MPPT3CPGBXX02XNQTP
  • promiseRaceStressTestWorkflow | wrun_01KMDYH9Y93SDT6D7TEE6CMKHT
  • error handling error propagation workflow errors nested function calls preserve message and stack trace
  • error handling error propagation workflow errors cross-file imports preserve message and stack trace
  • error handling error propagation step errors basic step error preserves message and stack trace
  • error handling error propagation step errors cross-file step error preserves message and function names in stack
  • error handling retry behavior regular Error retries until success
  • error handling retry behavior FatalError fails immediately without retries
  • error handling retry behavior RetryableError respects custom retryAfter delay
  • error handling retry behavior maxRetries=0 disables retries
  • error handling catchability FatalError can be caught and detected with FatalError.is()
  • hookCleanupTestWorkflow - hook token reuse after workflow completion | wrun_01KMDYM7FJ0MWDXPS56VM7PF4D
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously | wrun_01KMDYMVS1CFTACB4081WS8K9V
  • hookDisposeTestWorkflow - hook token reuse after explicit disposal while workflow still running | wrun_01KMDYNGM0E0ZXFK8E2CQ9CYZV
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars) | wrun_01KMDYP5F3H7NXZ9W494MXF8EQ
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument | wrun_01KMDYPFQ0MK8D4B36398E2S6A
  • closureVariableWorkflow - nested step functions with closure variables | wrun_01KMDYPN9T3F9HBR5W1TQSBAVH
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step | wrun_01KMDYPQBVNH7GG5Y8ZDS8B1P3
  • health check (queue-based) - workflow and step endpoints respond to health check messages
  • pathsAliasWorkflow - TypeScript path aliases resolve correctly | wrun_01KMDYQ7ZKSHZ1DF03RZ64NGXD
  • Calculator.calculate - static workflow method using static step methods from another class | wrun_01KMDYQEMCVB21XR91WVHJEGK4
  • AllInOneService.processNumber - static workflow method using sibling static step methods | wrun_01KMDYQP3ST1XWR4FTRNPGNYE4
  • ChainableService.processWithThis - static step methods using this to reference the class | wrun_01KMDYQWMSK6ZYMFNY7ME546A9
  • thisSerializationWorkflow - step function invoked with .call() and .apply() | wrun_01KMDYR36AW71R74TKNQEA26QC
  • customSerializationWorkflow - custom class serialization with WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE | wrun_01KMDYR9J9RVJCAWSQG76N5656
  • instanceMethodStepWorkflow - instance methods with "use step" directive | wrun_01KMDYRH7WBXH2XGDZPC5CXB49
  • crossContextSerdeWorkflow - classes defined in step code are deserializable in workflow context | wrun_01KMDYRVMSG67Q29E0JT62SCPB
  • stepFunctionAsStartArgWorkflow - step function reference passed as start() argument | wrun_01KMDYS38DF4PJE3SJHH7YV0GQ
  • cancelRun - cancelling a running workflow | wrun_01KMDYSB4MVW6TNAVS70ZDC36V
  • cancelRun via CLI - cancelling a running workflow | wrun_01KMDYSM88CM0VKBMVABG1ZRM0
  • pages router addTenWorkflow via pages router
  • pages router promiseAllWorkflow via pages router
  • pages router sleepingWorkflow via pages router
  • hookWithSleepWorkflow - hook payloads delivered correctly with concurrent sleep | wrun_01KMDYT02V9K533MN8PA31072V
  • sleepInLoopWorkflow - sleep inside loop with steps actually delays each iteration | wrun_01KMDYTKS4H2ZFNHJC3AH7D70B
  • sleepWithSequentialStepsWorkflow - sequential steps work with concurrent sleep (control) | wrun_01KMDYTZMDM1Q8370F5R6NNHJV

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro7007
✅ example7007
✅ express7007
✅ fastify7007
✅ hono7007
✅ nextjs-turbopack7502
✅ nextjs-webpack7502
✅ nitro7007
✅ nuxt7007
✅ sveltekit7007
✅ vite7007
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable66011
✅ express-stable66011
✅ fastify-stable66011
✅ hono-stable66011
✅ nextjs-turbopack-canary55022
✅ nextjs-turbopack-stable7205
✅ nextjs-webpack-canary55022
✅ nextjs-webpack-stable7205
✅ nitro-stable66011
✅ nuxt-stable66011
✅ sveltekit-stable66011
✅ vite-stable66011
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable66011
✅ express-stable66011
✅ fastify-stable66011
✅ hono-stable66011
✅ nextjs-turbopack-canary55022
✅ nextjs-turbopack-stable7205
✅ nextjs-webpack-canary55022
✅ nextjs-webpack-stable7205
✅ nitro-stable66011
✅ nuxt-stable66011
✅ sveltekit-stable66011
✅ vite-stable66011
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable66011
✅ express-stable66011
✅ fastify-stable66011
✅ hono-stable66011
✅ nextjs-turbopack-canary55022
✅ nextjs-turbopack-stable7205
✅ nextjs-webpack-canary55022
✅ nextjs-webpack-stable7205
✅ nitro-stable66011
✅ nuxt-stable66011
✅ sveltekit-stable66011
✅ vite-stable66011
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack7205
❌ 🌍 Community Worlds
AppPassedFailedSkipped
✅ mongodb-dev302
❌ mongodb5235
✅ redis-dev302
❌ redis5325
✅ turso-dev302
❌ turso4515
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable66011
✅ e2e-local-postgres-nest-stable66011
✅ e2e-local-prod-nest-stable66011

📋 View full workflow run

@github-actions

github-actionsBot commented Feb 20, 2026

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

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

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro0.037s (-21.3% 🟢)1.005s (~)0.967s101.00x
💻 LocalExpress0.043s (-4.5%)1.005s (~)0.962s101.14x
🐘 PostgresNitro0.047s (-31.9% 🟢)1.012s (~)0.965s101.26x
🐘 PostgresExpress0.048s (-32.2% 🟢)1.012s (-0.5%)0.964s101.29x
💻 LocalNext.js (Turbopack)0.051s1.006s0.956s101.35x
🌐 RedisNext.js (Turbopack)0.054s1.005s0.952s101.44x
🐘 PostgresNext.js (Turbopack)0.062s1.012s0.950s101.65x
workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.097s (-2.8%)2.005s (~)0.908s101.00x
🌐 RedisNext.js (Turbopack)1.120s2.007s0.887s101.02x
🐘 PostgresNitro1.124s (-1.6%)2.012s (~)0.888s101.02x
💻 LocalNext.js (Turbopack)1.124s2.006s0.883s101.02x
🐘 PostgresExpress1.126s (-1.1%)2.012s (~)0.886s101.03x
💻 LocalExpress1.131s (~)2.005s (~)0.874s101.03x
🐘 PostgresNext.js (Turbopack)1.143s2.013s0.869s101.04x
workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro10.635s (-2.4%)11.023s (~)0.388s31.00x
🌐 RedisNext.js (Turbopack)10.680s11.025s0.345s31.00x
🐘 PostgresNitro10.689s (-2.4%)11.036s (~)0.348s31.01x
🐘 PostgresExpress10.705s (-1.8%)11.038s (~)0.333s31.01x
💻 LocalNext.js (Turbopack)10.847s11.025s0.178s31.02x
💻 LocalExpress10.934s (~)11.023s (~)0.089s31.03x
🐘 PostgresNext.js (Turbopack)10.996s11.382s0.386s31.03x
workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)14.091s15.033s0.941s41.00x
🐘 PostgresExpress14.123s (-3.1%)15.042s (~)0.919s41.00x
🐘 PostgresNitro14.129s (-4.7%)15.039s (~)0.910s41.00x
💻 LocalNitro14.183s (-4.9%)15.029s (~)0.847s41.01x
🐘 PostgresNext.js (Turbopack)14.580s15.049s0.469s41.03x
💻 LocalNext.js (Turbopack)14.703s15.031s0.328s41.04x
💻 LocalExpress14.911s (-0.6%)15.029s (-3.2%)0.118s41.06x
workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)13.030s13.457s0.427s71.00x
🐘 PostgresExpress13.096s (-6.5% 🟢)14.038s (-3.0%)0.942s71.01x
🐘 PostgresNitro13.119s (-8.7% 🟢)14.038s (-6.7% 🟢)0.919s71.01x
🐘 PostgresNext.js (Turbopack)14.145s14.759s0.613s71.09x
💻 LocalNitro14.789s (-10.1% 🟢)15.026s (-11.8% 🟢)0.237s61.14x
💻 LocalNext.js (Turbopack)16.157s17.033s0.875s61.24x
💻 LocalExpress16.514s (-1.0%)17.029s (~)0.516s61.27x
Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.228s (-3.0%)2.011s (~)0.783s151.00x
🐘 PostgresNitro1.231s (-4.8%)2.011s (~)0.780s151.00x
🌐 RedisNext.js (Turbopack)1.266s2.007s0.741s151.03x
🐘 PostgresNext.js (Turbopack)1.286s2.011s0.725s151.05x
💻 LocalNitro1.472s (-0.8%)2.005s (~)0.534s151.20x
💻 LocalExpress1.503s (~)2.006s (~)0.503s151.22x
💻 LocalNext.js (Turbopack)1.524s2.006s0.482s151.24x
Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express2.341s (-4.9%)3.012s (~)0.671s101.00x
🐘 PostgresNitro2.353s (-4.8%)3.013s (~)0.660s101.01x
🌐 RedisNext.js (Turbopack)2.478s3.009s0.531s101.06x
🐘 PostgresNext.js (Turbopack)2.484s3.015s0.531s101.06x
💻 LocalNitro2.551s (-11.2% 🟢)3.007s (-3.2%)0.456s101.09x
💻 LocalExpress2.970s (-0.9%)3.308s (-7.2% 🟢)0.338s101.27x
💻 LocalNext.js (Turbopack)2.995s3.309s0.314s101.28x
Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express3.519s (-1.9%)4.014s (~)0.495s81.00x
🐘 PostgresNitro3.523s (-3.0%)4.015s (~)0.492s81.00x
🐘 PostgresNext.js (Turbopack)3.844s4.017s0.173s81.09x
🌐 RedisNext.js (Turbopack)4.117s4.727s0.610s71.17x
💻 LocalNitro7.025s (-13.5% 🟢)7.514s (-14.3% 🟢)0.489s42.00x
💻 LocalExpress8.144s (-3.9%)8.769s (-2.8%)0.625s42.31x
💻 LocalNext.js (Turbopack)8.563s9.019s0.456s42.43x
Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.207s (-4.1%)2.011s (~)0.803s151.00x
🐘 PostgresNitro1.216s (-4.7%)2.010s (~)0.794s151.01x
🌐 RedisNext.js (Turbopack)1.252s2.007s0.755s151.04x
🐘 PostgresNext.js (Turbopack)1.254s2.011s0.757s151.04x
💻 LocalNitro1.484s (-4.9%)2.006s (~)0.522s151.23x
💻 LocalNext.js (Turbopack)1.515s2.005s0.490s151.25x
💻 LocalExpress1.550s (+0.9%)2.005s (~)0.455s151.28x
Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro2.342s (-4.7%)3.012s (~)0.670s101.00x
🐘 PostgresExpress2.353s (-4.0%)3.012s (~)0.659s101.00x
🐘 PostgresNext.js (Turbopack)2.474s3.013s0.539s101.06x
🌐 RedisNext.js (Turbopack)2.498s3.009s0.511s101.07x
💻 LocalNitro2.924s (-3.4%)3.208s (-12.7% 🟢)0.283s101.25x
💻 LocalNext.js (Turbopack)3.012s3.675s0.663s91.29x
💻 LocalExpress3.090s (+1.9%)3.885s (+3.4%)0.795s81.32x
Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express3.515s (-2.3%)4.013s (~)0.497s81.00x
🐘 PostgresNitro3.518s (-2.7%)4.014s (~)0.496s81.00x
🐘 PostgresNext.js (Turbopack)3.841s4.018s0.177s81.09x
🌐 RedisNext.js (Turbopack)4.031s4.583s0.553s71.15x
💻 LocalNitro7.511s (-13.1% 🟢)8.016s (-11.2% 🟢)0.506s42.14x
💻 LocalExpress8.608s (-5.0%)9.020s (-7.7% 🟢)0.412s42.45x
💻 LocalNext.js (Turbopack)9.214s9.772s0.557s42.62x
workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)0.601s1.005s0.404s601.00x
🐘 PostgresExpress0.642s (-25.8% 🟢)1.009s (-1.7%)0.366s601.07x
🐘 PostgresNitro0.657s (-26.8% 🟢)1.008s (-3.4%)0.351s601.09x
💻 LocalNitro0.672s (-29.7% 🟢)1.004s (-1.7%)0.332s601.12x
🐘 PostgresNext.js (Turbopack)0.870s1.009s0.140s601.45x
💻 LocalNext.js (Turbopack)0.878s1.057s0.179s571.46x
💻 LocalExpress0.973s (-1.0%)1.114s (+1.7%)0.140s551.62x
workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)1.460s2.006s0.546s451.00x
🐘 PostgresExpress1.553s (-26.6% 🟢)2.010s (-33.3% 🟢)0.457s451.06x
🐘 PostgresNitro1.563s (-27.7% 🟢)2.032s (-31.8% 🟢)0.470s451.07x
🐘 PostgresNext.js (Turbopack)2.127s2.948s0.821s311.46x
💻 LocalNitro2.193s (-25.8% 🟢)3.007s (-3.3%)0.815s301.50x
💻 LocalNext.js (Turbopack)2.736s3.008s0.272s301.87x
💻 LocalExpress2.966s (-1.6%)3.222s (-9.2% 🟢)0.255s282.03x
workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)3.077s3.437s0.360s351.00x
🐘 PostgresNitro3.153s (-27.3% 🟢)4.012s (-20.0% 🟢)0.859s301.02x
🐘 PostgresExpress3.188s (-25.2% 🟢)4.012s (-19.3% 🟢)0.824s301.04x
🐘 PostgresNext.js (Turbopack)4.345s5.015s0.670s241.41x
💻 LocalNitro7.218s (-20.1% 🟢)7.827s (-17.4% 🟢)0.610s162.35x
💻 LocalNext.js (Turbopack)8.797s9.232s0.434s142.86x
💻 LocalExpress8.995s (-1.7%)9.479s (-3.1%)0.484s132.92x
workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.243s (-19.7% 🟢)1.008s (~)0.766s601.00x
🐘 PostgresNitro0.245s (-16.9% 🟢)1.009s (~)0.764s601.01x
🌐 RedisNext.js (Turbopack)0.260s1.005s0.744s601.07x
🐘 PostgresNext.js (Turbopack)0.287s1.010s0.723s601.18x
💻 LocalNitro0.576s (-3.7%)1.004s (~)0.428s602.37x
💻 LocalExpress0.581s (-3.7%)1.004s (~)0.424s602.39x
💻 LocalNext.js (Turbopack)0.585s1.004s0.420s602.41x
workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.419s (-20.5% 🟢)1.009s (~)0.589s901.00x
🐘 PostgresNitro0.424s (-21.4% 🟢)1.009s (~)0.585s901.01x
🐘 PostgresNext.js (Turbopack)0.590s1.033s0.443s881.41x
🌐 RedisNext.js (Turbopack)1.194s2.006s0.812s452.85x
💻 LocalNitro2.345s (-5.2% 🟢)3.007s (~)0.662s305.59x
💻 LocalExpress2.498s (-3.2%)3.009s (~)0.510s305.96x
💻 LocalNext.js (Turbopack)2.664s3.009s0.345s306.36x
workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.606s (-35.5% 🟢)1.009s (-15.4% 🟢)0.403s1191.00x
🐘 PostgresExpress0.620s (-30.9% 🟢)1.009s (-3.1%)0.389s1191.02x
🐘 PostgresNext.js (Turbopack)1.001s1.477s0.476s821.65x
🌐 RedisNext.js (Turbopack)2.883s3.085s0.202s394.76x
💻 LocalNitro10.189s (-8.3% 🟢)10.855s (-6.9% 🟢)0.666s1216.83x
💻 LocalExpress11.127s (-1.4%)11.845s (-1.5%)0.718s1118.38x
💻 LocalNext.js (Turbopack)11.287s11.753s0.465s1118.64x
Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro0.140s (-28.6% 🟢)1.003s (~)0.009s (-26.3% 🟢)1.014s (~)0.874s101.00x
🐘 PostgresExpress0.164s (-24.1% 🟢)0.999s (+0.5%)0.001s (-8.3% 🟢)1.013s (~)0.848s101.17x
🐘 PostgresNitro0.167s (-30.3% 🟢)0.999s (+0.7%)0.001s (-21.4% 🟢)1.013s (~)0.846s101.19x
🌐 RedisNext.js (Turbopack)0.169s1.000s0.001s1.008s0.838s101.21x
💻 LocalNext.js (Turbopack)0.178s1.001s0.012s1.018s0.840s101.27x
💻 LocalExpress0.199s (-2.4%)1.003s (~)0.012s (-2.5%)1.017s (~)0.818s101.42x
🐘 PostgresNext.js (Turbopack)0.224s1.001s0.001s1.013s0.789s101.60x
stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)0.447s0.999s0.003s1.012s0.565s601.00x
🐘 PostgresNitro0.563s (-22.1% 🟢)1.005s (~)0.003s (-13.6% 🟢)1.023s (~)0.460s591.26x
💻 LocalNitro0.569s (-21.7% 🟢)1.008s (~)0.009s (-6.2% 🟢)1.021s (~)0.452s591.27x
🐘 PostgresExpress0.610s (-12.0% 🟢)1.006s (~)0.005s (+20.4% 🔺)1.028s (~)0.417s591.37x
💻 LocalNext.js (Turbopack)0.672s1.008s0.010s1.024s0.352s591.50x
💻 LocalExpress0.723s (-6.9% 🟢)1.009s (-1.7%)0.009s (-12.1% 🟢)1.022s (-1.8%)0.300s591.62x
🐘 PostgresNext.js (Turbopack)0.760s1.026s0.008s1.050s0.290s591.70x
10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)0.916s1.016s0.000s1.022s0.106s591.00x
🐘 PostgresNitro0.988s (+7.3% 🔺)1.384s (+30.2% 🔺)0.000s (-36.4% 🟢)1.398s (+29.3% 🔺)0.410s441.08x
🐘 PostgresExpress1.066s (+19.3% 🔺)1.664s (+53.7% 🔺)0.000s (+205.6% 🔺)1.677s (+51.8% 🔺)0.612s361.16x
💻 LocalNitro1.140s (-6.9% 🟢)2.017s (~)0.000s (-38.5% 🟢)2.020s (~)0.880s301.24x
🐘 PostgresNext.js (Turbopack)1.175s1.909s0.000s1.930s0.755s321.28x
💻 LocalExpress1.235s (-1.8%)2.020s (~)0.000s (-35.3% 🟢)2.023s (~)0.788s301.35x
💻 LocalNext.js (Turbopack)1.295s2.019s0.000s2.024s0.729s301.41x
fan-out fan-in 10 streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.886s (~)2.223s (~)0.000s (-100.0% 🟢)2.233s (~)0.347s271.00x
🌐 RedisNext.js (Turbopack)1.963s2.180s0.000s2.186s0.223s281.04x
🐘 PostgresExpress2.034s (+12.7% 🔺)2.441s (+16.3% 🔺)0.000s (+Infinity% 🔺)2.453s (+15.6% 🔺)0.418s251.08x
🐘 PostgresNext.js (Turbopack)2.302s3.005s0.000s3.015s0.713s201.22x
💻 LocalNitro3.350s (+0.6%)3.967s (~)0.000s (-60.0% 🟢)3.971s (~)0.621s161.78x
💻 LocalExpress3.508s (+1.9%)4.030s (-1.6%)0.001s (+37.5% 🔺)4.035s (-1.6%)0.527s151.86x
💻 LocalNext.js (Turbopack)3.784s4.387s0.000s4.392s0.608s142.01x

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNitro21/21
🐘 PostgresExpress12/21
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express🐘 Postgres20/21
Next.js (Turbopack)🌐 Redis15/21
Nitro🐘 Postgres17/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)

📋 View full workflow run


Some benchmark jobs failed:

  • Local: success
  • Postgres: success
  • Vercel: failure

Check the workflow run for details.

@TooTallNateGraphite App

Copy link
Copy Markdown
MemberAuthor

This stack of pull requests is managed by Graphite. Learn more about stacking.

@TooTallNate
TooTallNate marked this pull request as ready for review February 20, 2026 22:47
CopilotAI review requested due to automatic review settings February 20, 2026 22:47

CopilotAI 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.

Pull request overview

This PR extends the @workflow/swc-plugin transform to detect custom class serialization methods when the WORKFLOW_SERIALIZE / WORKFLOW_DESERIALIZE symbols are accessed via CommonJS require() patterns, aligning behavior with codebases that have been transpiled from ESM to CJS.

Changes:

  • Track CommonJS require() bindings during identifier collection (namespace const x = require(...) and destructured const { ... } = require(...)) and recognize x.WORKFLOW_SERIALIZE / x.WORKFLOW_DESERIALIZE as serialization symbols.
  • Add fixture coverage for both namespace-require and destructured-require inputs across workflow/step/client outputs.
  • Update the plugin spec and add a changeset entry documenting the new behavior.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 2 comments.

Show a summary per file
FileDescription
packages/swc-plugin-workflow/transform/src/lib.rsAdds require-binding tracking and member-expression symbol recognition for serialization detection.
packages/swc-plugin-workflow/transform/tests/fixture/custom-serialization-require-namespace/input.jsNew fixture input for namespace require() pattern.
packages/swc-plugin-workflow/transform/tests/fixture/custom-serialization-require-namespace/output-workflow.jsExpected workflow-mode output for namespace require() fixture.
packages/swc-plugin-workflow/transform/tests/fixture/custom-serialization-require-namespace/output-step.jsExpected step-mode output for namespace require() fixture.
packages/swc-plugin-workflow/transform/tests/fixture/custom-serialization-require-namespace/output-client.jsExpected client-mode output for namespace require() fixture.
packages/swc-plugin-workflow/transform/tests/fixture/custom-serialization-require-destructured/input.jsNew fixture input for destructured require() pattern.
packages/swc-plugin-workflow/transform/tests/fixture/custom-serialization-require-destructured/output-workflow.jsExpected workflow-mode output for destructured require() fixture.
packages/swc-plugin-workflow/transform/tests/fixture/custom-serialization-require-destructured/output-step.jsExpected step-mode output for destructured require() fixture.
packages/swc-plugin-workflow/transform/tests/fixture/custom-serialization-require-destructured/output-client.jsExpected client-mode output for destructured require() fixture.
packages/swc-plugin-workflow/spec.mdDocuments supported CommonJS require() symbol patterns.
.changeset/social-forks-tease.mdDeclares a patch release for the new detection behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment threadpackages/swc-plugin-workflow/transform/src/lib.rs Outdated
Comment threadpackages/swc-plugin-workflow/transform/src/lib.rs Outdated
…WC plugin
The SWC compiler plugin now detects classes with custom serialization methods (`WORKFLOW_SERIALIZE` / `WORKFLOW_DESERIALIZE`) when symbols are obtained via CommonJS `require()` calls, in addition to the existing ESM import and `Symbol.for()` patterns.
This handles code that has been pre-compiled from ESM to CommonJS by tools like TypeScript (tsc), esbuild, or tsup, which transform:
```ts
import { WORKFLOW_SERIALIZE } from "@workflow/serde"
```
into either:
```ts
const serde_1 = require("@workflow/serde") // namespace require
const { WORKFLOW_SERIALIZE } = require(...) // destructured require
```
Both patterns are now recognized during the identifier collection phase, and classes using them are properly registered with registerSerializationClass().
@TooTallNateTooTallNate changed the title Add CommonJS require() support for class serialization detection in SWC pluginAdd CommonJS require() support for class serialization detection in SWC pluginMar 23, 2026
…on_bindings helper
- Tighten is_require_call to require exactly one string literal argument,
rejecting dynamic requires and multi-arg calls.
- Extract track_serialization_bindings helper to deduplicate the require
tracking logic between Decl::Var and ExportDecl(Decl::Var) branches.
@TooTallNate
TooTallNateforce-pushed the 02-20-add_commonjs_require_support_for_class_serialization_detection_in_swc_plugin branch from 3648e46 to f461c59CompareMarch 23, 2026 14:01
@TooTallNate
TooTallNate requested a review from a team as a code ownerMarch 23, 2026 14:01
@TooTallNate
TooTallNate merged commit 992d768 into mainMar 23, 2026
98 of 103 checks passed
@TooTallNate
TooTallNate deleted the 02-20-add_commonjs_require_support_for_class_serialization_detection_in_swc_plugin branch March 23, 2026 18:30
@ghostghost mentioned this pull request Mar 23, 2026

@pranaygppranaygp 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.

Post-merge review — overall this is a clean, well-tested addition. A few observations below.

// Track CommonJS namespace require: const serde_1 = require("...")
if self.is_require_call(init) {
self.require_namespace_identifiers
.insert(ident.id.sym.to_string());

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.

Nit: This tracks everyrequire() call as a namespace identifier, regardless of the module being required. So const fs = require("fs") would cause fs.WORKFLOW_SERIALIZE to be treated as a serialization symbol if it ever appeared. This is unlikely to cause real bugs since nobody would name a property WORKFLOW_SERIALIZE on an unrelated module, but it is semantically imprecise.

Consider either:

  1. Restricting to known serde modules (e.g. checking the string arg contains @workflow/serde or similar), or
  2. Documenting this as an intentional trade-off (broad tracking is simpler and the false-positive risk is negligible).

match prop {
ObjectPatProp::Assign(assign) => {
// const { WORKFLOW_SERIALIZE } = require("...")
let name = assign.key.sym.to_string();

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.

Nit: The ObjectPatProp::Assign arm handles const { WORKFLOW_SERIALIZE } = require(...), but in the SWC AST this node actually represents const { WORKFLOW_SERIALIZE = defaultValue } = require(...) (a destructured binding with a default value). A plain shorthand const { WORKFLOW_SERIALIZE } = require(...) is typically parsed as ObjectPatProp::KeyValue where key and value ident are the same.

In practice this still works because SWC may represent the no-default shorthand either way depending on version, and the logic is correct for both paths. But worth noting that the Assign arm would also match const { WORKFLOW_SERIALIZE = someDefault } = require(...), which is an unusual but technically valid pattern.

@@ -0,0 +1,25 @@
// Test custom serialization with CommonJS destructured require
const { WORKFLOW_SERIALIZE, WORKFLOW_DESERIALIZE } = require("@workflow/serde");

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.

The spec documents renamed destructured require (const { WORKFLOW_SERIALIZE: WS } = require(...)) but there's no test fixture covering this case. Adding a custom-serialization-require-destructured-renamed fixture would ensure the ObjectPatProp::KeyValue branch stays correct.

pranaygp added a commit that referenced this pull request Mar 23, 2026
* origin/main:
[ai] Add experimental_context to DurableAgentOptions (#1489)
[ai] Expose configured tools on DurableAgent instances (#1488)
fix(builders): catch node builtin usage when entry fields diverge (#1455)
[web-shared] Fix timeline duration format and precision (#1482)
[cli] Add bulk cancel, --status filter, fix step JSON hydration (#1467)
[utils] Re-export parseName utilities and add workflow/observability module (#1453)
[o11y] Polish display when run data has expired (#1438)
Add CommonJS `require()` support for class serialization detection in SWC plugin (#1144)
fix(next): stabilize deferred canary e2e in nextjs workbenches (#1468)
[web] Support legacy newline-delimited stream format in `useStreamReader` (#1473)
Revert "Add support for calling start() inside workflow functions (#1133)" (#1475)
pranaygp added a commit that referenced this pull request Mar 24, 2026
…naygp-db9e68c1
* 'main' of https://github.com/vercel/workflow: (32 commits)
chore: bump @nestjs/* to ^11.1.17 (#1497)
chore: bump hono to ^4.12.8 (#1495)
Revert "Inline class serialization registration to fix 3rd-party package supp…" (#1493)
[world] Add stream pagination and metadata endpoints (#1470)
[cli] [world-local] Ensure update checks don't suggest upgrading from stable release to pre-releases (#1490)
Remove NestJS Vercel integration while in experimental phase (#1485)
feat: export semantic error types and add API reference docs (#1447)
feat: enforce max queue deliveries in handlers with graceful failure (#1344)
[world-postgres] Migrate client from `postgres.js` to `pg` (#1484)
Inline class serialization registration to fix 3rd-party package support (#1480)
[ai] Add experimental_context to DurableAgentOptions (#1489)
[ai] Expose configured tools on DurableAgent instances (#1488)
fix(builders): catch node builtin usage when entry fields diverge (#1455)
[web-shared] Fix timeline duration format and precision (#1482)
[cli] Add bulk cancel, --status filter, fix step JSON hydration (#1467)
[utils] Re-export parseName utilities and add workflow/observability module (#1453)
[o11y] Polish display when run data has expired (#1438)
Add CommonJS `require()` support for class serialization detection in SWC plugin (#1144)
fix(next): stabilize deferred canary e2e in nextjs workbenches (#1468)
[web] Support legacy newline-delimited stream format in `useStreamReader` (#1473)
...
TooTallNate added a commit that referenced this pull request Mar 24, 2026
The original PR #1480 was merged but reverted because it didn't include
updated fixtures for the CJS require patterns added by PR #1144
(custom-serialization-require-destructured and
custom-serialization-require-namespace). These fixtures still had the
old 'import { registerSerializationClass }' pattern instead of the
new inline IIFE.
TooTallNate added a commit that referenced this pull request Mar 24, 2026
…ort (v2) (#1503)
* Inline class serialization registration to fix 3rd-party package support (#1480)
* Inline class serialization registration to fix 3rd-party package support
The SWC plugin previously generated:
import { registerSerializationClass } from "workflow/internal/class-serialization";
registerSerializationClass("class//...", ClassName);
This broke for 3rd-party packages (e.g. @vercel/sandbox) that define
serializable classes but don't depend on the 'workflow' package. The
bare 'workflow' specifier is unresolvable from within node_modules of
a package that doesn't list it as a dependency.
Now the plugin generates a self-contained IIFE that uses
Symbol.for('workflow-class-registry') on globalThis directly, with
zero module dependencies:
(function(__wf_cls, __wf_id) {
var __wf_sym = Symbol.for("workflow-class-registry"),
__wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map());
__wf_reg.set(__wf_id, __wf_cls);
Object.defineProperty(__wf_cls, "classId", { ... });
})(ClassName, "class//...");
This is fully compatible with the existing deserialization side in
@workflow/core which reads from the same globalThis registry.
* Address review feedback: fix comment and update docstring
- Fix inaccurate IIFE comment in lib.rs: the second arg is the
generated class ID string, not the literal "classId"
- Update registerSerializationClass docstring to reflect that the
SWC plugin now inlines equivalent logic rather than importing it
* Update CJS require fixture outputs for inline class serialization
The original PR #1480 was merged but reverted because it didn't include
updated fixtures for the CJS require patterns added by PR #1144
(custom-serialization-require-destructured and
custom-serialization-require-namespace). These fixtures still had the
old 'import { registerSerializationClass }' pattern instead of the
new inline IIFE.
pranaygp pushed a commit to vercel/sandbox that referenced this pull request Mar 27, 2026
…ibility (#109)
## Summary
Makes `@vercel/sandbox` fully compatible with the Workflow DevKit
compiler so that `Sandbox`, `Command`, and `CommandFinished` instances
can be used directly inside `"use workflow"` functions — no wrapper step
functions needed.
Supersedes #58.
## Problem
When `@vercel/sandbox` is imported in a workflow context, the workflow
builder tries to bundle it into the workflow VM bundle (because it has
`WORKFLOW_SERIALIZE`/`WORKFLOW_DESERIALIZE` on its classes). This fails
because:
1. The SDK's public methods use Node.js APIs (`fs`, `stream`, `zlib`,
`undici`, etc.) which are forbidden in the workflow VM
2. The compiled `dist/index.js` was a single bundled file that hoisted
all Node.js imports to the top, making them impossible to tree-shake
3. The `Sandbox` and `Command` classes had a sync `client` getter that
directly referenced `APIClient`, pulling the entire HTTP client stack
into the module scope
## Solution
### 1. `"use step"` annotations on all public async methods
Added `"use step"` to all 22 public async methods across `Sandbox` (14),
`Command` (5), and `Snapshot` (3). The SWC plugin strips these method
bodies in workflow mode, replacing them with durable step proxies. This
eliminates all Node.js API references from the workflow bundle.
### 2. Async `ensureClient()` replaces sync `client` getter
The sync `get client()` getter directly referenced `APIClient`, which
pulls in `undici`, `zlib`, `tar-stream`, `jsonlines`, etc. Replaced
with:
```typescript
private async ensureClient(): Promise<APIClient> {
"use step";
if (this._client) return this._client;
const credentials = getSandboxCredentials();
this._client = new APIClient({ ... });
return this._client;
}
```
Since `ensureClient()` is itself `"use step"`, its body (including `new
APIClient(...)`) gets stripped in workflow mode. All instance methods
now call `const client = await this.ensureClient();` instead of
`this.client`.
### 3. `bundle: false` in tsdown config
Changed from single-file bundling to per-file output. This keeps Node.js
imports local to the files that use them, so after the SWC plugin strips
step method bodies, the now-unused Node.js imports can be eliminated by
esbuild's tree-shaking.
### 4. Workflow-code-runner example updated
- Removed `serverExternalPackages: ["@vercel/sandbox"]` from
`next.config.ts` (no longer needed)
- Updated `workflow` dependency to use a tarball that includes the
inline class serialization registration fix (vercel/workflow#1480)
## Testing
- `pnpm build` succeeds for the full monorepo (all 8 tasks)
- The `workflow-code-runner` example app builds successfully with all
workflow routes generated
- 22 `"use step"` directives survive compilation in both ESM and CJS
dist output
## Related
- vercel/workflow#1480 — Inline class serialization registration (fixes
SWC import resolution for 3rd-party packages)
- vercel/workflow#1481 — Build-time warning when
`serverExternalPackages` hides workflow-enabled packages
- vercel/workflow#1144 — CJS detection for serde symbols (pending
review)
- #58 — Previous attempt (superseded by this PR)
---------
Signed-off-by: Peter Wielander <mittgfu@gmail.com>
Co-authored-by: Peter Wielander <mittgfu@gmail.com>
Sign up for freeto 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.

4 participants

@TooTallNate@pranaygp@VaguelySerious
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Add CommonJS `require()` support for class serialization detection in SWC plugin by TooTallNate · Pull Request #1144 · vercel/workflow · GitHub
Skip to content

Add CommonJS require() support for class serialization detection in SWC plugin - #1144

Merged
TooTallNate merged 3 commits into
mainfrom
02-20-add_commonjs_require_support_for_class_serialization_detection_in_swc_plugin
Mar 23, 2026
Merged

Add CommonJS require() support for class serialization detection in SWC plugin#1144
TooTallNate merged 3 commits into
mainfrom
02-20-add_commonjs_require_support_for_class_serialization_detection_in_swc_plugin

Conversation

@TooTallNate

Copy link
Copy Markdown
Member

The SWC compiler plugin now detects classes with custom serialization methods (WORKFLOW_SERIALIZE / WORKFLOW_DESERIALIZE) when symbols are obtained via CommonJS require() calls, in addition to the existing ESM import and Symbol.for() patterns.

This handles code that has been pre-compiled from ESM to CommonJS by tools like TypeScript (tsc), esbuild, or tsup, which transform:

import{WORKFLOW_SERIALIZE}from"@workflow/serde"

into either:

constserde_1=require("@workflow/serde")// namespace requireconst{WORKFLOW_SERIALIZE}=require(...)// destructured require

Both patterns are now recognized during the identifier collection phase, and classes using them are properly registered with registerSerializationClass().

@changeset-bot

changeset-botBot commented Feb 20, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 14ea197

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

This PR includes changesets to release 17 packages
NameType
@workflow/swc-pluginPatch
@workflow/astroPatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nestPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
workflowPatch
@workflow/vitePatch
@workflow/vitestPatch
@workflow/world-testingPatch
@workflow/nuxtPatch
@workflow/aiPatch
@workflow/corePatch
@workflow/web-sharedPatch

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

@vercel

vercelBot commented Feb 20, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Feb 20, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production780067847
✅ 💻 Local Development7820142924
✅ 📦 Local Production7820142924
✅ 🐘 Local Postgres7820142924
✅ 🪟 Windows720577
❌ 🌍 Community Worlds1185621195
✅ 📋 Other198033231
Total3514565524122

❌ Failed Tests

🌍 Community Worlds (56 failed)

mongodb (3 failed):

  • hookWorkflow is not resumable via public webhook endpoint | wrun_01KMDYE4RWSYQ6538DVH70M5P0
  • webhookWorkflow | wrun_01KMDYED4N9B1F4PF1X6F86J26
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously | wrun_01KMDYMVS1CFTACB4081WS8K9V

redis (2 failed):

  • hookWorkflow is not resumable via public webhook endpoint | wrun_01KMDYE4RWSYQ6538DVH70M5P0
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously | wrun_01KMDYMVS1CFTACB4081WS8K9V

turso (51 failed):

  • addTenWorkflow | wrun_01KMDYCYXATZBJYFAPPKVRZKDZ
  • addTenWorkflow | wrun_01KMDYCYXATZBJYFAPPKVRZKDZ
  • wellKnownAgentWorkflow (.well-known/agent) | wrun_01KMDYEKVFEVFMCXC32DMPJ3KB
  • should work with react rendering in step
  • promiseAllWorkflow | wrun_01KMDYD5F67YMHR6KTJF5YFYM3
  • promiseRaceWorkflow | wrun_01KMDYDBXWV7065WDZ655A6X2Y
  • promiseAnyWorkflow | wrun_01KMDYDEFBS29N1Y4E04XS0PHS
  • importedStepOnlyWorkflow | wrun_01KMDYF31ZMHDYKRR9870BQJFE
  • hookWorkflow | wrun_01KMDYDTDYK5ZZTVGBV7PBPJAZ
  • hookWorkflow is not resumable via public webhook endpoint | wrun_01KMDYE4RWSYQ6538DVH70M5P0
  • webhookWorkflow | wrun_01KMDYED4N9B1F4PF1X6F86J26
  • sleepingWorkflow | wrun_01KMDYEKNPTWHR8GD4VJXRHSWR
  • parallelSleepWorkflow | wrun_01KMDYF1MMFSRK8683TXYGPEFF
  • nullByteWorkflow | wrun_01KMDYF6AS6SP633VT5B7JZ0Q8
  • workflowAndStepMetadataWorkflow | wrun_01KMDYF8B1WBHNCPZKHFYCH5G0
  • fetchWorkflow | wrun_01KMDYH6MPPT3CPGBXX02XNQTP
  • promiseRaceStressTestWorkflow | wrun_01KMDYH9Y93SDT6D7TEE6CMKHT
  • error handling error propagation workflow errors nested function calls preserve message and stack trace
  • error handling error propagation workflow errors cross-file imports preserve message and stack trace
  • error handling error propagation step errors basic step error preserves message and stack trace
  • error handling error propagation step errors cross-file step error preserves message and function names in stack
  • error handling retry behavior regular Error retries until success
  • error handling retry behavior FatalError fails immediately without retries
  • error handling retry behavior RetryableError respects custom retryAfter delay
  • error handling retry behavior maxRetries=0 disables retries
  • error handling catchability FatalError can be caught and detected with FatalError.is()
  • hookCleanupTestWorkflow - hook token reuse after workflow completion | wrun_01KMDYM7FJ0MWDXPS56VM7PF4D
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously | wrun_01KMDYMVS1CFTACB4081WS8K9V
  • hookDisposeTestWorkflow - hook token reuse after explicit disposal while workflow still running | wrun_01KMDYNGM0E0ZXFK8E2CQ9CYZV
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars) | wrun_01KMDYP5F3H7NXZ9W494MXF8EQ
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument | wrun_01KMDYPFQ0MK8D4B36398E2S6A
  • closureVariableWorkflow - nested step functions with closure variables | wrun_01KMDYPN9T3F9HBR5W1TQSBAVH
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step | wrun_01KMDYPQBVNH7GG5Y8ZDS8B1P3
  • health check (queue-based) - workflow and step endpoints respond to health check messages
  • pathsAliasWorkflow - TypeScript path aliases resolve correctly | wrun_01KMDYQ7ZKSHZ1DF03RZ64NGXD
  • Calculator.calculate - static workflow method using static step methods from another class | wrun_01KMDYQEMCVB21XR91WVHJEGK4
  • AllInOneService.processNumber - static workflow method using sibling static step methods | wrun_01KMDYQP3ST1XWR4FTRNPGNYE4
  • ChainableService.processWithThis - static step methods using this to reference the class | wrun_01KMDYQWMSK6ZYMFNY7ME546A9
  • thisSerializationWorkflow - step function invoked with .call() and .apply() | wrun_01KMDYR36AW71R74TKNQEA26QC
  • customSerializationWorkflow - custom class serialization with WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE | wrun_01KMDYR9J9RVJCAWSQG76N5656
  • instanceMethodStepWorkflow - instance methods with "use step" directive | wrun_01KMDYRH7WBXH2XGDZPC5CXB49
  • crossContextSerdeWorkflow - classes defined in step code are deserializable in workflow context | wrun_01KMDYRVMSG67Q29E0JT62SCPB
  • stepFunctionAsStartArgWorkflow - step function reference passed as start() argument | wrun_01KMDYS38DF4PJE3SJHH7YV0GQ
  • cancelRun - cancelling a running workflow | wrun_01KMDYSB4MVW6TNAVS70ZDC36V
  • cancelRun via CLI - cancelling a running workflow | wrun_01KMDYSM88CM0VKBMVABG1ZRM0
  • pages router addTenWorkflow via pages router
  • pages router promiseAllWorkflow via pages router
  • pages router sleepingWorkflow via pages router
  • hookWithSleepWorkflow - hook payloads delivered correctly with concurrent sleep | wrun_01KMDYT02V9K533MN8PA31072V
  • sleepInLoopWorkflow - sleep inside loop with steps actually delays each iteration | wrun_01KMDYTKS4H2ZFNHJC3AH7D70B
  • sleepWithSequentialStepsWorkflow - sequential steps work with concurrent sleep (control) | wrun_01KMDYTZMDM1Q8370F5R6NNHJV

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro7007
✅ example7007
✅ express7007
✅ fastify7007
✅ hono7007
✅ nextjs-turbopack7502
✅ nextjs-webpack7502
✅ nitro7007
✅ nuxt7007
✅ sveltekit7007
✅ vite7007
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable66011
✅ express-stable66011
✅ fastify-stable66011
✅ hono-stable66011
✅ nextjs-turbopack-canary55022
✅ nextjs-turbopack-stable7205
✅ nextjs-webpack-canary55022
✅ nextjs-webpack-stable7205
✅ nitro-stable66011
✅ nuxt-stable66011
✅ sveltekit-stable66011
✅ vite-stable66011
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable66011
✅ express-stable66011
✅ fastify-stable66011
✅ hono-stable66011
✅ nextjs-turbopack-canary55022
✅ nextjs-turbopack-stable7205
✅ nextjs-webpack-canary55022
✅ nextjs-webpack-stable7205
✅ nitro-stable66011
✅ nuxt-stable66011
✅ sveltekit-stable66011
✅ vite-stable66011
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable66011
✅ express-stable66011
✅ fastify-stable66011
✅ hono-stable66011
✅ nextjs-turbopack-canary55022
✅ nextjs-turbopack-stable7205
✅ nextjs-webpack-canary55022
✅ nextjs-webpack-stable7205
✅ nitro-stable66011
✅ nuxt-stable66011
✅ sveltekit-stable66011
✅ vite-stable66011
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack7205
❌ 🌍 Community Worlds
AppPassedFailedSkipped
✅ mongodb-dev302
❌ mongodb5235
✅ redis-dev302
❌ redis5325
✅ turso-dev302
❌ turso4515
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable66011
✅ e2e-local-postgres-nest-stable66011
✅ e2e-local-prod-nest-stable66011

📋 View full workflow run

@github-actions

github-actionsBot commented Feb 20, 2026

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

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

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro0.037s (-21.3% 🟢)1.005s (~)0.967s101.00x
💻 LocalExpress0.043s (-4.5%)1.005s (~)0.962s101.14x
🐘 PostgresNitro0.047s (-31.9% 🟢)1.012s (~)0.965s101.26x
🐘 PostgresExpress0.048s (-32.2% 🟢)1.012s (-0.5%)0.964s101.29x
💻 LocalNext.js (Turbopack)0.051s1.006s0.956s101.35x
🌐 RedisNext.js (Turbopack)0.054s1.005s0.952s101.44x
🐘 PostgresNext.js (Turbopack)0.062s1.012s0.950s101.65x
workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.097s (-2.8%)2.005s (~)0.908s101.00x
🌐 RedisNext.js (Turbopack)1.120s2.007s0.887s101.02x
🐘 PostgresNitro1.124s (-1.6%)2.012s (~)0.888s101.02x
💻 LocalNext.js (Turbopack)1.124s2.006s0.883s101.02x
🐘 PostgresExpress1.126s (-1.1%)2.012s (~)0.886s101.03x
💻 LocalExpress1.131s (~)2.005s (~)0.874s101.03x
🐘 PostgresNext.js (Turbopack)1.143s2.013s0.869s101.04x
workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro10.635s (-2.4%)11.023s (~)0.388s31.00x
🌐 RedisNext.js (Turbopack)10.680s11.025s0.345s31.00x
🐘 PostgresNitro10.689s (-2.4%)11.036s (~)0.348s31.01x
🐘 PostgresExpress10.705s (-1.8%)11.038s (~)0.333s31.01x
💻 LocalNext.js (Turbopack)10.847s11.025s0.178s31.02x
💻 LocalExpress10.934s (~)11.023s (~)0.089s31.03x
🐘 PostgresNext.js (Turbopack)10.996s11.382s0.386s31.03x
workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)14.091s15.033s0.941s41.00x
🐘 PostgresExpress14.123s (-3.1%)15.042s (~)0.919s41.00x
🐘 PostgresNitro14.129s (-4.7%)15.039s (~)0.910s41.00x
💻 LocalNitro14.183s (-4.9%)15.029s (~)0.847s41.01x
🐘 PostgresNext.js (Turbopack)14.580s15.049s0.469s41.03x
💻 LocalNext.js (Turbopack)14.703s15.031s0.328s41.04x
💻 LocalExpress14.911s (-0.6%)15.029s (-3.2%)0.118s41.06x
workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)13.030s13.457s0.427s71.00x
🐘 PostgresExpress13.096s (-6.5% 🟢)14.038s (-3.0%)0.942s71.01x
🐘 PostgresNitro13.119s (-8.7% 🟢)14.038s (-6.7% 🟢)0.919s71.01x
🐘 PostgresNext.js (Turbopack)14.145s14.759s0.613s71.09x
💻 LocalNitro14.789s (-10.1% 🟢)15.026s (-11.8% 🟢)0.237s61.14x
💻 LocalNext.js (Turbopack)16.157s17.033s0.875s61.24x
💻 LocalExpress16.514s (-1.0%)17.029s (~)0.516s61.27x
Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.228s (-3.0%)2.011s (~)0.783s151.00x
🐘 PostgresNitro1.231s (-4.8%)2.011s (~)0.780s151.00x
🌐 RedisNext.js (Turbopack)1.266s2.007s0.741s151.03x
🐘 PostgresNext.js (Turbopack)1.286s2.011s0.725s151.05x
💻 LocalNitro1.472s (-0.8%)2.005s (~)0.534s151.20x
💻 LocalExpress1.503s (~)2.006s (~)0.503s151.22x
💻 LocalNext.js (Turbopack)1.524s2.006s0.482s151.24x
Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express2.341s (-4.9%)3.012s (~)0.671s101.00x
🐘 PostgresNitro2.353s (-4.8%)3.013s (~)0.660s101.01x
🌐 RedisNext.js (Turbopack)2.478s3.009s0.531s101.06x
🐘 PostgresNext.js (Turbopack)2.484s3.015s0.531s101.06x
💻 LocalNitro2.551s (-11.2% 🟢)3.007s (-3.2%)0.456s101.09x
💻 LocalExpress2.970s (-0.9%)3.308s (-7.2% 🟢)0.338s101.27x
💻 LocalNext.js (Turbopack)2.995s3.309s0.314s101.28x
Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express3.519s (-1.9%)4.014s (~)0.495s81.00x
🐘 PostgresNitro3.523s (-3.0%)4.015s (~)0.492s81.00x
🐘 PostgresNext.js (Turbopack)3.844s4.017s0.173s81.09x
🌐 RedisNext.js (Turbopack)4.117s4.727s0.610s71.17x
💻 LocalNitro7.025s (-13.5% 🟢)7.514s (-14.3% 🟢)0.489s42.00x
💻 LocalExpress8.144s (-3.9%)8.769s (-2.8%)0.625s42.31x
💻 LocalNext.js (Turbopack)8.563s9.019s0.456s42.43x
Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.207s (-4.1%)2.011s (~)0.803s151.00x
🐘 PostgresNitro1.216s (-4.7%)2.010s (~)0.794s151.01x
🌐 RedisNext.js (Turbopack)1.252s2.007s0.755s151.04x
🐘 PostgresNext.js (Turbopack)1.254s2.011s0.757s151.04x
💻 LocalNitro1.484s (-4.9%)2.006s (~)0.522s151.23x
💻 LocalNext.js (Turbopack)1.515s2.005s0.490s151.25x
💻 LocalExpress1.550s (+0.9%)2.005s (~)0.455s151.28x
Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro2.342s (-4.7%)3.012s (~)0.670s101.00x
🐘 PostgresExpress2.353s (-4.0%)3.012s (~)0.659s101.00x
🐘 PostgresNext.js (Turbopack)2.474s3.013s0.539s101.06x
🌐 RedisNext.js (Turbopack)2.498s3.009s0.511s101.07x
💻 LocalNitro2.924s (-3.4%)3.208s (-12.7% 🟢)0.283s101.25x
💻 LocalNext.js (Turbopack)3.012s3.675s0.663s91.29x
💻 LocalExpress3.090s (+1.9%)3.885s (+3.4%)0.795s81.32x
Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express3.515s (-2.3%)4.013s (~)0.497s81.00x
🐘 PostgresNitro3.518s (-2.7%)4.014s (~)0.496s81.00x
🐘 PostgresNext.js (Turbopack)3.841s4.018s0.177s81.09x
🌐 RedisNext.js (Turbopack)4.031s4.583s0.553s71.15x
💻 LocalNitro7.511s (-13.1% 🟢)8.016s (-11.2% 🟢)0.506s42.14x
💻 LocalExpress8.608s (-5.0%)9.020s (-7.7% 🟢)0.412s42.45x
💻 LocalNext.js (Turbopack)9.214s9.772s0.557s42.62x
workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)0.601s1.005s0.404s601.00x
🐘 PostgresExpress0.642s (-25.8% 🟢)1.009s (-1.7%)0.366s601.07x
🐘 PostgresNitro0.657s (-26.8% 🟢)1.008s (-3.4%)0.351s601.09x
💻 LocalNitro0.672s (-29.7% 🟢)1.004s (-1.7%)0.332s601.12x
🐘 PostgresNext.js (Turbopack)0.870s1.009s0.140s601.45x
💻 LocalNext.js (Turbopack)0.878s1.057s0.179s571.46x
💻 LocalExpress0.973s (-1.0%)1.114s (+1.7%)0.140s551.62x
workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)1.460s2.006s0.546s451.00x
🐘 PostgresExpress1.553s (-26.6% 🟢)2.010s (-33.3% 🟢)0.457s451.06x
🐘 PostgresNitro1.563s (-27.7% 🟢)2.032s (-31.8% 🟢)0.470s451.07x
🐘 PostgresNext.js (Turbopack)2.127s2.948s0.821s311.46x
💻 LocalNitro2.193s (-25.8% 🟢)3.007s (-3.3%)0.815s301.50x
💻 LocalNext.js (Turbopack)2.736s3.008s0.272s301.87x
💻 LocalExpress2.966s (-1.6%)3.222s (-9.2% 🟢)0.255s282.03x
workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)3.077s3.437s0.360s351.00x
🐘 PostgresNitro3.153s (-27.3% 🟢)4.012s (-20.0% 🟢)0.859s301.02x
🐘 PostgresExpress3.188s (-25.2% 🟢)4.012s (-19.3% 🟢)0.824s301.04x
🐘 PostgresNext.js (Turbopack)4.345s5.015s0.670s241.41x
💻 LocalNitro7.218s (-20.1% 🟢)7.827s (-17.4% 🟢)0.610s162.35x
💻 LocalNext.js (Turbopack)8.797s9.232s0.434s142.86x
💻 LocalExpress8.995s (-1.7%)9.479s (-3.1%)0.484s132.92x
workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.243s (-19.7% 🟢)1.008s (~)0.766s601.00x
🐘 PostgresNitro0.245s (-16.9% 🟢)1.009s (~)0.764s601.01x
🌐 RedisNext.js (Turbopack)0.260s1.005s0.744s601.07x
🐘 PostgresNext.js (Turbopack)0.287s1.010s0.723s601.18x
💻 LocalNitro0.576s (-3.7%)1.004s (~)0.428s602.37x
💻 LocalExpress0.581s (-3.7%)1.004s (~)0.424s602.39x
💻 LocalNext.js (Turbopack)0.585s1.004s0.420s602.41x
workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.419s (-20.5% 🟢)1.009s (~)0.589s901.00x
🐘 PostgresNitro0.424s (-21.4% 🟢)1.009s (~)0.585s901.01x
🐘 PostgresNext.js (Turbopack)0.590s1.033s0.443s881.41x
🌐 RedisNext.js (Turbopack)1.194s2.006s0.812s452.85x
💻 LocalNitro2.345s (-5.2% 🟢)3.007s (~)0.662s305.59x
💻 LocalExpress2.498s (-3.2%)3.009s (~)0.510s305.96x
💻 LocalNext.js (Turbopack)2.664s3.009s0.345s306.36x
workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.606s (-35.5% 🟢)1.009s (-15.4% 🟢)0.403s1191.00x
🐘 PostgresExpress0.620s (-30.9% 🟢)1.009s (-3.1%)0.389s1191.02x
🐘 PostgresNext.js (Turbopack)1.001s1.477s0.476s821.65x
🌐 RedisNext.js (Turbopack)2.883s3.085s0.202s394.76x
💻 LocalNitro10.189s (-8.3% 🟢)10.855s (-6.9% 🟢)0.666s1216.83x
💻 LocalExpress11.127s (-1.4%)11.845s (-1.5%)0.718s1118.38x
💻 LocalNext.js (Turbopack)11.287s11.753s0.465s1118.64x
Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro0.140s (-28.6% 🟢)1.003s (~)0.009s (-26.3% 🟢)1.014s (~)0.874s101.00x
🐘 PostgresExpress0.164s (-24.1% 🟢)0.999s (+0.5%)0.001s (-8.3% 🟢)1.013s (~)0.848s101.17x
🐘 PostgresNitro0.167s (-30.3% 🟢)0.999s (+0.7%)0.001s (-21.4% 🟢)1.013s (~)0.846s101.19x
🌐 RedisNext.js (Turbopack)0.169s1.000s0.001s1.008s0.838s101.21x
💻 LocalNext.js (Turbopack)0.178s1.001s0.012s1.018s0.840s101.27x
💻 LocalExpress0.199s (-2.4%)1.003s (~)0.012s (-2.5%)1.017s (~)0.818s101.42x
🐘 PostgresNext.js (Turbopack)0.224s1.001s0.001s1.013s0.789s101.60x
stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)0.447s0.999s0.003s1.012s0.565s601.00x
🐘 PostgresNitro0.563s (-22.1% 🟢)1.005s (~)0.003s (-13.6% 🟢)1.023s (~)0.460s591.26x
💻 LocalNitro0.569s (-21.7% 🟢)1.008s (~)0.009s (-6.2% 🟢)1.021s (~)0.452s591.27x
🐘 PostgresExpress0.610s (-12.0% 🟢)1.006s (~)0.005s (+20.4% 🔺)1.028s (~)0.417s591.37x
💻 LocalNext.js (Turbopack)0.672s1.008s0.010s1.024s0.352s591.50x
💻 LocalExpress0.723s (-6.9% 🟢)1.009s (-1.7%)0.009s (-12.1% 🟢)1.022s (-1.8%)0.300s591.62x
🐘 PostgresNext.js (Turbopack)0.760s1.026s0.008s1.050s0.290s591.70x
10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)0.916s1.016s0.000s1.022s0.106s591.00x
🐘 PostgresNitro0.988s (+7.3% 🔺)1.384s (+30.2% 🔺)0.000s (-36.4% 🟢)1.398s (+29.3% 🔺)0.410s441.08x
🐘 PostgresExpress1.066s (+19.3% 🔺)1.664s (+53.7% 🔺)0.000s (+205.6% 🔺)1.677s (+51.8% 🔺)0.612s361.16x
💻 LocalNitro1.140s (-6.9% 🟢)2.017s (~)0.000s (-38.5% 🟢)2.020s (~)0.880s301.24x
🐘 PostgresNext.js (Turbopack)1.175s1.909s0.000s1.930s0.755s321.28x
💻 LocalExpress1.235s (-1.8%)2.020s (~)0.000s (-35.3% 🟢)2.023s (~)0.788s301.35x
💻 LocalNext.js (Turbopack)1.295s2.019s0.000s2.024s0.729s301.41x
fan-out fan-in 10 streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.886s (~)2.223s (~)0.000s (-100.0% 🟢)2.233s (~)0.347s271.00x
🌐 RedisNext.js (Turbopack)1.963s2.180s0.000s2.186s0.223s281.04x
🐘 PostgresExpress2.034s (+12.7% 🔺)2.441s (+16.3% 🔺)0.000s (+Infinity% 🔺)2.453s (+15.6% 🔺)0.418s251.08x
🐘 PostgresNext.js (Turbopack)2.302s3.005s0.000s3.015s0.713s201.22x
💻 LocalNitro3.350s (+0.6%)3.967s (~)0.000s (-60.0% 🟢)3.971s (~)0.621s161.78x
💻 LocalExpress3.508s (+1.9%)4.030s (-1.6%)0.001s (+37.5% 🔺)4.035s (-1.6%)0.527s151.86x
💻 LocalNext.js (Turbopack)3.784s4.387s0.000s4.392s0.608s142.01x

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNitro21/21
🐘 PostgresExpress12/21
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express🐘 Postgres20/21
Next.js (Turbopack)🌐 Redis15/21
Nitro🐘 Postgres17/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)

📋 View full workflow run


Some benchmark jobs failed:

  • Local: success
  • Postgres: success
  • Vercel: failure

Check the workflow run for details.

@TooTallNateGraphite App

Copy link
Copy Markdown
MemberAuthor

This stack of pull requests is managed by Graphite. Learn more about stacking.

@TooTallNate
TooTallNate marked this pull request as ready for review February 20, 2026 22:47
CopilotAI review requested due to automatic review settings February 20, 2026 22:47

CopilotAI 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.

Pull request overview

This PR extends the @workflow/swc-plugin transform to detect custom class serialization methods when the WORKFLOW_SERIALIZE / WORKFLOW_DESERIALIZE symbols are accessed via CommonJS require() patterns, aligning behavior with codebases that have been transpiled from ESM to CJS.

Changes:

  • Track CommonJS require() bindings during identifier collection (namespace const x = require(...) and destructured const { ... } = require(...)) and recognize x.WORKFLOW_SERIALIZE / x.WORKFLOW_DESERIALIZE as serialization symbols.
  • Add fixture coverage for both namespace-require and destructured-require inputs across workflow/step/client outputs.
  • Update the plugin spec and add a changeset entry documenting the new behavior.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 2 comments.

Show a summary per file
FileDescription
packages/swc-plugin-workflow/transform/src/lib.rsAdds require-binding tracking and member-expression symbol recognition for serialization detection.
packages/swc-plugin-workflow/transform/tests/fixture/custom-serialization-require-namespace/input.jsNew fixture input for namespace require() pattern.
packages/swc-plugin-workflow/transform/tests/fixture/custom-serialization-require-namespace/output-workflow.jsExpected workflow-mode output for namespace require() fixture.
packages/swc-plugin-workflow/transform/tests/fixture/custom-serialization-require-namespace/output-step.jsExpected step-mode output for namespace require() fixture.
packages/swc-plugin-workflow/transform/tests/fixture/custom-serialization-require-namespace/output-client.jsExpected client-mode output for namespace require() fixture.
packages/swc-plugin-workflow/transform/tests/fixture/custom-serialization-require-destructured/input.jsNew fixture input for destructured require() pattern.
packages/swc-plugin-workflow/transform/tests/fixture/custom-serialization-require-destructured/output-workflow.jsExpected workflow-mode output for destructured require() fixture.
packages/swc-plugin-workflow/transform/tests/fixture/custom-serialization-require-destructured/output-step.jsExpected step-mode output for destructured require() fixture.
packages/swc-plugin-workflow/transform/tests/fixture/custom-serialization-require-destructured/output-client.jsExpected client-mode output for destructured require() fixture.
packages/swc-plugin-workflow/spec.mdDocuments supported CommonJS require() symbol patterns.
.changeset/social-forks-tease.mdDeclares a patch release for the new detection behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment threadpackages/swc-plugin-workflow/transform/src/lib.rs Outdated
Comment threadpackages/swc-plugin-workflow/transform/src/lib.rs Outdated
…WC plugin
The SWC compiler plugin now detects classes with custom serialization methods (`WORKFLOW_SERIALIZE` / `WORKFLOW_DESERIALIZE`) when symbols are obtained via CommonJS `require()` calls, in addition to the existing ESM import and `Symbol.for()` patterns.
This handles code that has been pre-compiled from ESM to CommonJS by tools like TypeScript (tsc), esbuild, or tsup, which transform:
```ts
import { WORKFLOW_SERIALIZE } from "@workflow/serde"
```
into either:
```ts
const serde_1 = require("@workflow/serde") // namespace require
const { WORKFLOW_SERIALIZE } = require(...) // destructured require
```
Both patterns are now recognized during the identifier collection phase, and classes using them are properly registered with registerSerializationClass().
@TooTallNateTooTallNate changed the title Add CommonJS require() support for class serialization detection in SWC pluginAdd CommonJS require() support for class serialization detection in SWC pluginMar 23, 2026
…on_bindings helper
- Tighten is_require_call to require exactly one string literal argument,
rejecting dynamic requires and multi-arg calls.
- Extract track_serialization_bindings helper to deduplicate the require
tracking logic between Decl::Var and ExportDecl(Decl::Var) branches.
@TooTallNate
TooTallNateforce-pushed the 02-20-add_commonjs_require_support_for_class_serialization_detection_in_swc_plugin branch from 3648e46 to f461c59CompareMarch 23, 2026 14:01
@TooTallNate
TooTallNate requested a review from a team as a code ownerMarch 23, 2026 14:01
@TooTallNate
TooTallNate merged commit 992d768 into mainMar 23, 2026
98 of 103 checks passed
@TooTallNate
TooTallNate deleted the 02-20-add_commonjs_require_support_for_class_serialization_detection_in_swc_plugin branch March 23, 2026 18:30
@ghostghost mentioned this pull request Mar 23, 2026

@pranaygppranaygp 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.

Post-merge review — overall this is a clean, well-tested addition. A few observations below.

// Track CommonJS namespace require: const serde_1 = require("...")
if self.is_require_call(init) {
self.require_namespace_identifiers
.insert(ident.id.sym.to_string());

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.

Nit: This tracks everyrequire() call as a namespace identifier, regardless of the module being required. So const fs = require("fs") would cause fs.WORKFLOW_SERIALIZE to be treated as a serialization symbol if it ever appeared. This is unlikely to cause real bugs since nobody would name a property WORKFLOW_SERIALIZE on an unrelated module, but it is semantically imprecise.

Consider either:

  1. Restricting to known serde modules (e.g. checking the string arg contains @workflow/serde or similar), or
  2. Documenting this as an intentional trade-off (broad tracking is simpler and the false-positive risk is negligible).

match prop {
ObjectPatProp::Assign(assign) => {
// const { WORKFLOW_SERIALIZE } = require("...")
let name = assign.key.sym.to_string();

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.

Nit: The ObjectPatProp::Assign arm handles const { WORKFLOW_SERIALIZE } = require(...), but in the SWC AST this node actually represents const { WORKFLOW_SERIALIZE = defaultValue } = require(...) (a destructured binding with a default value). A plain shorthand const { WORKFLOW_SERIALIZE } = require(...) is typically parsed as ObjectPatProp::KeyValue where key and value ident are the same.

In practice this still works because SWC may represent the no-default shorthand either way depending on version, and the logic is correct for both paths. But worth noting that the Assign arm would also match const { WORKFLOW_SERIALIZE = someDefault } = require(...), which is an unusual but technically valid pattern.

@@ -0,0 +1,25 @@
// Test custom serialization with CommonJS destructured require
const { WORKFLOW_SERIALIZE, WORKFLOW_DESERIALIZE } = require("@workflow/serde");

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.

The spec documents renamed destructured require (const { WORKFLOW_SERIALIZE: WS } = require(...)) but there's no test fixture covering this case. Adding a custom-serialization-require-destructured-renamed fixture would ensure the ObjectPatProp::KeyValue branch stays correct.

pranaygp added a commit that referenced this pull request Mar 23, 2026
* origin/main:
[ai] Add experimental_context to DurableAgentOptions (#1489)
[ai] Expose configured tools on DurableAgent instances (#1488)
fix(builders): catch node builtin usage when entry fields diverge (#1455)
[web-shared] Fix timeline duration format and precision (#1482)
[cli] Add bulk cancel, --status filter, fix step JSON hydration (#1467)
[utils] Re-export parseName utilities and add workflow/observability module (#1453)
[o11y] Polish display when run data has expired (#1438)
Add CommonJS `require()` support for class serialization detection in SWC plugin (#1144)
fix(next): stabilize deferred canary e2e in nextjs workbenches (#1468)
[web] Support legacy newline-delimited stream format in `useStreamReader` (#1473)
Revert "Add support for calling start() inside workflow functions (#1133)" (#1475)
pranaygp added a commit that referenced this pull request Mar 24, 2026
…naygp-db9e68c1
* 'main' of https://github.com/vercel/workflow: (32 commits)
chore: bump @nestjs/* to ^11.1.17 (#1497)
chore: bump hono to ^4.12.8 (#1495)
Revert "Inline class serialization registration to fix 3rd-party package supp…" (#1493)
[world] Add stream pagination and metadata endpoints (#1470)
[cli] [world-local] Ensure update checks don't suggest upgrading from stable release to pre-releases (#1490)
Remove NestJS Vercel integration while in experimental phase (#1485)
feat: export semantic error types and add API reference docs (#1447)
feat: enforce max queue deliveries in handlers with graceful failure (#1344)
[world-postgres] Migrate client from `postgres.js` to `pg` (#1484)
Inline class serialization registration to fix 3rd-party package support (#1480)
[ai] Add experimental_context to DurableAgentOptions (#1489)
[ai] Expose configured tools on DurableAgent instances (#1488)
fix(builders): catch node builtin usage when entry fields diverge (#1455)
[web-shared] Fix timeline duration format and precision (#1482)
[cli] Add bulk cancel, --status filter, fix step JSON hydration (#1467)
[utils] Re-export parseName utilities and add workflow/observability module (#1453)
[o11y] Polish display when run data has expired (#1438)
Add CommonJS `require()` support for class serialization detection in SWC plugin (#1144)
fix(next): stabilize deferred canary e2e in nextjs workbenches (#1468)
[web] Support legacy newline-delimited stream format in `useStreamReader` (#1473)
...
TooTallNate added a commit that referenced this pull request Mar 24, 2026
The original PR #1480 was merged but reverted because it didn't include
updated fixtures for the CJS require patterns added by PR #1144
(custom-serialization-require-destructured and
custom-serialization-require-namespace). These fixtures still had the
old 'import { registerSerializationClass }' pattern instead of the
new inline IIFE.
TooTallNate added a commit that referenced this pull request Mar 24, 2026
…ort (v2) (#1503)
* Inline class serialization registration to fix 3rd-party package support (#1480)
* Inline class serialization registration to fix 3rd-party package support
The SWC plugin previously generated:
import { registerSerializationClass } from "workflow/internal/class-serialization";
registerSerializationClass("class//...", ClassName);
This broke for 3rd-party packages (e.g. @vercel/sandbox) that define
serializable classes but don't depend on the 'workflow' package. The
bare 'workflow' specifier is unresolvable from within node_modules of
a package that doesn't list it as a dependency.
Now the plugin generates a self-contained IIFE that uses
Symbol.for('workflow-class-registry') on globalThis directly, with
zero module dependencies:
(function(__wf_cls, __wf_id) {
var __wf_sym = Symbol.for("workflow-class-registry"),
__wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map());
__wf_reg.set(__wf_id, __wf_cls);
Object.defineProperty(__wf_cls, "classId", { ... });
})(ClassName, "class//...");
This is fully compatible with the existing deserialization side in
@workflow/core which reads from the same globalThis registry.
* Address review feedback: fix comment and update docstring
- Fix inaccurate IIFE comment in lib.rs: the second arg is the
generated class ID string, not the literal "classId"
- Update registerSerializationClass docstring to reflect that the
SWC plugin now inlines equivalent logic rather than importing it
* Update CJS require fixture outputs for inline class serialization
The original PR #1480 was merged but reverted because it didn't include
updated fixtures for the CJS require patterns added by PR #1144
(custom-serialization-require-destructured and
custom-serialization-require-namespace). These fixtures still had the
old 'import { registerSerializationClass }' pattern instead of the
new inline IIFE.
pranaygp pushed a commit to vercel/sandbox that referenced this pull request Mar 27, 2026
…ibility (#109)
## Summary
Makes `@vercel/sandbox` fully compatible with the Workflow DevKit
compiler so that `Sandbox`, `Command`, and `CommandFinished` instances
can be used directly inside `"use workflow"` functions — no wrapper step
functions needed.
Supersedes #58.
## Problem
When `@vercel/sandbox` is imported in a workflow context, the workflow
builder tries to bundle it into the workflow VM bundle (because it has
`WORKFLOW_SERIALIZE`/`WORKFLOW_DESERIALIZE` on its classes). This fails
because:
1. The SDK's public methods use Node.js APIs (`fs`, `stream`, `zlib`,
`undici`, etc.) which are forbidden in the workflow VM
2. The compiled `dist/index.js` was a single bundled file that hoisted
all Node.js imports to the top, making them impossible to tree-shake
3. The `Sandbox` and `Command` classes had a sync `client` getter that
directly referenced `APIClient`, pulling the entire HTTP client stack
into the module scope
## Solution
### 1. `"use step"` annotations on all public async methods
Added `"use step"` to all 22 public async methods across `Sandbox` (14),
`Command` (5), and `Snapshot` (3). The SWC plugin strips these method
bodies in workflow mode, replacing them with durable step proxies. This
eliminates all Node.js API references from the workflow bundle.
### 2. Async `ensureClient()` replaces sync `client` getter
The sync `get client()` getter directly referenced `APIClient`, which
pulls in `undici`, `zlib`, `tar-stream`, `jsonlines`, etc. Replaced
with:
```typescript
private async ensureClient(): Promise<APIClient> {
"use step";
if (this._client) return this._client;
const credentials = getSandboxCredentials();
this._client = new APIClient({ ... });
return this._client;
}
```
Since `ensureClient()` is itself `"use step"`, its body (including `new
APIClient(...)`) gets stripped in workflow mode. All instance methods
now call `const client = await this.ensureClient();` instead of
`this.client`.
### 3. `bundle: false` in tsdown config
Changed from single-file bundling to per-file output. This keeps Node.js
imports local to the files that use them, so after the SWC plugin strips
step method bodies, the now-unused Node.js imports can be eliminated by
esbuild's tree-shaking.
### 4. Workflow-code-runner example updated
- Removed `serverExternalPackages: ["@vercel/sandbox"]` from
`next.config.ts` (no longer needed)
- Updated `workflow` dependency to use a tarball that includes the
inline class serialization registration fix (vercel/workflow#1480)
## Testing
- `pnpm build` succeeds for the full monorepo (all 8 tasks)
- The `workflow-code-runner` example app builds successfully with all
workflow routes generated
- 22 `"use step"` directives survive compilation in both ESM and CJS
dist output
## Related
- vercel/workflow#1480 — Inline class serialization registration (fixes
SWC import resolution for 3rd-party packages)
- vercel/workflow#1481 — Build-time warning when
`serverExternalPackages` hides workflow-enabled packages
- vercel/workflow#1144 — CJS detection for serde symbols (pending
review)
- #58 — Previous attempt (superseded by this PR)
---------
Signed-off-by: Peter Wielander <mittgfu@gmail.com>
Co-authored-by: Peter Wielander <mittgfu@gmail.com>
Sign up for freeto 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.

4 participants

@TooTallNate@pranaygp@VaguelySerious
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Add CommonJS `require()` support for class serialization detection in SWC plugin by TooTallNate · Pull Request #1144 · vercel/workflow · GitHub
Skip to content

Add CommonJS require() support for class serialization detection in SWC plugin - #1144

Merged
TooTallNate merged 3 commits into
mainfrom
02-20-add_commonjs_require_support_for_class_serialization_detection_in_swc_plugin
Mar 23, 2026
Merged

Add CommonJS require() support for class serialization detection in SWC plugin#1144
TooTallNate merged 3 commits into
mainfrom
02-20-add_commonjs_require_support_for_class_serialization_detection_in_swc_plugin

Conversation

@TooTallNate

Copy link
Copy Markdown
Member

The SWC compiler plugin now detects classes with custom serialization methods (WORKFLOW_SERIALIZE / WORKFLOW_DESERIALIZE) when symbols are obtained via CommonJS require() calls, in addition to the existing ESM import and Symbol.for() patterns.

This handles code that has been pre-compiled from ESM to CommonJS by tools like TypeScript (tsc), esbuild, or tsup, which transform:

import{WORKFLOW_SERIALIZE}from"@workflow/serde"

into either:

constserde_1=require("@workflow/serde")// namespace requireconst{WORKFLOW_SERIALIZE}=require(...)// destructured require

Both patterns are now recognized during the identifier collection phase, and classes using them are properly registered with registerSerializationClass().

@changeset-bot

changeset-botBot commented Feb 20, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 14ea197

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

This PR includes changesets to release 17 packages
NameType
@workflow/swc-pluginPatch
@workflow/astroPatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nestPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
workflowPatch
@workflow/vitePatch
@workflow/vitestPatch
@workflow/world-testingPatch
@workflow/nuxtPatch
@workflow/aiPatch
@workflow/corePatch
@workflow/web-sharedPatch

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

@vercel

vercelBot commented Feb 20, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Feb 20, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production780067847
✅ 💻 Local Development7820142924
✅ 📦 Local Production7820142924
✅ 🐘 Local Postgres7820142924
✅ 🪟 Windows720577
❌ 🌍 Community Worlds1185621195
✅ 📋 Other198033231
Total3514565524122

❌ Failed Tests

🌍 Community Worlds (56 failed)

mongodb (3 failed):

  • hookWorkflow is not resumable via public webhook endpoint | wrun_01KMDYE4RWSYQ6538DVH70M5P0
  • webhookWorkflow | wrun_01KMDYED4N9B1F4PF1X6F86J26
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously | wrun_01KMDYMVS1CFTACB4081WS8K9V

redis (2 failed):

  • hookWorkflow is not resumable via public webhook endpoint | wrun_01KMDYE4RWSYQ6538DVH70M5P0
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously | wrun_01KMDYMVS1CFTACB4081WS8K9V

turso (51 failed):

  • addTenWorkflow | wrun_01KMDYCYXATZBJYFAPPKVRZKDZ
  • addTenWorkflow | wrun_01KMDYCYXATZBJYFAPPKVRZKDZ
  • wellKnownAgentWorkflow (.well-known/agent) | wrun_01KMDYEKVFEVFMCXC32DMPJ3KB
  • should work with react rendering in step
  • promiseAllWorkflow | wrun_01KMDYD5F67YMHR6KTJF5YFYM3
  • promiseRaceWorkflow | wrun_01KMDYDBXWV7065WDZ655A6X2Y
  • promiseAnyWorkflow | wrun_01KMDYDEFBS29N1Y4E04XS0PHS
  • importedStepOnlyWorkflow | wrun_01KMDYF31ZMHDYKRR9870BQJFE
  • hookWorkflow | wrun_01KMDYDTDYK5ZZTVGBV7PBPJAZ
  • hookWorkflow is not resumable via public webhook endpoint | wrun_01KMDYE4RWSYQ6538DVH70M5P0
  • webhookWorkflow | wrun_01KMDYED4N9B1F4PF1X6F86J26
  • sleepingWorkflow | wrun_01KMDYEKNPTWHR8GD4VJXRHSWR
  • parallelSleepWorkflow | wrun_01KMDYF1MMFSRK8683TXYGPEFF
  • nullByteWorkflow | wrun_01KMDYF6AS6SP633VT5B7JZ0Q8
  • workflowAndStepMetadataWorkflow | wrun_01KMDYF8B1WBHNCPZKHFYCH5G0
  • fetchWorkflow | wrun_01KMDYH6MPPT3CPGBXX02XNQTP
  • promiseRaceStressTestWorkflow | wrun_01KMDYH9Y93SDT6D7TEE6CMKHT
  • error handling error propagation workflow errors nested function calls preserve message and stack trace
  • error handling error propagation workflow errors cross-file imports preserve message and stack trace
  • error handling error propagation step errors basic step error preserves message and stack trace
  • error handling error propagation step errors cross-file step error preserves message and function names in stack
  • error handling retry behavior regular Error retries until success
  • error handling retry behavior FatalError fails immediately without retries
  • error handling retry behavior RetryableError respects custom retryAfter delay
  • error handling retry behavior maxRetries=0 disables retries
  • error handling catchability FatalError can be caught and detected with FatalError.is()
  • hookCleanupTestWorkflow - hook token reuse after workflow completion | wrun_01KMDYM7FJ0MWDXPS56VM7PF4D
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously | wrun_01KMDYMVS1CFTACB4081WS8K9V
  • hookDisposeTestWorkflow - hook token reuse after explicit disposal while workflow still running | wrun_01KMDYNGM0E0ZXFK8E2CQ9CYZV
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars) | wrun_01KMDYP5F3H7NXZ9W494MXF8EQ
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument | wrun_01KMDYPFQ0MK8D4B36398E2S6A
  • closureVariableWorkflow - nested step functions with closure variables | wrun_01KMDYPN9T3F9HBR5W1TQSBAVH
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step | wrun_01KMDYPQBVNH7GG5Y8ZDS8B1P3
  • health check (queue-based) - workflow and step endpoints respond to health check messages
  • pathsAliasWorkflow - TypeScript path aliases resolve correctly | wrun_01KMDYQ7ZKSHZ1DF03RZ64NGXD
  • Calculator.calculate - static workflow method using static step methods from another class | wrun_01KMDYQEMCVB21XR91WVHJEGK4
  • AllInOneService.processNumber - static workflow method using sibling static step methods | wrun_01KMDYQP3ST1XWR4FTRNPGNYE4
  • ChainableService.processWithThis - static step methods using this to reference the class | wrun_01KMDYQWMSK6ZYMFNY7ME546A9
  • thisSerializationWorkflow - step function invoked with .call() and .apply() | wrun_01KMDYR36AW71R74TKNQEA26QC
  • customSerializationWorkflow - custom class serialization with WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE | wrun_01KMDYR9J9RVJCAWSQG76N5656
  • instanceMethodStepWorkflow - instance methods with "use step" directive | wrun_01KMDYRH7WBXH2XGDZPC5CXB49
  • crossContextSerdeWorkflow - classes defined in step code are deserializable in workflow context | wrun_01KMDYRVMSG67Q29E0JT62SCPB
  • stepFunctionAsStartArgWorkflow - step function reference passed as start() argument | wrun_01KMDYS38DF4PJE3SJHH7YV0GQ
  • cancelRun - cancelling a running workflow | wrun_01KMDYSB4MVW6TNAVS70ZDC36V
  • cancelRun via CLI - cancelling a running workflow | wrun_01KMDYSM88CM0VKBMVABG1ZRM0
  • pages router addTenWorkflow via pages router
  • pages router promiseAllWorkflow via pages router
  • pages router sleepingWorkflow via pages router
  • hookWithSleepWorkflow - hook payloads delivered correctly with concurrent sleep | wrun_01KMDYT02V9K533MN8PA31072V
  • sleepInLoopWorkflow - sleep inside loop with steps actually delays each iteration | wrun_01KMDYTKS4H2ZFNHJC3AH7D70B
  • sleepWithSequentialStepsWorkflow - sequential steps work with concurrent sleep (control) | wrun_01KMDYTZMDM1Q8370F5R6NNHJV

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro7007
✅ example7007
✅ express7007
✅ fastify7007
✅ hono7007
✅ nextjs-turbopack7502
✅ nextjs-webpack7502
✅ nitro7007
✅ nuxt7007
✅ sveltekit7007
✅ vite7007
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable66011
✅ express-stable66011
✅ fastify-stable66011
✅ hono-stable66011
✅ nextjs-turbopack-canary55022
✅ nextjs-turbopack-stable7205
✅ nextjs-webpack-canary55022
✅ nextjs-webpack-stable7205
✅ nitro-stable66011
✅ nuxt-stable66011
✅ sveltekit-stable66011
✅ vite-stable66011
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable66011
✅ express-stable66011
✅ fastify-stable66011
✅ hono-stable66011
✅ nextjs-turbopack-canary55022
✅ nextjs-turbopack-stable7205
✅ nextjs-webpack-canary55022
✅ nextjs-webpack-stable7205
✅ nitro-stable66011
✅ nuxt-stable66011
✅ sveltekit-stable66011
✅ vite-stable66011
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable66011
✅ express-stable66011
✅ fastify-stable66011
✅ hono-stable66011
✅ nextjs-turbopack-canary55022
✅ nextjs-turbopack-stable7205
✅ nextjs-webpack-canary55022
✅ nextjs-webpack-stable7205
✅ nitro-stable66011
✅ nuxt-stable66011
✅ sveltekit-stable66011
✅ vite-stable66011
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack7205
❌ 🌍 Community Worlds
AppPassedFailedSkipped
✅ mongodb-dev302
❌ mongodb5235
✅ redis-dev302
❌ redis5325
✅ turso-dev302
❌ turso4515
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable66011
✅ e2e-local-postgres-nest-stable66011
✅ e2e-local-prod-nest-stable66011

📋 View full workflow run

@github-actions

github-actionsBot commented Feb 20, 2026

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

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

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro0.037s (-21.3% 🟢)1.005s (~)0.967s101.00x
💻 LocalExpress0.043s (-4.5%)1.005s (~)0.962s101.14x
🐘 PostgresNitro0.047s (-31.9% 🟢)1.012s (~)0.965s101.26x
🐘 PostgresExpress0.048s (-32.2% 🟢)1.012s (-0.5%)0.964s101.29x
💻 LocalNext.js (Turbopack)0.051s1.006s0.956s101.35x
🌐 RedisNext.js (Turbopack)0.054s1.005s0.952s101.44x
🐘 PostgresNext.js (Turbopack)0.062s1.012s0.950s101.65x
workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.097s (-2.8%)2.005s (~)0.908s101.00x
🌐 RedisNext.js (Turbopack)1.120s2.007s0.887s101.02x
🐘 PostgresNitro1.124s (-1.6%)2.012s (~)0.888s101.02x
💻 LocalNext.js (Turbopack)1.124s2.006s0.883s101.02x
🐘 PostgresExpress1.126s (-1.1%)2.012s (~)0.886s101.03x
💻 LocalExpress1.131s (~)2.005s (~)0.874s101.03x
🐘 PostgresNext.js (Turbopack)1.143s2.013s0.869s101.04x
workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro10.635s (-2.4%)11.023s (~)0.388s31.00x
🌐 RedisNext.js (Turbopack)10.680s11.025s0.345s31.00x
🐘 PostgresNitro10.689s (-2.4%)11.036s (~)0.348s31.01x
🐘 PostgresExpress10.705s (-1.8%)11.038s (~)0.333s31.01x
💻 LocalNext.js (Turbopack)10.847s11.025s0.178s31.02x
💻 LocalExpress10.934s (~)11.023s (~)0.089s31.03x
🐘 PostgresNext.js (Turbopack)10.996s11.382s0.386s31.03x
workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)14.091s15.033s0.941s41.00x
🐘 PostgresExpress14.123s (-3.1%)15.042s (~)0.919s41.00x
🐘 PostgresNitro14.129s (-4.7%)15.039s (~)0.910s41.00x
💻 LocalNitro14.183s (-4.9%)15.029s (~)0.847s41.01x
🐘 PostgresNext.js (Turbopack)14.580s15.049s0.469s41.03x
💻 LocalNext.js (Turbopack)14.703s15.031s0.328s41.04x
💻 LocalExpress14.911s (-0.6%)15.029s (-3.2%)0.118s41.06x
workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)13.030s13.457s0.427s71.00x
🐘 PostgresExpress13.096s (-6.5% 🟢)14.038s (-3.0%)0.942s71.01x
🐘 PostgresNitro13.119s (-8.7% 🟢)14.038s (-6.7% 🟢)0.919s71.01x
🐘 PostgresNext.js (Turbopack)14.145s14.759s0.613s71.09x
💻 LocalNitro14.789s (-10.1% 🟢)15.026s (-11.8% 🟢)0.237s61.14x
💻 LocalNext.js (Turbopack)16.157s17.033s0.875s61.24x
💻 LocalExpress16.514s (-1.0%)17.029s (~)0.516s61.27x
Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.228s (-3.0%)2.011s (~)0.783s151.00x
🐘 PostgresNitro1.231s (-4.8%)2.011s (~)0.780s151.00x
🌐 RedisNext.js (Turbopack)1.266s2.007s0.741s151.03x
🐘 PostgresNext.js (Turbopack)1.286s2.011s0.725s151.05x
💻 LocalNitro1.472s (-0.8%)2.005s (~)0.534s151.20x
💻 LocalExpress1.503s (~)2.006s (~)0.503s151.22x
💻 LocalNext.js (Turbopack)1.524s2.006s0.482s151.24x
Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express2.341s (-4.9%)3.012s (~)0.671s101.00x
🐘 PostgresNitro2.353s (-4.8%)3.013s (~)0.660s101.01x
🌐 RedisNext.js (Turbopack)2.478s3.009s0.531s101.06x
🐘 PostgresNext.js (Turbopack)2.484s3.015s0.531s101.06x
💻 LocalNitro2.551s (-11.2% 🟢)3.007s (-3.2%)0.456s101.09x
💻 LocalExpress2.970s (-0.9%)3.308s (-7.2% 🟢)0.338s101.27x
💻 LocalNext.js (Turbopack)2.995s3.309s0.314s101.28x
Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express3.519s (-1.9%)4.014s (~)0.495s81.00x
🐘 PostgresNitro3.523s (-3.0%)4.015s (~)0.492s81.00x
🐘 PostgresNext.js (Turbopack)3.844s4.017s0.173s81.09x
🌐 RedisNext.js (Turbopack)4.117s4.727s0.610s71.17x
💻 LocalNitro7.025s (-13.5% 🟢)7.514s (-14.3% 🟢)0.489s42.00x
💻 LocalExpress8.144s (-3.9%)8.769s (-2.8%)0.625s42.31x
💻 LocalNext.js (Turbopack)8.563s9.019s0.456s42.43x
Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.207s (-4.1%)2.011s (~)0.803s151.00x
🐘 PostgresNitro1.216s (-4.7%)2.010s (~)0.794s151.01x
🌐 RedisNext.js (Turbopack)1.252s2.007s0.755s151.04x
🐘 PostgresNext.js (Turbopack)1.254s2.011s0.757s151.04x
💻 LocalNitro1.484s (-4.9%)2.006s (~)0.522s151.23x
💻 LocalNext.js (Turbopack)1.515s2.005s0.490s151.25x
💻 LocalExpress1.550s (+0.9%)2.005s (~)0.455s151.28x
Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro2.342s (-4.7%)3.012s (~)0.670s101.00x
🐘 PostgresExpress2.353s (-4.0%)3.012s (~)0.659s101.00x
🐘 PostgresNext.js (Turbopack)2.474s3.013s0.539s101.06x
🌐 RedisNext.js (Turbopack)2.498s3.009s0.511s101.07x
💻 LocalNitro2.924s (-3.4%)3.208s (-12.7% 🟢)0.283s101.25x
💻 LocalNext.js (Turbopack)3.012s3.675s0.663s91.29x
💻 LocalExpress3.090s (+1.9%)3.885s (+3.4%)0.795s81.32x
Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express3.515s (-2.3%)4.013s (~)0.497s81.00x
🐘 PostgresNitro3.518s (-2.7%)4.014s (~)0.496s81.00x
🐘 PostgresNext.js (Turbopack)3.841s4.018s0.177s81.09x
🌐 RedisNext.js (Turbopack)4.031s4.583s0.553s71.15x
💻 LocalNitro7.511s (-13.1% 🟢)8.016s (-11.2% 🟢)0.506s42.14x
💻 LocalExpress8.608s (-5.0%)9.020s (-7.7% 🟢)0.412s42.45x
💻 LocalNext.js (Turbopack)9.214s9.772s0.557s42.62x
workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)0.601s1.005s0.404s601.00x
🐘 PostgresExpress0.642s (-25.8% 🟢)1.009s (-1.7%)0.366s601.07x
🐘 PostgresNitro0.657s (-26.8% 🟢)1.008s (-3.4%)0.351s601.09x
💻 LocalNitro0.672s (-29.7% 🟢)1.004s (-1.7%)0.332s601.12x
🐘 PostgresNext.js (Turbopack)0.870s1.009s0.140s601.45x
💻 LocalNext.js (Turbopack)0.878s1.057s0.179s571.46x
💻 LocalExpress0.973s (-1.0%)1.114s (+1.7%)0.140s551.62x
workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)1.460s2.006s0.546s451.00x
🐘 PostgresExpress1.553s (-26.6% 🟢)2.010s (-33.3% 🟢)0.457s451.06x
🐘 PostgresNitro1.563s (-27.7% 🟢)2.032s (-31.8% 🟢)0.470s451.07x
🐘 PostgresNext.js (Turbopack)2.127s2.948s0.821s311.46x
💻 LocalNitro2.193s (-25.8% 🟢)3.007s (-3.3%)0.815s301.50x
💻 LocalNext.js (Turbopack)2.736s3.008s0.272s301.87x
💻 LocalExpress2.966s (-1.6%)3.222s (-9.2% 🟢)0.255s282.03x
workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)3.077s3.437s0.360s351.00x
🐘 PostgresNitro3.153s (-27.3% 🟢)4.012s (-20.0% 🟢)0.859s301.02x
🐘 PostgresExpress3.188s (-25.2% 🟢)4.012s (-19.3% 🟢)0.824s301.04x
🐘 PostgresNext.js (Turbopack)4.345s5.015s0.670s241.41x
💻 LocalNitro7.218s (-20.1% 🟢)7.827s (-17.4% 🟢)0.610s162.35x
💻 LocalNext.js (Turbopack)8.797s9.232s0.434s142.86x
💻 LocalExpress8.995s (-1.7%)9.479s (-3.1%)0.484s132.92x
workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.243s (-19.7% 🟢)1.008s (~)0.766s601.00x
🐘 PostgresNitro0.245s (-16.9% 🟢)1.009s (~)0.764s601.01x
🌐 RedisNext.js (Turbopack)0.260s1.005s0.744s601.07x
🐘 PostgresNext.js (Turbopack)0.287s1.010s0.723s601.18x
💻 LocalNitro0.576s (-3.7%)1.004s (~)0.428s602.37x
💻 LocalExpress0.581s (-3.7%)1.004s (~)0.424s602.39x
💻 LocalNext.js (Turbopack)0.585s1.004s0.420s602.41x
workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.419s (-20.5% 🟢)1.009s (~)0.589s901.00x
🐘 PostgresNitro0.424s (-21.4% 🟢)1.009s (~)0.585s901.01x
🐘 PostgresNext.js (Turbopack)0.590s1.033s0.443s881.41x
🌐 RedisNext.js (Turbopack)1.194s2.006s0.812s452.85x
💻 LocalNitro2.345s (-5.2% 🟢)3.007s (~)0.662s305.59x
💻 LocalExpress2.498s (-3.2%)3.009s (~)0.510s305.96x
💻 LocalNext.js (Turbopack)2.664s3.009s0.345s306.36x
workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.606s (-35.5% 🟢)1.009s (-15.4% 🟢)0.403s1191.00x
🐘 PostgresExpress0.620s (-30.9% 🟢)1.009s (-3.1%)0.389s1191.02x
🐘 PostgresNext.js (Turbopack)1.001s1.477s0.476s821.65x
🌐 RedisNext.js (Turbopack)2.883s3.085s0.202s394.76x
💻 LocalNitro10.189s (-8.3% 🟢)10.855s (-6.9% 🟢)0.666s1216.83x
💻 LocalExpress11.127s (-1.4%)11.845s (-1.5%)0.718s1118.38x
💻 LocalNext.js (Turbopack)11.287s11.753s0.465s1118.64x
Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro0.140s (-28.6% 🟢)1.003s (~)0.009s (-26.3% 🟢)1.014s (~)0.874s101.00x
🐘 PostgresExpress0.164s (-24.1% 🟢)0.999s (+0.5%)0.001s (-8.3% 🟢)1.013s (~)0.848s101.17x
🐘 PostgresNitro0.167s (-30.3% 🟢)0.999s (+0.7%)0.001s (-21.4% 🟢)1.013s (~)0.846s101.19x
🌐 RedisNext.js (Turbopack)0.169s1.000s0.001s1.008s0.838s101.21x
💻 LocalNext.js (Turbopack)0.178s1.001s0.012s1.018s0.840s101.27x
💻 LocalExpress0.199s (-2.4%)1.003s (~)0.012s (-2.5%)1.017s (~)0.818s101.42x
🐘 PostgresNext.js (Turbopack)0.224s1.001s0.001s1.013s0.789s101.60x
stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)0.447s0.999s0.003s1.012s0.565s601.00x
🐘 PostgresNitro0.563s (-22.1% 🟢)1.005s (~)0.003s (-13.6% 🟢)1.023s (~)0.460s591.26x
💻 LocalNitro0.569s (-21.7% 🟢)1.008s (~)0.009s (-6.2% 🟢)1.021s (~)0.452s591.27x
🐘 PostgresExpress0.610s (-12.0% 🟢)1.006s (~)0.005s (+20.4% 🔺)1.028s (~)0.417s591.37x
💻 LocalNext.js (Turbopack)0.672s1.008s0.010s1.024s0.352s591.50x
💻 LocalExpress0.723s (-6.9% 🟢)1.009s (-1.7%)0.009s (-12.1% 🟢)1.022s (-1.8%)0.300s591.62x
🐘 PostgresNext.js (Turbopack)0.760s1.026s0.008s1.050s0.290s591.70x
10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)0.916s1.016s0.000s1.022s0.106s591.00x
🐘 PostgresNitro0.988s (+7.3% 🔺)1.384s (+30.2% 🔺)0.000s (-36.4% 🟢)1.398s (+29.3% 🔺)0.410s441.08x
🐘 PostgresExpress1.066s (+19.3% 🔺)1.664s (+53.7% 🔺)0.000s (+205.6% 🔺)1.677s (+51.8% 🔺)0.612s361.16x
💻 LocalNitro1.140s (-6.9% 🟢)2.017s (~)0.000s (-38.5% 🟢)2.020s (~)0.880s301.24x
🐘 PostgresNext.js (Turbopack)1.175s1.909s0.000s1.930s0.755s321.28x
💻 LocalExpress1.235s (-1.8%)2.020s (~)0.000s (-35.3% 🟢)2.023s (~)0.788s301.35x
💻 LocalNext.js (Turbopack)1.295s2.019s0.000s2.024s0.729s301.41x
fan-out fan-in 10 streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.886s (~)2.223s (~)0.000s (-100.0% 🟢)2.233s (~)0.347s271.00x
🌐 RedisNext.js (Turbopack)1.963s2.180s0.000s2.186s0.223s281.04x
🐘 PostgresExpress2.034s (+12.7% 🔺)2.441s (+16.3% 🔺)0.000s (+Infinity% 🔺)2.453s (+15.6% 🔺)0.418s251.08x
🐘 PostgresNext.js (Turbopack)2.302s3.005s0.000s3.015s0.713s201.22x
💻 LocalNitro3.350s (+0.6%)3.967s (~)0.000s (-60.0% 🟢)3.971s (~)0.621s161.78x
💻 LocalExpress3.508s (+1.9%)4.030s (-1.6%)0.001s (+37.5% 🔺)4.035s (-1.6%)0.527s151.86x
💻 LocalNext.js (Turbopack)3.784s4.387s0.000s4.392s0.608s142.01x

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNitro21/21
🐘 PostgresExpress12/21
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express🐘 Postgres20/21
Next.js (Turbopack)🌐 Redis15/21
Nitro🐘 Postgres17/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)

📋 View full workflow run


Some benchmark jobs failed:

  • Local: success
  • Postgres: success
  • Vercel: failure

Check the workflow run for details.

@TooTallNateGraphite App

Copy link
Copy Markdown
MemberAuthor

This stack of pull requests is managed by Graphite. Learn more about stacking.

@TooTallNate
TooTallNate marked this pull request as ready for review February 20, 2026 22:47
CopilotAI review requested due to automatic review settings February 20, 2026 22:47

CopilotAI 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.

Pull request overview

This PR extends the @workflow/swc-plugin transform to detect custom class serialization methods when the WORKFLOW_SERIALIZE / WORKFLOW_DESERIALIZE symbols are accessed via CommonJS require() patterns, aligning behavior with codebases that have been transpiled from ESM to CJS.

Changes:

  • Track CommonJS require() bindings during identifier collection (namespace const x = require(...) and destructured const { ... } = require(...)) and recognize x.WORKFLOW_SERIALIZE / x.WORKFLOW_DESERIALIZE as serialization symbols.
  • Add fixture coverage for both namespace-require and destructured-require inputs across workflow/step/client outputs.
  • Update the plugin spec and add a changeset entry documenting the new behavior.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 2 comments.

Show a summary per file
FileDescription
packages/swc-plugin-workflow/transform/src/lib.rsAdds require-binding tracking and member-expression symbol recognition for serialization detection.
packages/swc-plugin-workflow/transform/tests/fixture/custom-serialization-require-namespace/input.jsNew fixture input for namespace require() pattern.
packages/swc-plugin-workflow/transform/tests/fixture/custom-serialization-require-namespace/output-workflow.jsExpected workflow-mode output for namespace require() fixture.
packages/swc-plugin-workflow/transform/tests/fixture/custom-serialization-require-namespace/output-step.jsExpected step-mode output for namespace require() fixture.
packages/swc-plugin-workflow/transform/tests/fixture/custom-serialization-require-namespace/output-client.jsExpected client-mode output for namespace require() fixture.
packages/swc-plugin-workflow/transform/tests/fixture/custom-serialization-require-destructured/input.jsNew fixture input for destructured require() pattern.
packages/swc-plugin-workflow/transform/tests/fixture/custom-serialization-require-destructured/output-workflow.jsExpected workflow-mode output for destructured require() fixture.
packages/swc-plugin-workflow/transform/tests/fixture/custom-serialization-require-destructured/output-step.jsExpected step-mode output for destructured require() fixture.
packages/swc-plugin-workflow/transform/tests/fixture/custom-serialization-require-destructured/output-client.jsExpected client-mode output for destructured require() fixture.
packages/swc-plugin-workflow/spec.mdDocuments supported CommonJS require() symbol patterns.
.changeset/social-forks-tease.mdDeclares a patch release for the new detection behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment threadpackages/swc-plugin-workflow/transform/src/lib.rs Outdated
Comment threadpackages/swc-plugin-workflow/transform/src/lib.rs Outdated
…WC plugin
The SWC compiler plugin now detects classes with custom serialization methods (`WORKFLOW_SERIALIZE` / `WORKFLOW_DESERIALIZE`) when symbols are obtained via CommonJS `require()` calls, in addition to the existing ESM import and `Symbol.for()` patterns.
This handles code that has been pre-compiled from ESM to CommonJS by tools like TypeScript (tsc), esbuild, or tsup, which transform:
```ts
import { WORKFLOW_SERIALIZE } from "@workflow/serde"
```
into either:
```ts
const serde_1 = require("@workflow/serde") // namespace require
const { WORKFLOW_SERIALIZE } = require(...) // destructured require
```
Both patterns are now recognized during the identifier collection phase, and classes using them are properly registered with registerSerializationClass().
@TooTallNateTooTallNate changed the title Add CommonJS require() support for class serialization detection in SWC pluginAdd CommonJS require() support for class serialization detection in SWC pluginMar 23, 2026
…on_bindings helper
- Tighten is_require_call to require exactly one string literal argument,
rejecting dynamic requires and multi-arg calls.
- Extract track_serialization_bindings helper to deduplicate the require
tracking logic between Decl::Var and ExportDecl(Decl::Var) branches.
@TooTallNate
TooTallNateforce-pushed the 02-20-add_commonjs_require_support_for_class_serialization_detection_in_swc_plugin branch from 3648e46 to f461c59CompareMarch 23, 2026 14:01
@TooTallNate
TooTallNate requested a review from a team as a code ownerMarch 23, 2026 14:01
@TooTallNate
TooTallNate merged commit 992d768 into mainMar 23, 2026
98 of 103 checks passed
@TooTallNate
TooTallNate deleted the 02-20-add_commonjs_require_support_for_class_serialization_detection_in_swc_plugin branch March 23, 2026 18:30
@ghostghost mentioned this pull request Mar 23, 2026

@pranaygppranaygp 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.

Post-merge review — overall this is a clean, well-tested addition. A few observations below.

// Track CommonJS namespace require: const serde_1 = require("...")
if self.is_require_call(init) {
self.require_namespace_identifiers
.insert(ident.id.sym.to_string());

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.

Nit: This tracks everyrequire() call as a namespace identifier, regardless of the module being required. So const fs = require("fs") would cause fs.WORKFLOW_SERIALIZE to be treated as a serialization symbol if it ever appeared. This is unlikely to cause real bugs since nobody would name a property WORKFLOW_SERIALIZE on an unrelated module, but it is semantically imprecise.

Consider either:

  1. Restricting to known serde modules (e.g. checking the string arg contains @workflow/serde or similar), or
  2. Documenting this as an intentional trade-off (broad tracking is simpler and the false-positive risk is negligible).

match prop {
ObjectPatProp::Assign(assign) => {
// const { WORKFLOW_SERIALIZE } = require("...")
let name = assign.key.sym.to_string();

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.

Nit: The ObjectPatProp::Assign arm handles const { WORKFLOW_SERIALIZE } = require(...), but in the SWC AST this node actually represents const { WORKFLOW_SERIALIZE = defaultValue } = require(...) (a destructured binding with a default value). A plain shorthand const { WORKFLOW_SERIALIZE } = require(...) is typically parsed as ObjectPatProp::KeyValue where key and value ident are the same.

In practice this still works because SWC may represent the no-default shorthand either way depending on version, and the logic is correct for both paths. But worth noting that the Assign arm would also match const { WORKFLOW_SERIALIZE = someDefault } = require(...), which is an unusual but technically valid pattern.

@@ -0,0 +1,25 @@
// Test custom serialization with CommonJS destructured require
const { WORKFLOW_SERIALIZE, WORKFLOW_DESERIALIZE } = require("@workflow/serde");

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.

The spec documents renamed destructured require (const { WORKFLOW_SERIALIZE: WS } = require(...)) but there's no test fixture covering this case. Adding a custom-serialization-require-destructured-renamed fixture would ensure the ObjectPatProp::KeyValue branch stays correct.

pranaygp added a commit that referenced this pull request Mar 23, 2026
* origin/main:
[ai] Add experimental_context to DurableAgentOptions (#1489)
[ai] Expose configured tools on DurableAgent instances (#1488)
fix(builders): catch node builtin usage when entry fields diverge (#1455)
[web-shared] Fix timeline duration format and precision (#1482)
[cli] Add bulk cancel, --status filter, fix step JSON hydration (#1467)
[utils] Re-export parseName utilities and add workflow/observability module (#1453)
[o11y] Polish display when run data has expired (#1438)
Add CommonJS `require()` support for class serialization detection in SWC plugin (#1144)
fix(next): stabilize deferred canary e2e in nextjs workbenches (#1468)
[web] Support legacy newline-delimited stream format in `useStreamReader` (#1473)
Revert "Add support for calling start() inside workflow functions (#1133)" (#1475)
pranaygp added a commit that referenced this pull request Mar 24, 2026
…naygp-db9e68c1
* 'main' of https://github.com/vercel/workflow: (32 commits)
chore: bump @nestjs/* to ^11.1.17 (#1497)
chore: bump hono to ^4.12.8 (#1495)
Revert "Inline class serialization registration to fix 3rd-party package supp…" (#1493)
[world] Add stream pagination and metadata endpoints (#1470)
[cli] [world-local] Ensure update checks don't suggest upgrading from stable release to pre-releases (#1490)
Remove NestJS Vercel integration while in experimental phase (#1485)
feat: export semantic error types and add API reference docs (#1447)
feat: enforce max queue deliveries in handlers with graceful failure (#1344)
[world-postgres] Migrate client from `postgres.js` to `pg` (#1484)
Inline class serialization registration to fix 3rd-party package support (#1480)
[ai] Add experimental_context to DurableAgentOptions (#1489)
[ai] Expose configured tools on DurableAgent instances (#1488)
fix(builders): catch node builtin usage when entry fields diverge (#1455)
[web-shared] Fix timeline duration format and precision (#1482)
[cli] Add bulk cancel, --status filter, fix step JSON hydration (#1467)
[utils] Re-export parseName utilities and add workflow/observability module (#1453)
[o11y] Polish display when run data has expired (#1438)
Add CommonJS `require()` support for class serialization detection in SWC plugin (#1144)
fix(next): stabilize deferred canary e2e in nextjs workbenches (#1468)
[web] Support legacy newline-delimited stream format in `useStreamReader` (#1473)
...
TooTallNate added a commit that referenced this pull request Mar 24, 2026
The original PR #1480 was merged but reverted because it didn't include
updated fixtures for the CJS require patterns added by PR #1144
(custom-serialization-require-destructured and
custom-serialization-require-namespace). These fixtures still had the
old 'import { registerSerializationClass }' pattern instead of the
new inline IIFE.
TooTallNate added a commit that referenced this pull request Mar 24, 2026
…ort (v2) (#1503)
* Inline class serialization registration to fix 3rd-party package support (#1480)
* Inline class serialization registration to fix 3rd-party package support
The SWC plugin previously generated:
import { registerSerializationClass } from "workflow/internal/class-serialization";
registerSerializationClass("class//...", ClassName);
This broke for 3rd-party packages (e.g. @vercel/sandbox) that define
serializable classes but don't depend on the 'workflow' package. The
bare 'workflow' specifier is unresolvable from within node_modules of
a package that doesn't list it as a dependency.
Now the plugin generates a self-contained IIFE that uses
Symbol.for('workflow-class-registry') on globalThis directly, with
zero module dependencies:
(function(__wf_cls, __wf_id) {
var __wf_sym = Symbol.for("workflow-class-registry"),
__wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map());
__wf_reg.set(__wf_id, __wf_cls);
Object.defineProperty(__wf_cls, "classId", { ... });
})(ClassName, "class//...");
This is fully compatible with the existing deserialization side in
@workflow/core which reads from the same globalThis registry.
* Address review feedback: fix comment and update docstring
- Fix inaccurate IIFE comment in lib.rs: the second arg is the
generated class ID string, not the literal "classId"
- Update registerSerializationClass docstring to reflect that the
SWC plugin now inlines equivalent logic rather than importing it
* Update CJS require fixture outputs for inline class serialization
The original PR #1480 was merged but reverted because it didn't include
updated fixtures for the CJS require patterns added by PR #1144
(custom-serialization-require-destructured and
custom-serialization-require-namespace). These fixtures still had the
old 'import { registerSerializationClass }' pattern instead of the
new inline IIFE.
pranaygp pushed a commit to vercel/sandbox that referenced this pull request Mar 27, 2026
…ibility (#109)
## Summary
Makes `@vercel/sandbox` fully compatible with the Workflow DevKit
compiler so that `Sandbox`, `Command`, and `CommandFinished` instances
can be used directly inside `"use workflow"` functions — no wrapper step
functions needed.
Supersedes #58.
## Problem
When `@vercel/sandbox` is imported in a workflow context, the workflow
builder tries to bundle it into the workflow VM bundle (because it has
`WORKFLOW_SERIALIZE`/`WORKFLOW_DESERIALIZE` on its classes). This fails
because:
1. The SDK's public methods use Node.js APIs (`fs`, `stream`, `zlib`,
`undici`, etc.) which are forbidden in the workflow VM
2. The compiled `dist/index.js` was a single bundled file that hoisted
all Node.js imports to the top, making them impossible to tree-shake
3. The `Sandbox` and `Command` classes had a sync `client` getter that
directly referenced `APIClient`, pulling the entire HTTP client stack
into the module scope
## Solution
### 1. `"use step"` annotations on all public async methods
Added `"use step"` to all 22 public async methods across `Sandbox` (14),
`Command` (5), and `Snapshot` (3). The SWC plugin strips these method
bodies in workflow mode, replacing them with durable step proxies. This
eliminates all Node.js API references from the workflow bundle.
### 2. Async `ensureClient()` replaces sync `client` getter
The sync `get client()` getter directly referenced `APIClient`, which
pulls in `undici`, `zlib`, `tar-stream`, `jsonlines`, etc. Replaced
with:
```typescript
private async ensureClient(): Promise<APIClient> {
"use step";
if (this._client) return this._client;
const credentials = getSandboxCredentials();
this._client = new APIClient({ ... });
return this._client;
}
```
Since `ensureClient()` is itself `"use step"`, its body (including `new
APIClient(...)`) gets stripped in workflow mode. All instance methods
now call `const client = await this.ensureClient();` instead of
`this.client`.
### 3. `bundle: false` in tsdown config
Changed from single-file bundling to per-file output. This keeps Node.js
imports local to the files that use them, so after the SWC plugin strips
step method bodies, the now-unused Node.js imports can be eliminated by
esbuild's tree-shaking.
### 4. Workflow-code-runner example updated
- Removed `serverExternalPackages: ["@vercel/sandbox"]` from
`next.config.ts` (no longer needed)
- Updated `workflow` dependency to use a tarball that includes the
inline class serialization registration fix (vercel/workflow#1480)
## Testing
- `pnpm build` succeeds for the full monorepo (all 8 tasks)
- The `workflow-code-runner` example app builds successfully with all
workflow routes generated
- 22 `"use step"` directives survive compilation in both ESM and CJS
dist output
## Related
- vercel/workflow#1480 — Inline class serialization registration (fixes
SWC import resolution for 3rd-party packages)
- vercel/workflow#1481 — Build-time warning when
`serverExternalPackages` hides workflow-enabled packages
- vercel/workflow#1144 — CJS detection for serde symbols (pending
review)
- #58 — Previous attempt (superseded by this PR)
---------
Signed-off-by: Peter Wielander <mittgfu@gmail.com>
Co-authored-by: Peter Wielander <mittgfu@gmail.com>
Sign up for freeto 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.

4 participants

@TooTallNate@pranaygp@VaguelySerious
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); Add CommonJS `require()` support for class serialization detection in SWC plugin by TooTallNate · Pull Request #1144 · vercel/workflow · GitHub
Skip to content

Add CommonJS require() support for class serialization detection in SWC plugin - #1144

Merged
TooTallNate merged 3 commits into
mainfrom
02-20-add_commonjs_require_support_for_class_serialization_detection_in_swc_plugin
Mar 23, 2026
Merged

Add CommonJS require() support for class serialization detection in SWC plugin#1144
TooTallNate merged 3 commits into
mainfrom
02-20-add_commonjs_require_support_for_class_serialization_detection_in_swc_plugin

Conversation

@TooTallNate

Copy link
Copy Markdown
Member

The SWC compiler plugin now detects classes with custom serialization methods (WORKFLOW_SERIALIZE / WORKFLOW_DESERIALIZE) when symbols are obtained via CommonJS require() calls, in addition to the existing ESM import and Symbol.for() patterns.

This handles code that has been pre-compiled from ESM to CommonJS by tools like TypeScript (tsc), esbuild, or tsup, which transform:

import{WORKFLOW_SERIALIZE}from"@workflow/serde"

into either:

constserde_1=require("@workflow/serde")// namespace requireconst{WORKFLOW_SERIALIZE}=require(...)// destructured require

Both patterns are now recognized during the identifier collection phase, and classes using them are properly registered with registerSerializationClass().

@changeset-bot

changeset-botBot commented Feb 20, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 14ea197

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

This PR includes changesets to release 17 packages
NameType
@workflow/swc-pluginPatch
@workflow/astroPatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nestPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
workflowPatch
@workflow/vitePatch
@workflow/vitestPatch
@workflow/world-testingPatch
@workflow/nuxtPatch
@workflow/aiPatch
@workflow/corePatch
@workflow/web-sharedPatch

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

@vercel

vercelBot commented Feb 20, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Feb 20, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production780067847
✅ 💻 Local Development7820142924
✅ 📦 Local Production7820142924
✅ 🐘 Local Postgres7820142924
✅ 🪟 Windows720577
❌ 🌍 Community Worlds1185621195
✅ 📋 Other198033231
Total3514565524122

❌ Failed Tests

🌍 Community Worlds (56 failed)

mongodb (3 failed):

  • hookWorkflow is not resumable via public webhook endpoint | wrun_01KMDYE4RWSYQ6538DVH70M5P0
  • webhookWorkflow | wrun_01KMDYED4N9B1F4PF1X6F86J26
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously | wrun_01KMDYMVS1CFTACB4081WS8K9V

redis (2 failed):

  • hookWorkflow is not resumable via public webhook endpoint | wrun_01KMDYE4RWSYQ6538DVH70M5P0
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously | wrun_01KMDYMVS1CFTACB4081WS8K9V

turso (51 failed):

  • addTenWorkflow | wrun_01KMDYCYXATZBJYFAPPKVRZKDZ
  • addTenWorkflow | wrun_01KMDYCYXATZBJYFAPPKVRZKDZ
  • wellKnownAgentWorkflow (.well-known/agent) | wrun_01KMDYEKVFEVFMCXC32DMPJ3KB
  • should work with react rendering in step
  • promiseAllWorkflow | wrun_01KMDYD5F67YMHR6KTJF5YFYM3
  • promiseRaceWorkflow | wrun_01KMDYDBXWV7065WDZ655A6X2Y
  • promiseAnyWorkflow | wrun_01KMDYDEFBS29N1Y4E04XS0PHS
  • importedStepOnlyWorkflow | wrun_01KMDYF31ZMHDYKRR9870BQJFE
  • hookWorkflow | wrun_01KMDYDTDYK5ZZTVGBV7PBPJAZ
  • hookWorkflow is not resumable via public webhook endpoint | wrun_01KMDYE4RWSYQ6538DVH70M5P0
  • webhookWorkflow | wrun_01KMDYED4N9B1F4PF1X6F86J26
  • sleepingWorkflow | wrun_01KMDYEKNPTWHR8GD4VJXRHSWR
  • parallelSleepWorkflow | wrun_01KMDYF1MMFSRK8683TXYGPEFF
  • nullByteWorkflow | wrun_01KMDYF6AS6SP633VT5B7JZ0Q8
  • workflowAndStepMetadataWorkflow | wrun_01KMDYF8B1WBHNCPZKHFYCH5G0
  • fetchWorkflow | wrun_01KMDYH6MPPT3CPGBXX02XNQTP
  • promiseRaceStressTestWorkflow | wrun_01KMDYH9Y93SDT6D7TEE6CMKHT
  • error handling error propagation workflow errors nested function calls preserve message and stack trace
  • error handling error propagation workflow errors cross-file imports preserve message and stack trace
  • error handling error propagation step errors basic step error preserves message and stack trace
  • error handling error propagation step errors cross-file step error preserves message and function names in stack
  • error handling retry behavior regular Error retries until success
  • error handling retry behavior FatalError fails immediately without retries
  • error handling retry behavior RetryableError respects custom retryAfter delay
  • error handling retry behavior maxRetries=0 disables retries
  • error handling catchability FatalError can be caught and detected with FatalError.is()
  • hookCleanupTestWorkflow - hook token reuse after workflow completion | wrun_01KMDYM7FJ0MWDXPS56VM7PF4D
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously | wrun_01KMDYMVS1CFTACB4081WS8K9V
  • hookDisposeTestWorkflow - hook token reuse after explicit disposal while workflow still running | wrun_01KMDYNGM0E0ZXFK8E2CQ9CYZV
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars) | wrun_01KMDYP5F3H7NXZ9W494MXF8EQ
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument | wrun_01KMDYPFQ0MK8D4B36398E2S6A
  • closureVariableWorkflow - nested step functions with closure variables | wrun_01KMDYPN9T3F9HBR5W1TQSBAVH
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step | wrun_01KMDYPQBVNH7GG5Y8ZDS8B1P3
  • health check (queue-based) - workflow and step endpoints respond to health check messages
  • pathsAliasWorkflow - TypeScript path aliases resolve correctly | wrun_01KMDYQ7ZKSHZ1DF03RZ64NGXD
  • Calculator.calculate - static workflow method using static step methods from another class | wrun_01KMDYQEMCVB21XR91WVHJEGK4
  • AllInOneService.processNumber - static workflow method using sibling static step methods | wrun_01KMDYQP3ST1XWR4FTRNPGNYE4
  • ChainableService.processWithThis - static step methods using this to reference the class | wrun_01KMDYQWMSK6ZYMFNY7ME546A9
  • thisSerializationWorkflow - step function invoked with .call() and .apply() | wrun_01KMDYR36AW71R74TKNQEA26QC
  • customSerializationWorkflow - custom class serialization with WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE | wrun_01KMDYR9J9RVJCAWSQG76N5656
  • instanceMethodStepWorkflow - instance methods with "use step" directive | wrun_01KMDYRH7WBXH2XGDZPC5CXB49
  • crossContextSerdeWorkflow - classes defined in step code are deserializable in workflow context | wrun_01KMDYRVMSG67Q29E0JT62SCPB
  • stepFunctionAsStartArgWorkflow - step function reference passed as start() argument | wrun_01KMDYS38DF4PJE3SJHH7YV0GQ
  • cancelRun - cancelling a running workflow | wrun_01KMDYSB4MVW6TNAVS70ZDC36V
  • cancelRun via CLI - cancelling a running workflow | wrun_01KMDYSM88CM0VKBMVABG1ZRM0
  • pages router addTenWorkflow via pages router
  • pages router promiseAllWorkflow via pages router
  • pages router sleepingWorkflow via pages router
  • hookWithSleepWorkflow - hook payloads delivered correctly with concurrent sleep | wrun_01KMDYT02V9K533MN8PA31072V
  • sleepInLoopWorkflow - sleep inside loop with steps actually delays each iteration | wrun_01KMDYTKS4H2ZFNHJC3AH7D70B
  • sleepWithSequentialStepsWorkflow - sequential steps work with concurrent sleep (control) | wrun_01KMDYTZMDM1Q8370F5R6NNHJV

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro7007
✅ example7007
✅ express7007
✅ fastify7007
✅ hono7007
✅ nextjs-turbopack7502
✅ nextjs-webpack7502
✅ nitro7007
✅ nuxt7007
✅ sveltekit7007
✅ vite7007
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable66011
✅ express-stable66011
✅ fastify-stable66011
✅ hono-stable66011
✅ nextjs-turbopack-canary55022
✅ nextjs-turbopack-stable7205
✅ nextjs-webpack-canary55022
✅ nextjs-webpack-stable7205
✅ nitro-stable66011
✅ nuxt-stable66011
✅ sveltekit-stable66011
✅ vite-stable66011
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable66011
✅ express-stable66011
✅ fastify-stable66011
✅ hono-stable66011
✅ nextjs-turbopack-canary55022
✅ nextjs-turbopack-stable7205
✅ nextjs-webpack-canary55022
✅ nextjs-webpack-stable7205
✅ nitro-stable66011
✅ nuxt-stable66011
✅ sveltekit-stable66011
✅ vite-stable66011
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable66011
✅ express-stable66011
✅ fastify-stable66011
✅ hono-stable66011
✅ nextjs-turbopack-canary55022
✅ nextjs-turbopack-stable7205
✅ nextjs-webpack-canary55022
✅ nextjs-webpack-stable7205
✅ nitro-stable66011
✅ nuxt-stable66011
✅ sveltekit-stable66011
✅ vite-stable66011
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack7205
❌ 🌍 Community Worlds
AppPassedFailedSkipped
✅ mongodb-dev302
❌ mongodb5235
✅ redis-dev302
❌ redis5325
✅ turso-dev302
❌ turso4515
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable66011
✅ e2e-local-postgres-nest-stable66011
✅ e2e-local-prod-nest-stable66011

📋 View full workflow run

@github-actions

github-actionsBot commented Feb 20, 2026

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

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

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro0.037s (-21.3% 🟢)1.005s (~)0.967s101.00x
💻 LocalExpress0.043s (-4.5%)1.005s (~)0.962s101.14x
🐘 PostgresNitro0.047s (-31.9% 🟢)1.012s (~)0.965s101.26x
🐘 PostgresExpress0.048s (-32.2% 🟢)1.012s (-0.5%)0.964s101.29x
💻 LocalNext.js (Turbopack)0.051s1.006s0.956s101.35x
🌐 RedisNext.js (Turbopack)0.054s1.005s0.952s101.44x
🐘 PostgresNext.js (Turbopack)0.062s1.012s0.950s101.65x
workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.097s (-2.8%)2.005s (~)0.908s101.00x
🌐 RedisNext.js (Turbopack)1.120s2.007s0.887s101.02x
🐘 PostgresNitro1.124s (-1.6%)2.012s (~)0.888s101.02x
💻 LocalNext.js (Turbopack)1.124s2.006s0.883s101.02x
🐘 PostgresExpress1.126s (-1.1%)2.012s (~)0.886s101.03x
💻 LocalExpress1.131s (~)2.005s (~)0.874s101.03x
🐘 PostgresNext.js (Turbopack)1.143s2.013s0.869s101.04x
workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro10.635s (-2.4%)11.023s (~)0.388s31.00x
🌐 RedisNext.js (Turbopack)10.680s11.025s0.345s31.00x
🐘 PostgresNitro10.689s (-2.4%)11.036s (~)0.348s31.01x
🐘 PostgresExpress10.705s (-1.8%)11.038s (~)0.333s31.01x
💻 LocalNext.js (Turbopack)10.847s11.025s0.178s31.02x
💻 LocalExpress10.934s (~)11.023s (~)0.089s31.03x
🐘 PostgresNext.js (Turbopack)10.996s11.382s0.386s31.03x
workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)14.091s15.033s0.941s41.00x
🐘 PostgresExpress14.123s (-3.1%)15.042s (~)0.919s41.00x
🐘 PostgresNitro14.129s (-4.7%)15.039s (~)0.910s41.00x
💻 LocalNitro14.183s (-4.9%)15.029s (~)0.847s41.01x
🐘 PostgresNext.js (Turbopack)14.580s15.049s0.469s41.03x
💻 LocalNext.js (Turbopack)14.703s15.031s0.328s41.04x
💻 LocalExpress14.911s (-0.6%)15.029s (-3.2%)0.118s41.06x
workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)13.030s13.457s0.427s71.00x
🐘 PostgresExpress13.096s (-6.5% 🟢)14.038s (-3.0%)0.942s71.01x
🐘 PostgresNitro13.119s (-8.7% 🟢)14.038s (-6.7% 🟢)0.919s71.01x
🐘 PostgresNext.js (Turbopack)14.145s14.759s0.613s71.09x
💻 LocalNitro14.789s (-10.1% 🟢)15.026s (-11.8% 🟢)0.237s61.14x
💻 LocalNext.js (Turbopack)16.157s17.033s0.875s61.24x
💻 LocalExpress16.514s (-1.0%)17.029s (~)0.516s61.27x
Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.228s (-3.0%)2.011s (~)0.783s151.00x
🐘 PostgresNitro1.231s (-4.8%)2.011s (~)0.780s151.00x
🌐 RedisNext.js (Turbopack)1.266s2.007s0.741s151.03x
🐘 PostgresNext.js (Turbopack)1.286s2.011s0.725s151.05x
💻 LocalNitro1.472s (-0.8%)2.005s (~)0.534s151.20x
💻 LocalExpress1.503s (~)2.006s (~)0.503s151.22x
💻 LocalNext.js (Turbopack)1.524s2.006s0.482s151.24x
Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express2.341s (-4.9%)3.012s (~)0.671s101.00x
🐘 PostgresNitro2.353s (-4.8%)3.013s (~)0.660s101.01x
🌐 RedisNext.js (Turbopack)2.478s3.009s0.531s101.06x
🐘 PostgresNext.js (Turbopack)2.484s3.015s0.531s101.06x
💻 LocalNitro2.551s (-11.2% 🟢)3.007s (-3.2%)0.456s101.09x
💻 LocalExpress2.970s (-0.9%)3.308s (-7.2% 🟢)0.338s101.27x
💻 LocalNext.js (Turbopack)2.995s3.309s0.314s101.28x
Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express3.519s (-1.9%)4.014s (~)0.495s81.00x
🐘 PostgresNitro3.523s (-3.0%)4.015s (~)0.492s81.00x
🐘 PostgresNext.js (Turbopack)3.844s4.017s0.173s81.09x
🌐 RedisNext.js (Turbopack)4.117s4.727s0.610s71.17x
💻 LocalNitro7.025s (-13.5% 🟢)7.514s (-14.3% 🟢)0.489s42.00x
💻 LocalExpress8.144s (-3.9%)8.769s (-2.8%)0.625s42.31x
💻 LocalNext.js (Turbopack)8.563s9.019s0.456s42.43x
Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.207s (-4.1%)2.011s (~)0.803s151.00x
🐘 PostgresNitro1.216s (-4.7%)2.010s (~)0.794s151.01x
🌐 RedisNext.js (Turbopack)1.252s2.007s0.755s151.04x
🐘 PostgresNext.js (Turbopack)1.254s2.011s0.757s151.04x
💻 LocalNitro1.484s (-4.9%)2.006s (~)0.522s151.23x
💻 LocalNext.js (Turbopack)1.515s2.005s0.490s151.25x
💻 LocalExpress1.550s (+0.9%)2.005s (~)0.455s151.28x
Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro2.342s (-4.7%)3.012s (~)0.670s101.00x
🐘 PostgresExpress2.353s (-4.0%)3.012s (~)0.659s101.00x
🐘 PostgresNext.js (Turbopack)2.474s3.013s0.539s101.06x
🌐 RedisNext.js (Turbopack)2.498s3.009s0.511s101.07x
💻 LocalNitro2.924s (-3.4%)3.208s (-12.7% 🟢)0.283s101.25x
💻 LocalNext.js (Turbopack)3.012s3.675s0.663s91.29x
💻 LocalExpress3.090s (+1.9%)3.885s (+3.4%)0.795s81.32x
Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express3.515s (-2.3%)4.013s (~)0.497s81.00x
🐘 PostgresNitro3.518s (-2.7%)4.014s (~)0.496s81.00x
🐘 PostgresNext.js (Turbopack)3.841s4.018s0.177s81.09x
🌐 RedisNext.js (Turbopack)4.031s4.583s0.553s71.15x
💻 LocalNitro7.511s (-13.1% 🟢)8.016s (-11.2% 🟢)0.506s42.14x
💻 LocalExpress8.608s (-5.0%)9.020s (-7.7% 🟢)0.412s42.45x
💻 LocalNext.js (Turbopack)9.214s9.772s0.557s42.62x
workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)0.601s1.005s0.404s601.00x
🐘 PostgresExpress0.642s (-25.8% 🟢)1.009s (-1.7%)0.366s601.07x
🐘 PostgresNitro0.657s (-26.8% 🟢)1.008s (-3.4%)0.351s601.09x
💻 LocalNitro0.672s (-29.7% 🟢)1.004s (-1.7%)0.332s601.12x
🐘 PostgresNext.js (Turbopack)0.870s1.009s0.140s601.45x
💻 LocalNext.js (Turbopack)0.878s1.057s0.179s571.46x
💻 LocalExpress0.973s (-1.0%)1.114s (+1.7%)0.140s551.62x
workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)1.460s2.006s0.546s451.00x
🐘 PostgresExpress1.553s (-26.6% 🟢)2.010s (-33.3% 🟢)0.457s451.06x
🐘 PostgresNitro1.563s (-27.7% 🟢)2.032s (-31.8% 🟢)0.470s451.07x
🐘 PostgresNext.js (Turbopack)2.127s2.948s0.821s311.46x
💻 LocalNitro2.193s (-25.8% 🟢)3.007s (-3.3%)0.815s301.50x
💻 LocalNext.js (Turbopack)2.736s3.008s0.272s301.87x
💻 LocalExpress2.966s (-1.6%)3.222s (-9.2% 🟢)0.255s282.03x
workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)3.077s3.437s0.360s351.00x
🐘 PostgresNitro3.153s (-27.3% 🟢)4.012s (-20.0% 🟢)0.859s301.02x
🐘 PostgresExpress3.188s (-25.2% 🟢)4.012s (-19.3% 🟢)0.824s301.04x
🐘 PostgresNext.js (Turbopack)4.345s5.015s0.670s241.41x
💻 LocalNitro7.218s (-20.1% 🟢)7.827s (-17.4% 🟢)0.610s162.35x
💻 LocalNext.js (Turbopack)8.797s9.232s0.434s142.86x
💻 LocalExpress8.995s (-1.7%)9.479s (-3.1%)0.484s132.92x
workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.243s (-19.7% 🟢)1.008s (~)0.766s601.00x
🐘 PostgresNitro0.245s (-16.9% 🟢)1.009s (~)0.764s601.01x
🌐 RedisNext.js (Turbopack)0.260s1.005s0.744s601.07x
🐘 PostgresNext.js (Turbopack)0.287s1.010s0.723s601.18x
💻 LocalNitro0.576s (-3.7%)1.004s (~)0.428s602.37x
💻 LocalExpress0.581s (-3.7%)1.004s (~)0.424s602.39x
💻 LocalNext.js (Turbopack)0.585s1.004s0.420s602.41x
workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.419s (-20.5% 🟢)1.009s (~)0.589s901.00x
🐘 PostgresNitro0.424s (-21.4% 🟢)1.009s (~)0.585s901.01x
🐘 PostgresNext.js (Turbopack)0.590s1.033s0.443s881.41x
🌐 RedisNext.js (Turbopack)1.194s2.006s0.812s452.85x
💻 LocalNitro2.345s (-5.2% 🟢)3.007s (~)0.662s305.59x
💻 LocalExpress2.498s (-3.2%)3.009s (~)0.510s305.96x
💻 LocalNext.js (Turbopack)2.664s3.009s0.345s306.36x
workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.606s (-35.5% 🟢)1.009s (-15.4% 🟢)0.403s1191.00x
🐘 PostgresExpress0.620s (-30.9% 🟢)1.009s (-3.1%)0.389s1191.02x
🐘 PostgresNext.js (Turbopack)1.001s1.477s0.476s821.65x
🌐 RedisNext.js (Turbopack)2.883s3.085s0.202s394.76x
💻 LocalNitro10.189s (-8.3% 🟢)10.855s (-6.9% 🟢)0.666s1216.83x
💻 LocalExpress11.127s (-1.4%)11.845s (-1.5%)0.718s1118.38x
💻 LocalNext.js (Turbopack)11.287s11.753s0.465s1118.64x
Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro0.140s (-28.6% 🟢)1.003s (~)0.009s (-26.3% 🟢)1.014s (~)0.874s101.00x
🐘 PostgresExpress0.164s (-24.1% 🟢)0.999s (+0.5%)0.001s (-8.3% 🟢)1.013s (~)0.848s101.17x
🐘 PostgresNitro0.167s (-30.3% 🟢)0.999s (+0.7%)0.001s (-21.4% 🟢)1.013s (~)0.846s101.19x
🌐 RedisNext.js (Turbopack)0.169s1.000s0.001s1.008s0.838s101.21x
💻 LocalNext.js (Turbopack)0.178s1.001s0.012s1.018s0.840s101.27x
💻 LocalExpress0.199s (-2.4%)1.003s (~)0.012s (-2.5%)1.017s (~)0.818s101.42x
🐘 PostgresNext.js (Turbopack)0.224s1.001s0.001s1.013s0.789s101.60x
stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)0.447s0.999s0.003s1.012s0.565s601.00x
🐘 PostgresNitro0.563s (-22.1% 🟢)1.005s (~)0.003s (-13.6% 🟢)1.023s (~)0.460s591.26x
💻 LocalNitro0.569s (-21.7% 🟢)1.008s (~)0.009s (-6.2% 🟢)1.021s (~)0.452s591.27x
🐘 PostgresExpress0.610s (-12.0% 🟢)1.006s (~)0.005s (+20.4% 🔺)1.028s (~)0.417s591.37x
💻 LocalNext.js (Turbopack)0.672s1.008s0.010s1.024s0.352s591.50x
💻 LocalExpress0.723s (-6.9% 🟢)1.009s (-1.7%)0.009s (-12.1% 🟢)1.022s (-1.8%)0.300s591.62x
🐘 PostgresNext.js (Turbopack)0.760s1.026s0.008s1.050s0.290s591.70x
10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)0.916s1.016s0.000s1.022s0.106s591.00x
🐘 PostgresNitro0.988s (+7.3% 🔺)1.384s (+30.2% 🔺)0.000s (-36.4% 🟢)1.398s (+29.3% 🔺)0.410s441.08x
🐘 PostgresExpress1.066s (+19.3% 🔺)1.664s (+53.7% 🔺)0.000s (+205.6% 🔺)1.677s (+51.8% 🔺)0.612s361.16x
💻 LocalNitro1.140s (-6.9% 🟢)2.017s (~)0.000s (-38.5% 🟢)2.020s (~)0.880s301.24x
🐘 PostgresNext.js (Turbopack)1.175s1.909s0.000s1.930s0.755s321.28x
💻 LocalExpress1.235s (-1.8%)2.020s (~)0.000s (-35.3% 🟢)2.023s (~)0.788s301.35x
💻 LocalNext.js (Turbopack)1.295s2.019s0.000s2.024s0.729s301.41x
fan-out fan-in 10 streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.886s (~)2.223s (~)0.000s (-100.0% 🟢)2.233s (~)0.347s271.00x
🌐 RedisNext.js (Turbopack)1.963s2.180s0.000s2.186s0.223s281.04x
🐘 PostgresExpress2.034s (+12.7% 🔺)2.441s (+16.3% 🔺)0.000s (+Infinity% 🔺)2.453s (+15.6% 🔺)0.418s251.08x
🐘 PostgresNext.js (Turbopack)2.302s3.005s0.000s3.015s0.713s201.22x
💻 LocalNitro3.350s (+0.6%)3.967s (~)0.000s (-60.0% 🟢)3.971s (~)0.621s161.78x
💻 LocalExpress3.508s (+1.9%)4.030s (-1.6%)0.001s (+37.5% 🔺)4.035s (-1.6%)0.527s151.86x
💻 LocalNext.js (Turbopack)3.784s4.387s0.000s4.392s0.608s142.01x

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNitro21/21
🐘 PostgresExpress12/21
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express🐘 Postgres20/21
Next.js (Turbopack)🌐 Redis15/21
Nitro🐘 Postgres17/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)

📋 View full workflow run


Some benchmark jobs failed:

  • Local: success
  • Postgres: success
  • Vercel: failure

Check the workflow run for details.

@TooTallNateGraphite App

Copy link
Copy Markdown
MemberAuthor

This stack of pull requests is managed by Graphite. Learn more about stacking.

@TooTallNate
TooTallNate marked this pull request as ready for review February 20, 2026 22:47
CopilotAI review requested due to automatic review settings February 20, 2026 22:47

CopilotAI 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.

Pull request overview

This PR extends the @workflow/swc-plugin transform to detect custom class serialization methods when the WORKFLOW_SERIALIZE / WORKFLOW_DESERIALIZE symbols are accessed via CommonJS require() patterns, aligning behavior with codebases that have been transpiled from ESM to CJS.

Changes:

  • Track CommonJS require() bindings during identifier collection (namespace const x = require(...) and destructured const { ... } = require(...)) and recognize x.WORKFLOW_SERIALIZE / x.WORKFLOW_DESERIALIZE as serialization symbols.
  • Add fixture coverage for both namespace-require and destructured-require inputs across workflow/step/client outputs.
  • Update the plugin spec and add a changeset entry documenting the new behavior.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 2 comments.

Show a summary per file
FileDescription
packages/swc-plugin-workflow/transform/src/lib.rsAdds require-binding tracking and member-expression symbol recognition for serialization detection.
packages/swc-plugin-workflow/transform/tests/fixture/custom-serialization-require-namespace/input.jsNew fixture input for namespace require() pattern.
packages/swc-plugin-workflow/transform/tests/fixture/custom-serialization-require-namespace/output-workflow.jsExpected workflow-mode output for namespace require() fixture.
packages/swc-plugin-workflow/transform/tests/fixture/custom-serialization-require-namespace/output-step.jsExpected step-mode output for namespace require() fixture.
packages/swc-plugin-workflow/transform/tests/fixture/custom-serialization-require-namespace/output-client.jsExpected client-mode output for namespace require() fixture.
packages/swc-plugin-workflow/transform/tests/fixture/custom-serialization-require-destructured/input.jsNew fixture input for destructured require() pattern.
packages/swc-plugin-workflow/transform/tests/fixture/custom-serialization-require-destructured/output-workflow.jsExpected workflow-mode output for destructured require() fixture.
packages/swc-plugin-workflow/transform/tests/fixture/custom-serialization-require-destructured/output-step.jsExpected step-mode output for destructured require() fixture.
packages/swc-plugin-workflow/transform/tests/fixture/custom-serialization-require-destructured/output-client.jsExpected client-mode output for destructured require() fixture.
packages/swc-plugin-workflow/spec.mdDocuments supported CommonJS require() symbol patterns.
.changeset/social-forks-tease.mdDeclares a patch release for the new detection behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment threadpackages/swc-plugin-workflow/transform/src/lib.rs Outdated
Comment threadpackages/swc-plugin-workflow/transform/src/lib.rs Outdated
…WC plugin
The SWC compiler plugin now detects classes with custom serialization methods (`WORKFLOW_SERIALIZE` / `WORKFLOW_DESERIALIZE`) when symbols are obtained via CommonJS `require()` calls, in addition to the existing ESM import and `Symbol.for()` patterns.
This handles code that has been pre-compiled from ESM to CommonJS by tools like TypeScript (tsc), esbuild, or tsup, which transform:
```ts
import { WORKFLOW_SERIALIZE } from "@workflow/serde"
```
into either:
```ts
const serde_1 = require("@workflow/serde") // namespace require
const { WORKFLOW_SERIALIZE } = require(...) // destructured require
```
Both patterns are now recognized during the identifier collection phase, and classes using them are properly registered with registerSerializationClass().
@TooTallNateTooTallNate changed the title Add CommonJS require() support for class serialization detection in SWC pluginAdd CommonJS require() support for class serialization detection in SWC pluginMar 23, 2026
…on_bindings helper
- Tighten is_require_call to require exactly one string literal argument,
rejecting dynamic requires and multi-arg calls.
- Extract track_serialization_bindings helper to deduplicate the require
tracking logic between Decl::Var and ExportDecl(Decl::Var) branches.
@TooTallNate
TooTallNateforce-pushed the 02-20-add_commonjs_require_support_for_class_serialization_detection_in_swc_plugin branch from 3648e46 to f461c59CompareMarch 23, 2026 14:01
@TooTallNate
TooTallNate requested a review from a team as a code ownerMarch 23, 2026 14:01
@TooTallNate
TooTallNate merged commit 992d768 into mainMar 23, 2026
98 of 103 checks passed
@TooTallNate
TooTallNate deleted the 02-20-add_commonjs_require_support_for_class_serialization_detection_in_swc_plugin branch March 23, 2026 18:30
@ghostghost mentioned this pull request Mar 23, 2026

@pranaygppranaygp 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.

Post-merge review — overall this is a clean, well-tested addition. A few observations below.

// Track CommonJS namespace require: const serde_1 = require("...")
if self.is_require_call(init) {
self.require_namespace_identifiers
.insert(ident.id.sym.to_string());

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.

Nit: This tracks everyrequire() call as a namespace identifier, regardless of the module being required. So const fs = require("fs") would cause fs.WORKFLOW_SERIALIZE to be treated as a serialization symbol if it ever appeared. This is unlikely to cause real bugs since nobody would name a property WORKFLOW_SERIALIZE on an unrelated module, but it is semantically imprecise.

Consider either:

  1. Restricting to known serde modules (e.g. checking the string arg contains @workflow/serde or similar), or
  2. Documenting this as an intentional trade-off (broad tracking is simpler and the false-positive risk is negligible).

match prop {
ObjectPatProp::Assign(assign) => {
// const { WORKFLOW_SERIALIZE } = require("...")
let name = assign.key.sym.to_string();

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.

Nit: The ObjectPatProp::Assign arm handles const { WORKFLOW_SERIALIZE } = require(...), but in the SWC AST this node actually represents const { WORKFLOW_SERIALIZE = defaultValue } = require(...) (a destructured binding with a default value). A plain shorthand const { WORKFLOW_SERIALIZE } = require(...) is typically parsed as ObjectPatProp::KeyValue where key and value ident are the same.

In practice this still works because SWC may represent the no-default shorthand either way depending on version, and the logic is correct for both paths. But worth noting that the Assign arm would also match const { WORKFLOW_SERIALIZE = someDefault } = require(...), which is an unusual but technically valid pattern.

@@ -0,0 +1,25 @@
// Test custom serialization with CommonJS destructured require
const { WORKFLOW_SERIALIZE, WORKFLOW_DESERIALIZE } = require("@workflow/serde");

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.

The spec documents renamed destructured require (const { WORKFLOW_SERIALIZE: WS } = require(...)) but there's no test fixture covering this case. Adding a custom-serialization-require-destructured-renamed fixture would ensure the ObjectPatProp::KeyValue branch stays correct.

pranaygp added a commit that referenced this pull request Mar 23, 2026
* origin/main:
[ai] Add experimental_context to DurableAgentOptions (#1489)
[ai] Expose configured tools on DurableAgent instances (#1488)
fix(builders): catch node builtin usage when entry fields diverge (#1455)
[web-shared] Fix timeline duration format and precision (#1482)
[cli] Add bulk cancel, --status filter, fix step JSON hydration (#1467)
[utils] Re-export parseName utilities and add workflow/observability module (#1453)
[o11y] Polish display when run data has expired (#1438)
Add CommonJS `require()` support for class serialization detection in SWC plugin (#1144)
fix(next): stabilize deferred canary e2e in nextjs workbenches (#1468)
[web] Support legacy newline-delimited stream format in `useStreamReader` (#1473)
Revert "Add support for calling start() inside workflow functions (#1133)" (#1475)
pranaygp added a commit that referenced this pull request Mar 24, 2026
…naygp-db9e68c1
* 'main' of https://github.com/vercel/workflow: (32 commits)
chore: bump @nestjs/* to ^11.1.17 (#1497)
chore: bump hono to ^4.12.8 (#1495)
Revert "Inline class serialization registration to fix 3rd-party package supp…" (#1493)
[world] Add stream pagination and metadata endpoints (#1470)
[cli] [world-local] Ensure update checks don't suggest upgrading from stable release to pre-releases (#1490)
Remove NestJS Vercel integration while in experimental phase (#1485)
feat: export semantic error types and add API reference docs (#1447)
feat: enforce max queue deliveries in handlers with graceful failure (#1344)
[world-postgres] Migrate client from `postgres.js` to `pg` (#1484)
Inline class serialization registration to fix 3rd-party package support (#1480)
[ai] Add experimental_context to DurableAgentOptions (#1489)
[ai] Expose configured tools on DurableAgent instances (#1488)
fix(builders): catch node builtin usage when entry fields diverge (#1455)
[web-shared] Fix timeline duration format and precision (#1482)
[cli] Add bulk cancel, --status filter, fix step JSON hydration (#1467)
[utils] Re-export parseName utilities and add workflow/observability module (#1453)
[o11y] Polish display when run data has expired (#1438)
Add CommonJS `require()` support for class serialization detection in SWC plugin (#1144)
fix(next): stabilize deferred canary e2e in nextjs workbenches (#1468)
[web] Support legacy newline-delimited stream format in `useStreamReader` (#1473)
...
TooTallNate added a commit that referenced this pull request Mar 24, 2026
The original PR #1480 was merged but reverted because it didn't include
updated fixtures for the CJS require patterns added by PR #1144
(custom-serialization-require-destructured and
custom-serialization-require-namespace). These fixtures still had the
old 'import { registerSerializationClass }' pattern instead of the
new inline IIFE.
TooTallNate added a commit that referenced this pull request Mar 24, 2026
…ort (v2) (#1503)
* Inline class serialization registration to fix 3rd-party package support (#1480)
* Inline class serialization registration to fix 3rd-party package support
The SWC plugin previously generated:
import { registerSerializationClass } from "workflow/internal/class-serialization";
registerSerializationClass("class//...", ClassName);
This broke for 3rd-party packages (e.g. @vercel/sandbox) that define
serializable classes but don't depend on the 'workflow' package. The
bare 'workflow' specifier is unresolvable from within node_modules of
a package that doesn't list it as a dependency.
Now the plugin generates a self-contained IIFE that uses
Symbol.for('workflow-class-registry') on globalThis directly, with
zero module dependencies:
(function(__wf_cls, __wf_id) {
var __wf_sym = Symbol.for("workflow-class-registry"),
__wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map());
__wf_reg.set(__wf_id, __wf_cls);
Object.defineProperty(__wf_cls, "classId", { ... });
})(ClassName, "class//...");
This is fully compatible with the existing deserialization side in
@workflow/core which reads from the same globalThis registry.
* Address review feedback: fix comment and update docstring
- Fix inaccurate IIFE comment in lib.rs: the second arg is the
generated class ID string, not the literal "classId"
- Update registerSerializationClass docstring to reflect that the
SWC plugin now inlines equivalent logic rather than importing it
* Update CJS require fixture outputs for inline class serialization
The original PR #1480 was merged but reverted because it didn't include
updated fixtures for the CJS require patterns added by PR #1144
(custom-serialization-require-destructured and
custom-serialization-require-namespace). These fixtures still had the
old 'import { registerSerializationClass }' pattern instead of the
new inline IIFE.
pranaygp pushed a commit to vercel/sandbox that referenced this pull request Mar 27, 2026
…ibility (#109)
## Summary
Makes `@vercel/sandbox` fully compatible with the Workflow DevKit
compiler so that `Sandbox`, `Command`, and `CommandFinished` instances
can be used directly inside `"use workflow"` functions — no wrapper step
functions needed.
Supersedes #58.
## Problem
When `@vercel/sandbox` is imported in a workflow context, the workflow
builder tries to bundle it into the workflow VM bundle (because it has
`WORKFLOW_SERIALIZE`/`WORKFLOW_DESERIALIZE` on its classes). This fails
because:
1. The SDK's public methods use Node.js APIs (`fs`, `stream`, `zlib`,
`undici`, etc.) which are forbidden in the workflow VM
2. The compiled `dist/index.js` was a single bundled file that hoisted
all Node.js imports to the top, making them impossible to tree-shake
3. The `Sandbox` and `Command` classes had a sync `client` getter that
directly referenced `APIClient`, pulling the entire HTTP client stack
into the module scope
## Solution
### 1. `"use step"` annotations on all public async methods
Added `"use step"` to all 22 public async methods across `Sandbox` (14),
`Command` (5), and `Snapshot` (3). The SWC plugin strips these method
bodies in workflow mode, replacing them with durable step proxies. This
eliminates all Node.js API references from the workflow bundle.
### 2. Async `ensureClient()` replaces sync `client` getter
The sync `get client()` getter directly referenced `APIClient`, which
pulls in `undici`, `zlib`, `tar-stream`, `jsonlines`, etc. Replaced
with:
```typescript
private async ensureClient(): Promise<APIClient> {
"use step";
if (this._client) return this._client;
const credentials = getSandboxCredentials();
this._client = new APIClient({ ... });
return this._client;
}
```
Since `ensureClient()` is itself `"use step"`, its body (including `new
APIClient(...)`) gets stripped in workflow mode. All instance methods
now call `const client = await this.ensureClient();` instead of
`this.client`.
### 3. `bundle: false` in tsdown config
Changed from single-file bundling to per-file output. This keeps Node.js
imports local to the files that use them, so after the SWC plugin strips
step method bodies, the now-unused Node.js imports can be eliminated by
esbuild's tree-shaking.
### 4. Workflow-code-runner example updated
- Removed `serverExternalPackages: ["@vercel/sandbox"]` from
`next.config.ts` (no longer needed)
- Updated `workflow` dependency to use a tarball that includes the
inline class serialization registration fix (vercel/workflow#1480)
## Testing
- `pnpm build` succeeds for the full monorepo (all 8 tasks)
- The `workflow-code-runner` example app builds successfully with all
workflow routes generated
- 22 `"use step"` directives survive compilation in both ESM and CJS
dist output
## Related
- vercel/workflow#1480 — Inline class serialization registration (fixes
SWC import resolution for 3rd-party packages)
- vercel/workflow#1481 — Build-time warning when
`serverExternalPackages` hides workflow-enabled packages
- vercel/workflow#1144 — CJS detection for serde symbols (pending
review)
- #58 — Previous attempt (superseded by this PR)
---------
Signed-off-by: Peter Wielander <mittgfu@gmail.com>
Co-authored-by: Peter Wielander <mittgfu@gmail.com>
Sign up for freeto 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.

4 participants

@TooTallNate@pranaygp@VaguelySerious