Skip to content

Add hook.hasConflict for early hook conflict detection - #2015

Merged
TooTallNate merged 9 commits into
mainfrom
pranaygp/codex/hook-ready
Jun 11, 2026
Merged

Add hook.hasConflict for early hook conflict detection#2015
TooTallNate merged 9 commits into
mainfrom
pranaygp/codex/hook-ready

Conversation

@pranaygp

@pranaygppranaygp commented May 18, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds hook.hasConflict, a lazy Promise<boolean> getter that lets workflow code commit hook registration and check for token conflicts without waiting for hook payload data.

Calling createHook() alone does not register the hook — registration is only committed when the workflow suspends. Awaiting hook.hasConflict suspends the workflow to commit the registration, then resolves with false once hook_created is recorded, or true if another active hook already owns the token. Unlike awaiting the hook payload (which rejects with HookConflictError on conflict), hasConflict never throws, so workflows can branch on conflicts early:

using hook=createHook({token: `order:${orderId}`});if(awaithook.hasConflict){return;// another run already owns this token}

This gives workflows a way to claim a deterministic hook token for idempotency before running duplicate-sensitive work, and is a prerequisite for #2011, where the idempotency docs/examples should use await hook.hasConflict instead of requiring consumers to wait for or resume hook data just to claim the token.

Changes

  • Add Hook.hasConflict: Promise<boolean> to the public hook interface.
  • Resolve pending hasConflict waiters with false on hook_created and true on hook_conflict; both fast-paths settle through promiseQueue so resolution order matches event-log order.
  • Suspend workflow execution when hook.hasConflict is awaited before hook creation is recorded.
  • Skip inline step execution when a suspension contains an awaited hook creation, so a hasConflict continuation (e.g. hook.hasConflict.then(() => stepB())) can advance in parallel with other steps instead of only after they finish.
  • Document hook.hasConflict in the v4 and v5 hook/webhook docs and event-sourcing pages, including the suspension-commits-registration semantics.
  • Add core unit tests (resolution, suspension, conflict, payload preservation, suspension-handler scheduling) and e2e tests (registration without payload, non-blocking steps, parallel continuation scheduling, conflict resolves true).

Validation

  • cd packages/core && pnpm typecheck && pnpm vitest run src/ (1146 tests)
  • Hook e2e suite against local nextjs-turbopack dev server: pnpm vitest run packages/core/e2e/e2e.test.ts -t 'hook' (20 tests, including the new parallel-continuation test)

Docs Preview

Changed pages on the docs preview deployment:

Pagev4v5
createHook() API referencev4v5
createWebhook() API referencev4v5
Foundations: Hooksv4v5
How It Works: Event Sourcingv4v5

CopilotAI review requested due to automatic review settings May 18, 2026 23:22
@pranaygp
pranaygp requested a review from a team as a code ownerMay 18, 2026 23:22
@changeset-bot

changeset-botBot commented May 18, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 2342dae

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

This PR includes changesets to release 17 packages
NameType
@workflow/coreMinor
workflowMinor
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/webPatch
@workflow/world-testingPatch
@workflow/aiMajor
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch
@workflow/nuxtPatch

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 May 18, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented May 18, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production137602191595
✅ 💻 Local Development181102192030
✅ 📦 Local Production181102192030
✅ 🐘 Local Postgres179702332030
✅ 🪟 Windows14500145
✅ 📋 Other83701781015
Total7777010688845

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro119026
✅ example119026
✅ express119026
✅ fastify119026
✅ hono119026
✅ nextjs-turbopack14302
✅ nextjs-webpack14302
✅ nitro119026
✅ nuxt119026
✅ sveltekit13807
✅ vite119026
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable120025
✅ express-stable120025
✅ fastify-stable120025
✅ hono-stable120025
✅ nextjs-turbopack-canary126019
✅ nextjs-turbopack-stable-lazy-discovery-disabled14500
✅ nextjs-turbopack-stable-lazy-discovery-enabled14500
✅ nextjs-webpack-canary126019
✅ nextjs-webpack-stable-lazy-discovery-disabled14500
✅ nextjs-webpack-stable-lazy-discovery-enabled14500
✅ nitro-stable120025
✅ nuxt-stable120025
✅ sveltekit-stable13906
✅ vite-stable120025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable120025
✅ express-stable120025
✅ fastify-stable120025
✅ hono-stable120025
✅ nextjs-turbopack-canary126019
✅ nextjs-turbopack-stable-lazy-discovery-disabled14500
✅ nextjs-turbopack-stable-lazy-discovery-enabled14500
✅ nextjs-webpack-canary126019
✅ nextjs-webpack-stable-lazy-discovery-disabled14500
✅ nextjs-webpack-stable-lazy-discovery-enabled14500
✅ nitro-stable120025
✅ nuxt-stable120025
✅ sveltekit-stable13906
✅ vite-stable120025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable119026
✅ express-stable119026
✅ fastify-stable119026
✅ hono-stable119026
✅ nextjs-turbopack-canary125020
✅ nextjs-turbopack-stable-lazy-discovery-disabled14401
✅ nextjs-turbopack-stable-lazy-discovery-enabled14401
✅ nextjs-webpack-canary125020
✅ nextjs-webpack-stable-lazy-discovery-disabled14401
✅ nextjs-webpack-stable-lazy-discovery-enabled14401
✅ nitro-stable119026
✅ nuxt-stable119026
✅ sveltekit-stable13807
✅ vite-stable119026
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack14500
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable120025
✅ e2e-local-dev-tanstack-start-120025
✅ e2e-local-postgres-nest-stable119026
✅ e2e-local-postgres-tanstack-start-119026
✅ e2e-local-prod-nest-stable120025
✅ e2e-local-prod-tanstack-start-120025
✅ e2e-vercel-prod-tanstack-start119026

📋 View full workflow run

@github-actions

github-actionsBot commented May 18, 2026

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

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

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro0.040s (-5.4% 🟢)1.005s (~)0.965s101.00x
💻 LocalExpress0.041s (-2.8%)1.005s (~)0.964s101.03x
💻 LocalNext.js (Turbopack)0.060s (-4.0%)1.005s (~)0.945s101.51x
🐘 PostgresNitro0.062s (-5.2% 🟢)1.012s (~)0.950s101.56x
🐘 PostgresExpress0.067s (+9.0% 🔺)1.014s (~)0.947s101.67x
🐘 PostgresNext.js (Turbopack)0.068s (-8.6% 🟢)1.013s (~)0.945s101.71x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express0.414s (+52.9% 🔺)2.276s (-11.8% 🟢)1.863s101.00x
▲ VercelNitro0.420s (+11.1% 🔺)2.271s (+8.1% 🔺)1.851s101.01x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.093s (~)2.006s (~)0.913s101.00x
💻 LocalExpress1.094s (~)2.006s (~)0.912s101.00x
🐘 PostgresNitro1.105s (-0.7%)2.010s (~)0.906s101.01x
🐘 PostgresExpress1.114s (~)2.009s (~)0.895s101.02x
💻 LocalNext.js (Turbopack)1.136s (+0.7%)2.007s (~)0.871s101.04x
🐘 PostgresNext.js (Turbopack)1.150s (+0.6%)2.009s (~)0.859s101.05x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.681s (-4.1%)3.608s (-2.7%)1.927s101.00x
▲ VercelNitro32.051s (+1812.5% 🔺)34.165s (+890.4% 🔺)2.114s1019.06x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express10.500s (~)11.022s (~)0.523s31.00x
🐘 PostgresExpress10.570s (~)11.020s (~)0.451s31.01x
🐘 PostgresNitro10.578s (~)11.023s (~)0.444s31.01x
💻 LocalNitro10.651s (+1.0%)11.022s (~)0.372s31.01x
💻 LocalNext.js (Turbopack)10.792s (~)11.021s (~)0.229s31.03x
🐘 PostgresNext.js (Turbopack)10.937s (-0.7%)11.351s (-2.9%)0.415s31.04x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro13.882s (+4.1%)16.196s (+7.0% 🔺)2.314s21.00x
▲ VercelExpress14.281s (+6.5% 🔺)16.108s (+2.4%)1.827s21.03x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express13.701s (-1.0%)14.026s (~)0.325s51.00x
💻 LocalNitro13.738s (-0.7%)14.027s (-1.4%)0.289s51.00x
🐘 PostgresExpress13.763s (-0.5%)14.022s (~)0.259s51.00x
🐘 PostgresNitro13.921s (+0.7%)14.023s (~)0.102s51.02x
💻 LocalNext.js (Turbopack)14.364s (-0.9%)15.030s (~)0.665s41.05x
🐘 PostgresNext.js (Turbopack)14.456s (~)15.014s (~)0.558s41.06x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro21.579s (-3.8%)23.291s (-3.3%)1.712s31.00x
▲ VercelExpress24.509s (+14.1% 🔺)26.394s (+14.4% 🔺)1.885s31.14x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro12.331s (-1.1%)13.026s (~)0.695s71.00x
💻 LocalExpress12.446s (~)13.025s (~)0.579s71.01x
🐘 PostgresExpress12.526s (-1.7%)13.020s (~)0.494s71.02x
🐘 PostgresNitro12.719s (+1.1%)13.025s (~)0.307s71.03x
💻 LocalNext.js (Turbopack)13.813s (+1.0%)14.169s (+1.0%)0.356s71.12x
🐘 PostgresNext.js (Turbopack)13.852s (-0.9%)14.161s (-1.0%)0.309s71.12x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro29.649s (-6.5% 🟢)31.500s (-5.4% 🟢)1.851s31.00x
▲ VercelExpress31.179s (+5.1% 🔺)33.707s (+8.2% 🔺)2.528s31.05x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express1.208s (+1.5%)2.006s (~)0.798s151.00x
🐘 PostgresNitro1.208s (~)2.009s (~)0.800s151.00x
🐘 PostgresExpress1.231s (+2.4%)2.008s (~)0.777s151.02x
💻 LocalNitro1.279s (+4.6%)2.006s (~)0.726s151.06x
🐘 PostgresNext.js (Turbopack)1.339s (+6.4% 🔺)2.074s (+3.3%)0.734s151.11x
💻 LocalNext.js (Turbopack)1.368s (+1.4%)2.006s (~)0.638s151.13x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.970s (+16.4% 🔺)4.450s (-1.1%)1.480s71.00x
▲ VercelNitro3.506s (-5.2% 🟢)5.306s (+2.4%)1.799s61.18x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.366s (+8.5% 🔺)2.394s (+19.3% 🔺)1.028s131.00x
🐘 PostgresExpress1.389s (+10.0% 🔺)2.508s (+25.0% 🔺)1.119s121.02x
🐘 PostgresNext.js (Turbopack)1.618s (+15.5% 🔺)2.393s (+19.3% 🔺)0.776s131.18x
💻 LocalNitro1.704s (~)2.006s (~)0.302s151.25x
💻 LocalExpress1.729s (-6.1% 🟢)2.006s (-6.7% 🟢)0.277s151.27x
💻 LocalNext.js (Turbopack)1.842s (-2.0%)2.074s (-3.5%)0.232s151.35x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.196s (-27.7% 🟢)4.831s (-21.0% 🟢)1.635s71.00x
▲ VercelNitro3.207s (-48.6% 🟢)4.657s (-39.9% 🟢)1.450s71.00x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.680s (+19.4% 🔺)4.141s (+106.2% 🔺)2.461s81.00x
🐘 PostgresExpress1.718s (+22.5% 🔺)3.889s (+93.6% 🔺)2.172s81.02x
🐘 PostgresNext.js (Turbopack)3.508s (+94.5% 🔺)4.297s (+87.1% 🔺)0.789s72.09x
💻 LocalNitro4.696s (-10.5% 🟢)5.347s (-11.1% 🟢)0.650s62.80x
💻 LocalExpress5.272s (-2.1%)6.013s (-2.7%)0.742s53.14x
💻 LocalNext.js (Turbopack)5.395s (-5.4% 🟢)6.014s (-3.2%)0.619s53.21x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.396s (-51.8% 🟢)6.218s (-45.2% 🟢)1.823s61.00x
▲ VercelNitro4.696s (-72.4% 🟢)6.708s (-63.3% 🟢)2.012s51.07x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.221s (+0.8%)2.074s (+3.3%)0.853s151.00x
🐘 PostgresNitro1.224s (+2.8%)2.008s (~)0.784s151.00x
🐘 PostgresNext.js (Turbopack)1.278s (+1.2%)2.008s (~)0.729s151.05x
💻 LocalNext.js (Turbopack)1.391s (+2.9%)2.006s (~)0.615s151.14x
💻 LocalNitro1.502s (~)2.006s (~)0.505s151.23x
💻 LocalExpress1.539s (-7.2% 🟢)2.006s (-3.3%)0.467s151.26x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.632s (+2.1%)4.218s (~)1.586s81.00x
▲ VercelExpress2.986s (+10.6% 🔺)4.712s (~)1.726s71.13x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.394s (+9.6% 🔺)2.224s (+10.8% 🔺)0.830s141.00x
🐘 PostgresExpress1.424s (+9.8% 🔺)2.317s (+15.3% 🔺)0.892s131.02x
🐘 PostgresNext.js (Turbopack)1.506s (+7.3% 🔺)2.150s (+7.1% 🔺)0.644s141.08x
💻 LocalExpress1.865s (-13.6% 🟢)2.295s (-14.2% 🟢)0.429s141.34x
💻 LocalNext.js (Turbopack)2.055s (~)2.591s (-13.9% 🟢)0.536s121.47x
💻 LocalNitro2.153s (-0.8%)2.676s (+3.2%)0.523s121.55x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.328s (-2.9%)5.132s (+0.6%)1.803s61.00x
▲ VercelNitro3.433s (-28.9% 🟢)5.018s (-21.6% 🟢)1.585s61.03x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.617s (+16.5% 🔺)4.299s (+114.1% 🔺)2.682s71.00x
🐘 PostgresExpress1.837s (+31.1% 🔺)3.891s (+93.5% 🔺)2.053s81.14x
🐘 PostgresNext.js (Turbopack)3.367s (+86.2% 🔺)4.582s (+106.0% 🔺)1.215s72.08x
💻 LocalExpress4.294s (-32.4% 🟢)5.016s (-28.5% 🟢)0.721s62.66x
💻 LocalNitro5.167s (-5.0%)5.851s (-2.7%)0.685s63.19x
💻 LocalNext.js (Turbopack)5.712s (-5.7% 🟢)6.017s (-11.7% 🟢)0.305s53.53x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.990s (-21.8% 🟢)5.843s (-19.3% 🟢)1.853s61.00x
▲ VercelNitro4.324s (-38.2% 🟢)6.347s (-26.2% 🟢)2.023s51.08x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro0.589s (-1.8%)1.005s (~)0.416s601.00x
🐘 PostgresExpress0.603s (~)1.023s (+1.6%)0.421s591.02x
🐘 PostgresNitro0.603s (+4.1%)1.007s (~)0.404s601.02x
💻 LocalExpress0.611s (~)1.039s (+3.4%)0.429s581.04x
🐘 PostgresNext.js (Turbopack)0.811s (-5.0% 🟢)1.006s (-1.8%)0.194s601.38x
💻 LocalNext.js (Turbopack)0.892s (+3.3%)1.057s (+3.5%)0.166s571.51x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express5.921s (-41.6% 🟢)7.609s (-35.4% 🟢)1.688s81.00x
▲ VercelNitro6.497s (+31.0% 🔺)8.122s (+24.7% 🔺)1.625s81.10x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.360s (-5.1% 🟢)2.008s (~)0.647s451.00x
🐘 PostgresNitro1.410s (-1.8%)2.008s (-1.1%)0.598s451.04x
💻 LocalNitro1.508s (~)2.006s (~)0.497s451.11x
💻 LocalExpress1.526s (-1.8%)2.028s (~)0.502s451.12x
🐘 PostgresNext.js (Turbopack)1.894s (-6.7% 🟢)2.053s (-22.7% 🟢)0.159s441.39x
💻 LocalNext.js (Turbopack)2.084s (-0.7%)3.008s (~)0.924s301.53x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro14.267s (+8.9% 🔺)16.133s (+11.6% 🔺)1.866s61.00x
▲ VercelExpress15.632s (+8.8% 🔺)17.362s (+7.5% 🔺)1.729s61.10x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro2.756s (-1.4%)3.059s (-3.4%)0.303s401.00x
🐘 PostgresExpress2.805s (~)3.112s (-0.8%)0.307s391.02x
💻 LocalExpress3.225s (-1.8%)3.977s (-0.8%)0.752s311.17x
💻 LocalNitro3.248s (-1.5%)4.009s (~)0.761s301.18x
🐘 PostgresNext.js (Turbopack)3.766s (-5.5% 🟢)4.043s (-6.7% 🟢)0.276s301.37x
💻 LocalNext.js (Turbopack)4.291s (-1.3%)5.010s (~)0.720s241.56x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express32.130s (+22.5% 🔺)34.248s (+21.4% 🔺)2.117s41.00x
▲ VercelNitro34.856s (+19.7% 🔺)36.875s (+19.7% 🔺)2.019s41.08x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.244s (+11.1% 🔺)1.007s (~)0.763s601.00x
🐘 PostgresExpress0.250s (+7.4% 🔺)1.006s (~)0.756s601.03x
🐘 PostgresNext.js (Turbopack)0.289s (+4.2%)1.006s (~)0.717s601.19x
💻 LocalExpress0.423s (+2.4%)1.004s (~)0.582s601.74x
💻 LocalNitro0.431s (~)1.004s (-1.7%)0.573s601.77x
💻 LocalNext.js (Turbopack)0.569s (+4.1%)1.005s (~)0.436s602.33x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.874s (-27.0% 🟢)3.565s (-13.2% 🟢)1.692s171.00x
▲ VercelNitro2.088s (-26.0% 🟢)3.684s (-14.7% 🟢)1.596s171.11x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.393s (+9.5% 🔺)1.053s (+4.7%)0.661s861.00x
🐘 PostgresExpress0.411s (+9.7% 🔺)1.041s (+3.5%)0.630s871.05x
🐘 PostgresNext.js (Turbopack)0.583s (+16.8% 🔺)1.091s (+8.4% 🔺)0.508s831.48x
💻 LocalNitro2.128s (-2.5%)2.737s (+0.8%)0.609s335.42x
💻 LocalExpress2.226s (+9.2% 🔺)2.766s (+9.1% 🔺)0.540s335.67x
💻 LocalNext.js (Turbopack)2.521s (+2.9%)3.218s (+2.2%)0.697s296.42x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.431s (-62.9% 🟢)4.945s (-54.5% 🟢)1.514s191.00x
▲ VercelExpress21.388s (+295.0% 🔺)23.082s (+226.3% 🔺)1.694s176.23x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.795s (+16.7% 🔺)1.567s (+55.7% 🔺)0.772s771.00x
🐘 PostgresExpress0.805s (+15.6% 🔺)1.419s (+41.0% 🔺)0.614s851.01x
🐘 PostgresNext.js (Turbopack)2.760s (+174.5% 🔺)3.681s (+98.5% 🔺)0.921s333.47x
💻 LocalExpress9.388s (+0.8%)10.195s (+3.3%)0.807s1211.80x
💻 LocalNitro9.416s (+1.5%)9.874s (+0.8%)0.458s1311.84x
💻 LocalNext.js (Turbopack)10.703s (-2.2%)11.576s (+0.8%)0.873s1113.46x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express6.877s (-63.6% 🟢)8.574s (-58.2% 🟢)1.697s141.00x
▲ VercelNitro8.266s (-57.1% 🟢)10.222s (-53.8% 🟢)1.957s121.20x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express1.160s (-0.7%)2.005s (~)0.010s (-14.5% 🟢)2.017s (~)0.857s101.00x
🐘 PostgresNitro1.165s (-0.7%)2.001s (~)0.001s (-15.4% 🟢)2.010s (~)0.846s101.00x
💻 LocalNitro1.167s (~)2.005s (~)0.010s (~)2.018s (~)0.851s101.01x
🐘 PostgresExpress1.172s (-0.6%)2.000s (~)0.001s (-8.3% 🟢)2.010s (~)0.838s101.01x
💻 LocalNext.js (Turbopack)1.211s (~)2.004s (~)0.013s (+0.8%)2.020s (~)0.809s101.04x
🐘 PostgresNext.js (Turbopack)1.234s (-0.9%)2.002s (~)0.001s (+7.7% 🔺)2.011s (~)0.777s101.06x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.797s (+14.3% 🔺)4.260s (+26.7% 🔺)0.638s (-32.8% 🟢)5.340s (+12.0% 🔺)2.543s101.00x
▲ VercelExpress32.861s (+1415.8% 🔺)3.322s (+3.7%)1.152s (+26.8% 🔺)35.409s (+673.0% 🔺)2.548s1011.75x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro | Express

stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.566s (-1.8%)2.009s (~)0.012s (-1.6%)2.023s (~)0.457s301.00x
💻 LocalExpress1.568s (~)2.010s (~)0.013s (+14.2% 🔺)2.026s (~)0.457s301.00x
🐘 PostgresExpress1.579s (-1.4%)2.006s (~)0.005s (+7.1% 🔺)2.027s (~)0.448s301.01x
🐘 PostgresNitro1.616s (~)2.006s (~)0.005s (-8.7% 🟢)2.025s (~)0.408s301.03x
💻 LocalNext.js (Turbopack)1.730s (-0.6%)2.010s (~)0.012s (-1.9%)2.025s (~)0.295s301.10x
🐘 PostgresNext.js (Turbopack)1.755s (-1.7%)2.010s (~)0.005s (+2.7%)2.026s (~)0.270s301.12x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express5.998s (-1.2%)7.151s (-2.0%)0.235s (-10.5% 🟢)7.964s (~)1.966s81.00x
▲ VercelNitro6.812s (+15.1% 🔺)7.759s (+10.4% 🔺)0.245s (-47.7% 🟢)8.558s (+7.1% 🔺)1.745s81.14x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Express | Nitro

10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.798s (+12.6% 🔺)1.068s (+2.4%)0.000s (-66.1% 🟢)1.088s (+2.3%)0.291s561.00x
🐘 PostgresExpress0.805s (+13.8% 🔺)1.084s (+6.9% 🔺)0.000s (+221.8% 🔺)1.099s (+6.8% 🔺)0.293s551.01x
🐘 PostgresNext.js (Turbopack)0.998s (+15.9% 🔺)1.463s (+34.1% 🔺)0.000s (-100.0% 🟢)1.471s (+33.9% 🔺)0.473s411.25x
💻 LocalNitro1.386s (-3.8%)2.013s (~)0.001s (+260.0% 🔺)2.015s (~)0.630s301.74x
💻 LocalExpress1.391s (+3.1%)2.013s (~)0.000s (+175.0% 🔺)2.015s (~)0.624s301.74x
💻 LocalNext.js (Turbopack)1.462s (~)2.013s (~)0.000s (-40.0% 🟢)2.016s (~)0.554s301.83x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.320s (-1.2%)4.629s (+1.2%)0.000s (-100.0% 🟢)5.052s (~)1.731s121.00x
▲ VercelNitro3.595s (-5.5% 🟢)4.764s (-1.3%)0.001s (+Infinity% 🔺)5.183s (-0.7%)1.588s121.08x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Express | Nitro

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

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.508s (+13.1% 🔺)2.032s (~)0.000s (NaN%)2.061s (~)0.553s301.00x
🐘 PostgresExpress1.669s (+9.4% 🔺)2.217s (~)0.000s (-50.0% 🟢)2.252s (~)0.583s271.11x
🐘 PostgresNext.js (Turbopack)2.069s (+19.6% 🔺)2.404s (+6.3% 🔺)0.000s (+Infinity% 🔺)2.432s (+7.1% 🔺)0.363s251.37x
💻 LocalNext.js (Turbopack)2.812s (-1.4%)3.357s (-1.7%)0.001s (+100.0% 🔺)3.361s (-1.7%)0.549s181.86x
💻 LocalNitro2.994s (-5.5% 🟢)3.613s (-5.8% 🟢)0.000s (+119.6% 🔺)3.620s (-5.8% 🟢)0.626s171.98x
💻 LocalExpress3.129s (+1.1%)3.902s (+6.3% 🔺)0.001s (+44.9% 🔺)3.905s (+6.2% 🔺)0.776s162.07x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro5.070s (-31.3% 🟢)6.077s (-23.4% 🟢)0.000s (+Infinity% 🔺)6.502s (-28.0% 🟢)1.433s101.00x
▲ VercelExpress5.299s (~)6.264s (-4.8%)0.000s (NaN%)6.717s (-6.6% 🟢)1.418s91.05x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro | Express

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNitro10/21
🐘 PostgresNitro14/21
▲ VercelExpress13/21
Fastest World by Framework

Winner determined by most benchmark wins

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

Worlds:

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

📋 View full workflow run


Some benchmark jobs failed:

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

Check the workflow run for details.

Comment threaddocs/content/docs/v4/api-reference/workflow/create-hook.mdx Outdated

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

Adds a new hook.ready promise to let workflow code wait for hook registration (hook_created) independently of receiving payloads, enabling deterministic token claiming for idempotency patterns (and consistent conflict handling via HookConflictError).

Changes:

  • Extend the public Hook interface with ready: Promise<void> and implement it in the workflow VM hook runtime.
  • Resolve/reject ready waiters based on hook_created / hook_conflict, including suspension behavior when awaiting readiness before registration is recorded.
  • Add tests and update v4/v5 documentation to describe hook.ready (and webhook readiness via the shared Hook interface).

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 1 comment.

Show a summary per file
FileDescription
packages/core/src/workflow/hook.tsImplements hook.ready tracking/queuing and resolves/rejects it on hook_created/hook_conflict, including suspension when needed.
packages/core/src/workflow/hook.test.tsAdds unit tests covering ready resolution, suspension behavior, conflict rejection, and ensuring payload consumption is unchanged.
packages/core/src/workflow.test.tsAdds integration tests verifying suspension, resolution, and conflict behavior when hook.ready is used inside runWorkflow.
packages/core/src/create-hook.tsExtends the public Hook interface with documented ready: Promise<void>.
docs/content/docs/v5/how-it-works/event-sourcing.mdxDocuments that hook.ready rejects on hook conflicts in addition to payload awaiting.
docs/content/docs/v5/foundations/hooks.mdxAdds a section explaining when/how to use hook.ready to wait for registration.
docs/content/docs/v5/api-reference/workflow/create-webhook.mdxNotes ready availability on returned webhook objects and how to use it before sharing the URL.
docs/content/docs/v5/api-reference/workflow/create-hook.mdxDocuments hook.ready and adds a registration-focused example.
docs/content/docs/v4/how-it-works/event-sourcing.mdxMirrors v5 event-sourcing updates for hook.ready conflict semantics.
docs/content/docs/v4/foundations/hooks.mdxMirrors v5 “Waiting for Hook Registration” guidance.
docs/content/docs/v4/api-reference/workflow/create-webhook.mdxMirrors v5 webhook ready documentation note.
docs/content/docs/v4/api-reference/workflow/create-hook.mdxMirrors v5 hook.ready API docs and example.
.changeset/hook-ready.mdDeclares minor bumps for @workflow/core and workflow to ship the new API.

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

Comment threadpackages/core/src/workflow/hook.test.ts Outdated
Comment threaddocs/content/docs/v4/api-reference/workflow/create-webhook.mdx Outdated
Comment threaddocs/content/docs/v5/api-reference/workflow/create-webhook.mdx Outdated

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

Copilot reviewed 20 out of 20 changed files in this pull request and generated 1 comment.

Comment threadpackages/core/src/workflow/hook.ts Outdated
Comment threadpackages/core/src/runtime.ts Outdated
pranaygpand others added 3 commits May 18, 2026 21:44
…hook-ready
* origin-https/main: (119 commits)
fix: settle aborted parallel steps before completing abortParallelWorkflow (#2244)
Add native v4 workflow attribute events (#2226)
Version Packages (beta) (#2326)
[ci] Fix flaky windows unit tests (#2359)
Capture Vercel runtime logs when e2e Vercel Prod lanes fail (#2356)
Fix e2e failure reporting under vitest 4 and preserve fetch error causes (#2355)
[core] Fix process crash from rejected waitUntil promises (#2336)
[core] Remove duplicate `waitUntil` for suspension handler async operations (#2345)
Prevent local tests from hanging (#2338)
feat(core): add optional namespace for queue topic prefix (#2305)
[web-shared] Show precise durations in the new trace viewer (#2335)
Validate unique workflow step IDs at build time (#2018)
Move run attributes into their own detail card (#2327)
[core] Forward-port stream reconnect to getReadable level (#2318)
[docs] Add "Step executed multiple times" error page (#2310)
Fix flickering on the detail panel when navigating the trace viewer (#2325)
Version Packages (beta) (#2254)
Patch vulnerable package dependencies (#2301)
Deprecate DurableAgent and update it with WorkflowAgent in v5 docs (#2285)
[codex] Fix partial world-local exclusive writes (#2296)
...
# Conflicts:
#	packages/core/src/runtime.ts
#	packages/core/src/runtime/suspension-handler.ts
- hook.hasConflict resolves true when the token is owned by another
active hook, false once registration is committed — no throw, so
workflows can branch on conflicts early. Awaiting it suspends the
workflow to commit the hook registration (createHook alone does not).
- Chain the already-created fast-path through promiseQueue so
resolution order matches event-log order (review feedback).
- Skip inline step execution when a suspension has an awaited hook
creation so the hasConflict continuation can advance independently
of step execution (review feedback).
- Update unit tests, e2e tests, workbench workflows, and v4/v5 docs.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

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

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

Comment threaddocs/content/docs/v5/api-reference/workflow/create-webhook.mdx Outdated
Comment threaddocs/content/docs/v4/api-reference/workflow/create-webhook.mdx Outdated

@TooTallNateTooTallNate left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Approve — determinism-safe design, strong test coverage; one docs-sequencing flag

I built @workflow/core from this branch and ran the full unit suite locally (1146 tests, matching the PR's claimed validation) plus the three new test files individually (116 tests). All green.

The semantics hold up under determinism scrutiny

This touches the workflow VM's most replay-sensitive machinery, so I traced the settlement paths carefully:

  • Every hasConflict settlement goes through ctx.promiseQueue — both fast-paths (hasCreated / conflict-already-known) and the event-driven paths (hook_created → resolve false, hook_conflict → resolve true). Resolution order always matches event-log order, which is the invariant that keeps replay deterministic.
  • The hook_conflict consumer drains payload and hasConflict waiters synchronously (deferring actual settlement through promiseQueue), so the null-event handler can't see them and raise a spurious suspension — same pattern the existing payload path uses.
  • The suspension trigger condition (hasConflictPromises.length > 0 && !hasCreated && !hasConflict in the null-event handler, plus the eventLogEmpty fast-path in createHasConflictPromise) correctly mirrors the payload-awaiter pattern for both orderings of "awaited before/after log exhaustion." Both are unit-tested.
  • hasAwaitedHookCreation only affects scheduling, never event-log content — it decides which invocation executes steps (queue-all + timeoutSeconds: 0 re-invoke instead of inline), so there's no replay-divergence surface. The reasoning in the runtime.ts comment is exactly right: running a step inline would serialize work the workflow expressed as parallel (hook.hasConflict.then(() => stepB()) racing await stepA()).
  • The EntityConflictError path returning hasAwaitedHookCreation: true is correct and subtle: on a crash-replay where the hook event already exists, the awaiter still needs an immediate re-invocation to consume hook_created and resolve — silently swallowing it would hang the awaiter until something else woke the run.
  • The e2e parallelism proof (hookHasConflictThenStepParallelWorkflow: 100ms stepB triggered via the hasConflict continuation must complete while 10s stepA is still running, asserted via step timestamps) tests the actual claim end-to-end rather than just the mechanism.

My earlier docs comments were both addressed (the create-hook example now explicitly points to awaiting the hook/iterating for payload data).

One sequencing flag: the v4 docs (should-fix before merge, or fast-follow the backport)

hasConflict does not exist on stable — I checked origin/stable:packages/core/src/create-hook.ts and there are zero references. But this PR adds the API to the v4 docs (docs/content/docs/v4/...), and docs deploy from main. The moment this merges, the live v4 documentation advertises a property that no released 4.x SDK has — and it'll stay wrong until a backport merges to stableand ships in a 4.x release.

Two clean options:

  1. Drop the v4 docs pages from this PR and let the backport PR carry them (the backport flow cherry-picks docs/content/ normally, so they'd land together with the code on stable — though note they'd then need a separate main-side commit since v4 docs render from main), or
  2. Keep them but treat the stable backport + 4.x release as an immediate follow-up so the docs-reality gap is measured in days.

Given the changeset is minor (correct for a new API) and this is a self-contained feature — squarely in the auto-backport criteria — option 2 is probably fine, but it should be a deliberate choice, not an accident.

Non-blocking notes

  • get hasConflict() returns a fresh promise per property access (hook.hasConflict !== hook.hasConflict). Each settles deterministically through promiseQueue, so it's correctness-neutral, but identity-unstable promises from a property (vs a method) can surprise — e.g. registering two waiters from what reads like one. Caching the promise per hook after first access would be friendlier and would also make the hasConflictAwaiter flag-set happen exactly once. Fine as-is.
  • The local hasConflict boolean inside createCreateHook now shadows the public property name; a rename (sawConflictEvent?) would help the next reader.

CI

The two red E2E jobs are baseline flakes unrelated to this PR: astro fails AbortController > abortFromStepWorkflow (the abort area #2244 just touched on main) and hono fails resilient start: addTenWorkflow ... run_created returns 500 (known flake). No hook test failed anywhere. The required check needs a re-run to go green before merge.

Nice work — this is a well-reasoned addition in a part of the codebase where it's very easy to be subtly wrong.

TooTallNateand others added 3 commits June 11, 2026 14:29
State both resolution values explicitly (true = token already owned,
false = registered) instead of a parenthetical that only described the
false case.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment threadAGENTS.md
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
expect.anything()
);
expect(result.hasAwaitedHookCreation).toBe(true);
expect(result.timeoutSeconds).toBeUndefined();

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.

Test assertions check non-existent result.timeoutSeconds property, making toBeUndefined() assertions pass trivially without verifying actual behavior.

Fix on Vercel

@github-actions

Copy link
Copy Markdown
Contributor

No backport to stable for e163422 (AI decision).

This is a new minor feature adding the hook.hasConflict API to the public hook interface, intended for the next major release on main. It's a self-contained feature addition (not a bug fix) and the changeset is marked as a minor bump for the beta channel. Per the criteria, new feature additions targeted at the next major release should generally stay on main rather than be backported to stable.

To override, re-run the Backport to stable workflow manually via workflow_dispatch and paste this commit SHA into the ref input:

e163422551e80eda88a15fec3d3d574a00c53e93

@github-actionsgithub-actionsBot mentioned this pull request Jun 11, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Backport PR opened against stable: #2372. Merge conflicts were resolved by AI — please review carefully. (backport job run)

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.

3 participants

@pranaygp@TooTallNate
, '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 `hook.hasConflict` for early hook conflict detection by pranaygp · Pull Request #2015 · vercel/workflow · GitHub
Skip to content

Add hook.hasConflict for early hook conflict detection - #2015

Merged
TooTallNate merged 9 commits into
mainfrom
pranaygp/codex/hook-ready
Jun 11, 2026
Merged

Add hook.hasConflict for early hook conflict detection#2015
TooTallNate merged 9 commits into
mainfrom
pranaygp/codex/hook-ready

Conversation

@pranaygp

@pranaygppranaygp commented May 18, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds hook.hasConflict, a lazy Promise<boolean> getter that lets workflow code commit hook registration and check for token conflicts without waiting for hook payload data.

Calling createHook() alone does not register the hook — registration is only committed when the workflow suspends. Awaiting hook.hasConflict suspends the workflow to commit the registration, then resolves with false once hook_created is recorded, or true if another active hook already owns the token. Unlike awaiting the hook payload (which rejects with HookConflictError on conflict), hasConflict never throws, so workflows can branch on conflicts early:

using hook=createHook({token: `order:${orderId}`});if(awaithook.hasConflict){return;// another run already owns this token}

This gives workflows a way to claim a deterministic hook token for idempotency before running duplicate-sensitive work, and is a prerequisite for #2011, where the idempotency docs/examples should use await hook.hasConflict instead of requiring consumers to wait for or resume hook data just to claim the token.

Changes

  • Add Hook.hasConflict: Promise<boolean> to the public hook interface.
  • Resolve pending hasConflict waiters with false on hook_created and true on hook_conflict; both fast-paths settle through promiseQueue so resolution order matches event-log order.
  • Suspend workflow execution when hook.hasConflict is awaited before hook creation is recorded.
  • Skip inline step execution when a suspension contains an awaited hook creation, so a hasConflict continuation (e.g. hook.hasConflict.then(() => stepB())) can advance in parallel with other steps instead of only after they finish.
  • Document hook.hasConflict in the v4 and v5 hook/webhook docs and event-sourcing pages, including the suspension-commits-registration semantics.
  • Add core unit tests (resolution, suspension, conflict, payload preservation, suspension-handler scheduling) and e2e tests (registration without payload, non-blocking steps, parallel continuation scheduling, conflict resolves true).

Validation

  • cd packages/core && pnpm typecheck && pnpm vitest run src/ (1146 tests)
  • Hook e2e suite against local nextjs-turbopack dev server: pnpm vitest run packages/core/e2e/e2e.test.ts -t 'hook' (20 tests, including the new parallel-continuation test)

Docs Preview

Changed pages on the docs preview deployment:

Pagev4v5
createHook() API referencev4v5
createWebhook() API referencev4v5
Foundations: Hooksv4v5
How It Works: Event Sourcingv4v5

CopilotAI review requested due to automatic review settings May 18, 2026 23:22
@pranaygp
pranaygp requested a review from a team as a code ownerMay 18, 2026 23:22
@changeset-bot

changeset-botBot commented May 18, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 2342dae

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

This PR includes changesets to release 17 packages
NameType
@workflow/coreMinor
workflowMinor
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/webPatch
@workflow/world-testingPatch
@workflow/aiMajor
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch
@workflow/nuxtPatch

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 May 18, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented May 18, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production137602191595
✅ 💻 Local Development181102192030
✅ 📦 Local Production181102192030
✅ 🐘 Local Postgres179702332030
✅ 🪟 Windows14500145
✅ 📋 Other83701781015
Total7777010688845

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro119026
✅ example119026
✅ express119026
✅ fastify119026
✅ hono119026
✅ nextjs-turbopack14302
✅ nextjs-webpack14302
✅ nitro119026
✅ nuxt119026
✅ sveltekit13807
✅ vite119026
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable120025
✅ express-stable120025
✅ fastify-stable120025
✅ hono-stable120025
✅ nextjs-turbopack-canary126019
✅ nextjs-turbopack-stable-lazy-discovery-disabled14500
✅ nextjs-turbopack-stable-lazy-discovery-enabled14500
✅ nextjs-webpack-canary126019
✅ nextjs-webpack-stable-lazy-discovery-disabled14500
✅ nextjs-webpack-stable-lazy-discovery-enabled14500
✅ nitro-stable120025
✅ nuxt-stable120025
✅ sveltekit-stable13906
✅ vite-stable120025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable120025
✅ express-stable120025
✅ fastify-stable120025
✅ hono-stable120025
✅ nextjs-turbopack-canary126019
✅ nextjs-turbopack-stable-lazy-discovery-disabled14500
✅ nextjs-turbopack-stable-lazy-discovery-enabled14500
✅ nextjs-webpack-canary126019
✅ nextjs-webpack-stable-lazy-discovery-disabled14500
✅ nextjs-webpack-stable-lazy-discovery-enabled14500
✅ nitro-stable120025
✅ nuxt-stable120025
✅ sveltekit-stable13906
✅ vite-stable120025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable119026
✅ express-stable119026
✅ fastify-stable119026
✅ hono-stable119026
✅ nextjs-turbopack-canary125020
✅ nextjs-turbopack-stable-lazy-discovery-disabled14401
✅ nextjs-turbopack-stable-lazy-discovery-enabled14401
✅ nextjs-webpack-canary125020
✅ nextjs-webpack-stable-lazy-discovery-disabled14401
✅ nextjs-webpack-stable-lazy-discovery-enabled14401
✅ nitro-stable119026
✅ nuxt-stable119026
✅ sveltekit-stable13807
✅ vite-stable119026
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack14500
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable120025
✅ e2e-local-dev-tanstack-start-120025
✅ e2e-local-postgres-nest-stable119026
✅ e2e-local-postgres-tanstack-start-119026
✅ e2e-local-prod-nest-stable120025
✅ e2e-local-prod-tanstack-start-120025
✅ e2e-vercel-prod-tanstack-start119026

📋 View full workflow run

@github-actions

github-actionsBot commented May 18, 2026

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

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

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro0.040s (-5.4% 🟢)1.005s (~)0.965s101.00x
💻 LocalExpress0.041s (-2.8%)1.005s (~)0.964s101.03x
💻 LocalNext.js (Turbopack)0.060s (-4.0%)1.005s (~)0.945s101.51x
🐘 PostgresNitro0.062s (-5.2% 🟢)1.012s (~)0.950s101.56x
🐘 PostgresExpress0.067s (+9.0% 🔺)1.014s (~)0.947s101.67x
🐘 PostgresNext.js (Turbopack)0.068s (-8.6% 🟢)1.013s (~)0.945s101.71x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express0.414s (+52.9% 🔺)2.276s (-11.8% 🟢)1.863s101.00x
▲ VercelNitro0.420s (+11.1% 🔺)2.271s (+8.1% 🔺)1.851s101.01x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.093s (~)2.006s (~)0.913s101.00x
💻 LocalExpress1.094s (~)2.006s (~)0.912s101.00x
🐘 PostgresNitro1.105s (-0.7%)2.010s (~)0.906s101.01x
🐘 PostgresExpress1.114s (~)2.009s (~)0.895s101.02x
💻 LocalNext.js (Turbopack)1.136s (+0.7%)2.007s (~)0.871s101.04x
🐘 PostgresNext.js (Turbopack)1.150s (+0.6%)2.009s (~)0.859s101.05x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.681s (-4.1%)3.608s (-2.7%)1.927s101.00x
▲ VercelNitro32.051s (+1812.5% 🔺)34.165s (+890.4% 🔺)2.114s1019.06x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express10.500s (~)11.022s (~)0.523s31.00x
🐘 PostgresExpress10.570s (~)11.020s (~)0.451s31.01x
🐘 PostgresNitro10.578s (~)11.023s (~)0.444s31.01x
💻 LocalNitro10.651s (+1.0%)11.022s (~)0.372s31.01x
💻 LocalNext.js (Turbopack)10.792s (~)11.021s (~)0.229s31.03x
🐘 PostgresNext.js (Turbopack)10.937s (-0.7%)11.351s (-2.9%)0.415s31.04x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro13.882s (+4.1%)16.196s (+7.0% 🔺)2.314s21.00x
▲ VercelExpress14.281s (+6.5% 🔺)16.108s (+2.4%)1.827s21.03x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express13.701s (-1.0%)14.026s (~)0.325s51.00x
💻 LocalNitro13.738s (-0.7%)14.027s (-1.4%)0.289s51.00x
🐘 PostgresExpress13.763s (-0.5%)14.022s (~)0.259s51.00x
🐘 PostgresNitro13.921s (+0.7%)14.023s (~)0.102s51.02x
💻 LocalNext.js (Turbopack)14.364s (-0.9%)15.030s (~)0.665s41.05x
🐘 PostgresNext.js (Turbopack)14.456s (~)15.014s (~)0.558s41.06x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro21.579s (-3.8%)23.291s (-3.3%)1.712s31.00x
▲ VercelExpress24.509s (+14.1% 🔺)26.394s (+14.4% 🔺)1.885s31.14x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro12.331s (-1.1%)13.026s (~)0.695s71.00x
💻 LocalExpress12.446s (~)13.025s (~)0.579s71.01x
🐘 PostgresExpress12.526s (-1.7%)13.020s (~)0.494s71.02x
🐘 PostgresNitro12.719s (+1.1%)13.025s (~)0.307s71.03x
💻 LocalNext.js (Turbopack)13.813s (+1.0%)14.169s (+1.0%)0.356s71.12x
🐘 PostgresNext.js (Turbopack)13.852s (-0.9%)14.161s (-1.0%)0.309s71.12x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro29.649s (-6.5% 🟢)31.500s (-5.4% 🟢)1.851s31.00x
▲ VercelExpress31.179s (+5.1% 🔺)33.707s (+8.2% 🔺)2.528s31.05x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express1.208s (+1.5%)2.006s (~)0.798s151.00x
🐘 PostgresNitro1.208s (~)2.009s (~)0.800s151.00x
🐘 PostgresExpress1.231s (+2.4%)2.008s (~)0.777s151.02x
💻 LocalNitro1.279s (+4.6%)2.006s (~)0.726s151.06x
🐘 PostgresNext.js (Turbopack)1.339s (+6.4% 🔺)2.074s (+3.3%)0.734s151.11x
💻 LocalNext.js (Turbopack)1.368s (+1.4%)2.006s (~)0.638s151.13x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.970s (+16.4% 🔺)4.450s (-1.1%)1.480s71.00x
▲ VercelNitro3.506s (-5.2% 🟢)5.306s (+2.4%)1.799s61.18x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.366s (+8.5% 🔺)2.394s (+19.3% 🔺)1.028s131.00x
🐘 PostgresExpress1.389s (+10.0% 🔺)2.508s (+25.0% 🔺)1.119s121.02x
🐘 PostgresNext.js (Turbopack)1.618s (+15.5% 🔺)2.393s (+19.3% 🔺)0.776s131.18x
💻 LocalNitro1.704s (~)2.006s (~)0.302s151.25x
💻 LocalExpress1.729s (-6.1% 🟢)2.006s (-6.7% 🟢)0.277s151.27x
💻 LocalNext.js (Turbopack)1.842s (-2.0%)2.074s (-3.5%)0.232s151.35x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.196s (-27.7% 🟢)4.831s (-21.0% 🟢)1.635s71.00x
▲ VercelNitro3.207s (-48.6% 🟢)4.657s (-39.9% 🟢)1.450s71.00x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.680s (+19.4% 🔺)4.141s (+106.2% 🔺)2.461s81.00x
🐘 PostgresExpress1.718s (+22.5% 🔺)3.889s (+93.6% 🔺)2.172s81.02x
🐘 PostgresNext.js (Turbopack)3.508s (+94.5% 🔺)4.297s (+87.1% 🔺)0.789s72.09x
💻 LocalNitro4.696s (-10.5% 🟢)5.347s (-11.1% 🟢)0.650s62.80x
💻 LocalExpress5.272s (-2.1%)6.013s (-2.7%)0.742s53.14x
💻 LocalNext.js (Turbopack)5.395s (-5.4% 🟢)6.014s (-3.2%)0.619s53.21x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.396s (-51.8% 🟢)6.218s (-45.2% 🟢)1.823s61.00x
▲ VercelNitro4.696s (-72.4% 🟢)6.708s (-63.3% 🟢)2.012s51.07x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.221s (+0.8%)2.074s (+3.3%)0.853s151.00x
🐘 PostgresNitro1.224s (+2.8%)2.008s (~)0.784s151.00x
🐘 PostgresNext.js (Turbopack)1.278s (+1.2%)2.008s (~)0.729s151.05x
💻 LocalNext.js (Turbopack)1.391s (+2.9%)2.006s (~)0.615s151.14x
💻 LocalNitro1.502s (~)2.006s (~)0.505s151.23x
💻 LocalExpress1.539s (-7.2% 🟢)2.006s (-3.3%)0.467s151.26x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.632s (+2.1%)4.218s (~)1.586s81.00x
▲ VercelExpress2.986s (+10.6% 🔺)4.712s (~)1.726s71.13x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.394s (+9.6% 🔺)2.224s (+10.8% 🔺)0.830s141.00x
🐘 PostgresExpress1.424s (+9.8% 🔺)2.317s (+15.3% 🔺)0.892s131.02x
🐘 PostgresNext.js (Turbopack)1.506s (+7.3% 🔺)2.150s (+7.1% 🔺)0.644s141.08x
💻 LocalExpress1.865s (-13.6% 🟢)2.295s (-14.2% 🟢)0.429s141.34x
💻 LocalNext.js (Turbopack)2.055s (~)2.591s (-13.9% 🟢)0.536s121.47x
💻 LocalNitro2.153s (-0.8%)2.676s (+3.2%)0.523s121.55x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.328s (-2.9%)5.132s (+0.6%)1.803s61.00x
▲ VercelNitro3.433s (-28.9% 🟢)5.018s (-21.6% 🟢)1.585s61.03x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.617s (+16.5% 🔺)4.299s (+114.1% 🔺)2.682s71.00x
🐘 PostgresExpress1.837s (+31.1% 🔺)3.891s (+93.5% 🔺)2.053s81.14x
🐘 PostgresNext.js (Turbopack)3.367s (+86.2% 🔺)4.582s (+106.0% 🔺)1.215s72.08x
💻 LocalExpress4.294s (-32.4% 🟢)5.016s (-28.5% 🟢)0.721s62.66x
💻 LocalNitro5.167s (-5.0%)5.851s (-2.7%)0.685s63.19x
💻 LocalNext.js (Turbopack)5.712s (-5.7% 🟢)6.017s (-11.7% 🟢)0.305s53.53x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.990s (-21.8% 🟢)5.843s (-19.3% 🟢)1.853s61.00x
▲ VercelNitro4.324s (-38.2% 🟢)6.347s (-26.2% 🟢)2.023s51.08x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro0.589s (-1.8%)1.005s (~)0.416s601.00x
🐘 PostgresExpress0.603s (~)1.023s (+1.6%)0.421s591.02x
🐘 PostgresNitro0.603s (+4.1%)1.007s (~)0.404s601.02x
💻 LocalExpress0.611s (~)1.039s (+3.4%)0.429s581.04x
🐘 PostgresNext.js (Turbopack)0.811s (-5.0% 🟢)1.006s (-1.8%)0.194s601.38x
💻 LocalNext.js (Turbopack)0.892s (+3.3%)1.057s (+3.5%)0.166s571.51x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express5.921s (-41.6% 🟢)7.609s (-35.4% 🟢)1.688s81.00x
▲ VercelNitro6.497s (+31.0% 🔺)8.122s (+24.7% 🔺)1.625s81.10x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.360s (-5.1% 🟢)2.008s (~)0.647s451.00x
🐘 PostgresNitro1.410s (-1.8%)2.008s (-1.1%)0.598s451.04x
💻 LocalNitro1.508s (~)2.006s (~)0.497s451.11x
💻 LocalExpress1.526s (-1.8%)2.028s (~)0.502s451.12x
🐘 PostgresNext.js (Turbopack)1.894s (-6.7% 🟢)2.053s (-22.7% 🟢)0.159s441.39x
💻 LocalNext.js (Turbopack)2.084s (-0.7%)3.008s (~)0.924s301.53x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro14.267s (+8.9% 🔺)16.133s (+11.6% 🔺)1.866s61.00x
▲ VercelExpress15.632s (+8.8% 🔺)17.362s (+7.5% 🔺)1.729s61.10x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro2.756s (-1.4%)3.059s (-3.4%)0.303s401.00x
🐘 PostgresExpress2.805s (~)3.112s (-0.8%)0.307s391.02x
💻 LocalExpress3.225s (-1.8%)3.977s (-0.8%)0.752s311.17x
💻 LocalNitro3.248s (-1.5%)4.009s (~)0.761s301.18x
🐘 PostgresNext.js (Turbopack)3.766s (-5.5% 🟢)4.043s (-6.7% 🟢)0.276s301.37x
💻 LocalNext.js (Turbopack)4.291s (-1.3%)5.010s (~)0.720s241.56x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express32.130s (+22.5% 🔺)34.248s (+21.4% 🔺)2.117s41.00x
▲ VercelNitro34.856s (+19.7% 🔺)36.875s (+19.7% 🔺)2.019s41.08x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.244s (+11.1% 🔺)1.007s (~)0.763s601.00x
🐘 PostgresExpress0.250s (+7.4% 🔺)1.006s (~)0.756s601.03x
🐘 PostgresNext.js (Turbopack)0.289s (+4.2%)1.006s (~)0.717s601.19x
💻 LocalExpress0.423s (+2.4%)1.004s (~)0.582s601.74x
💻 LocalNitro0.431s (~)1.004s (-1.7%)0.573s601.77x
💻 LocalNext.js (Turbopack)0.569s (+4.1%)1.005s (~)0.436s602.33x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.874s (-27.0% 🟢)3.565s (-13.2% 🟢)1.692s171.00x
▲ VercelNitro2.088s (-26.0% 🟢)3.684s (-14.7% 🟢)1.596s171.11x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.393s (+9.5% 🔺)1.053s (+4.7%)0.661s861.00x
🐘 PostgresExpress0.411s (+9.7% 🔺)1.041s (+3.5%)0.630s871.05x
🐘 PostgresNext.js (Turbopack)0.583s (+16.8% 🔺)1.091s (+8.4% 🔺)0.508s831.48x
💻 LocalNitro2.128s (-2.5%)2.737s (+0.8%)0.609s335.42x
💻 LocalExpress2.226s (+9.2% 🔺)2.766s (+9.1% 🔺)0.540s335.67x
💻 LocalNext.js (Turbopack)2.521s (+2.9%)3.218s (+2.2%)0.697s296.42x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.431s (-62.9% 🟢)4.945s (-54.5% 🟢)1.514s191.00x
▲ VercelExpress21.388s (+295.0% 🔺)23.082s (+226.3% 🔺)1.694s176.23x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.795s (+16.7% 🔺)1.567s (+55.7% 🔺)0.772s771.00x
🐘 PostgresExpress0.805s (+15.6% 🔺)1.419s (+41.0% 🔺)0.614s851.01x
🐘 PostgresNext.js (Turbopack)2.760s (+174.5% 🔺)3.681s (+98.5% 🔺)0.921s333.47x
💻 LocalExpress9.388s (+0.8%)10.195s (+3.3%)0.807s1211.80x
💻 LocalNitro9.416s (+1.5%)9.874s (+0.8%)0.458s1311.84x
💻 LocalNext.js (Turbopack)10.703s (-2.2%)11.576s (+0.8%)0.873s1113.46x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express6.877s (-63.6% 🟢)8.574s (-58.2% 🟢)1.697s141.00x
▲ VercelNitro8.266s (-57.1% 🟢)10.222s (-53.8% 🟢)1.957s121.20x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express1.160s (-0.7%)2.005s (~)0.010s (-14.5% 🟢)2.017s (~)0.857s101.00x
🐘 PostgresNitro1.165s (-0.7%)2.001s (~)0.001s (-15.4% 🟢)2.010s (~)0.846s101.00x
💻 LocalNitro1.167s (~)2.005s (~)0.010s (~)2.018s (~)0.851s101.01x
🐘 PostgresExpress1.172s (-0.6%)2.000s (~)0.001s (-8.3% 🟢)2.010s (~)0.838s101.01x
💻 LocalNext.js (Turbopack)1.211s (~)2.004s (~)0.013s (+0.8%)2.020s (~)0.809s101.04x
🐘 PostgresNext.js (Turbopack)1.234s (-0.9%)2.002s (~)0.001s (+7.7% 🔺)2.011s (~)0.777s101.06x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.797s (+14.3% 🔺)4.260s (+26.7% 🔺)0.638s (-32.8% 🟢)5.340s (+12.0% 🔺)2.543s101.00x
▲ VercelExpress32.861s (+1415.8% 🔺)3.322s (+3.7%)1.152s (+26.8% 🔺)35.409s (+673.0% 🔺)2.548s1011.75x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro | Express

stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.566s (-1.8%)2.009s (~)0.012s (-1.6%)2.023s (~)0.457s301.00x
💻 LocalExpress1.568s (~)2.010s (~)0.013s (+14.2% 🔺)2.026s (~)0.457s301.00x
🐘 PostgresExpress1.579s (-1.4%)2.006s (~)0.005s (+7.1% 🔺)2.027s (~)0.448s301.01x
🐘 PostgresNitro1.616s (~)2.006s (~)0.005s (-8.7% 🟢)2.025s (~)0.408s301.03x
💻 LocalNext.js (Turbopack)1.730s (-0.6%)2.010s (~)0.012s (-1.9%)2.025s (~)0.295s301.10x
🐘 PostgresNext.js (Turbopack)1.755s (-1.7%)2.010s (~)0.005s (+2.7%)2.026s (~)0.270s301.12x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express5.998s (-1.2%)7.151s (-2.0%)0.235s (-10.5% 🟢)7.964s (~)1.966s81.00x
▲ VercelNitro6.812s (+15.1% 🔺)7.759s (+10.4% 🔺)0.245s (-47.7% 🟢)8.558s (+7.1% 🔺)1.745s81.14x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Express | Nitro

10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.798s (+12.6% 🔺)1.068s (+2.4%)0.000s (-66.1% 🟢)1.088s (+2.3%)0.291s561.00x
🐘 PostgresExpress0.805s (+13.8% 🔺)1.084s (+6.9% 🔺)0.000s (+221.8% 🔺)1.099s (+6.8% 🔺)0.293s551.01x
🐘 PostgresNext.js (Turbopack)0.998s (+15.9% 🔺)1.463s (+34.1% 🔺)0.000s (-100.0% 🟢)1.471s (+33.9% 🔺)0.473s411.25x
💻 LocalNitro1.386s (-3.8%)2.013s (~)0.001s (+260.0% 🔺)2.015s (~)0.630s301.74x
💻 LocalExpress1.391s (+3.1%)2.013s (~)0.000s (+175.0% 🔺)2.015s (~)0.624s301.74x
💻 LocalNext.js (Turbopack)1.462s (~)2.013s (~)0.000s (-40.0% 🟢)2.016s (~)0.554s301.83x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.320s (-1.2%)4.629s (+1.2%)0.000s (-100.0% 🟢)5.052s (~)1.731s121.00x
▲ VercelNitro3.595s (-5.5% 🟢)4.764s (-1.3%)0.001s (+Infinity% 🔺)5.183s (-0.7%)1.588s121.08x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Express | Nitro

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

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.508s (+13.1% 🔺)2.032s (~)0.000s (NaN%)2.061s (~)0.553s301.00x
🐘 PostgresExpress1.669s (+9.4% 🔺)2.217s (~)0.000s (-50.0% 🟢)2.252s (~)0.583s271.11x
🐘 PostgresNext.js (Turbopack)2.069s (+19.6% 🔺)2.404s (+6.3% 🔺)0.000s (+Infinity% 🔺)2.432s (+7.1% 🔺)0.363s251.37x
💻 LocalNext.js (Turbopack)2.812s (-1.4%)3.357s (-1.7%)0.001s (+100.0% 🔺)3.361s (-1.7%)0.549s181.86x
💻 LocalNitro2.994s (-5.5% 🟢)3.613s (-5.8% 🟢)0.000s (+119.6% 🔺)3.620s (-5.8% 🟢)0.626s171.98x
💻 LocalExpress3.129s (+1.1%)3.902s (+6.3% 🔺)0.001s (+44.9% 🔺)3.905s (+6.2% 🔺)0.776s162.07x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro5.070s (-31.3% 🟢)6.077s (-23.4% 🟢)0.000s (+Infinity% 🔺)6.502s (-28.0% 🟢)1.433s101.00x
▲ VercelExpress5.299s (~)6.264s (-4.8%)0.000s (NaN%)6.717s (-6.6% 🟢)1.418s91.05x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro | Express

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNitro10/21
🐘 PostgresNitro14/21
▲ VercelExpress13/21
Fastest World by Framework

Winner determined by most benchmark wins

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

Worlds:

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

📋 View full workflow run


Some benchmark jobs failed:

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

Check the workflow run for details.

Comment threaddocs/content/docs/v4/api-reference/workflow/create-hook.mdx Outdated

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

Adds a new hook.ready promise to let workflow code wait for hook registration (hook_created) independently of receiving payloads, enabling deterministic token claiming for idempotency patterns (and consistent conflict handling via HookConflictError).

Changes:

  • Extend the public Hook interface with ready: Promise<void> and implement it in the workflow VM hook runtime.
  • Resolve/reject ready waiters based on hook_created / hook_conflict, including suspension behavior when awaiting readiness before registration is recorded.
  • Add tests and update v4/v5 documentation to describe hook.ready (and webhook readiness via the shared Hook interface).

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 1 comment.

Show a summary per file
FileDescription
packages/core/src/workflow/hook.tsImplements hook.ready tracking/queuing and resolves/rejects it on hook_created/hook_conflict, including suspension when needed.
packages/core/src/workflow/hook.test.tsAdds unit tests covering ready resolution, suspension behavior, conflict rejection, and ensuring payload consumption is unchanged.
packages/core/src/workflow.test.tsAdds integration tests verifying suspension, resolution, and conflict behavior when hook.ready is used inside runWorkflow.
packages/core/src/create-hook.tsExtends the public Hook interface with documented ready: Promise<void>.
docs/content/docs/v5/how-it-works/event-sourcing.mdxDocuments that hook.ready rejects on hook conflicts in addition to payload awaiting.
docs/content/docs/v5/foundations/hooks.mdxAdds a section explaining when/how to use hook.ready to wait for registration.
docs/content/docs/v5/api-reference/workflow/create-webhook.mdxNotes ready availability on returned webhook objects and how to use it before sharing the URL.
docs/content/docs/v5/api-reference/workflow/create-hook.mdxDocuments hook.ready and adds a registration-focused example.
docs/content/docs/v4/how-it-works/event-sourcing.mdxMirrors v5 event-sourcing updates for hook.ready conflict semantics.
docs/content/docs/v4/foundations/hooks.mdxMirrors v5 “Waiting for Hook Registration” guidance.
docs/content/docs/v4/api-reference/workflow/create-webhook.mdxMirrors v5 webhook ready documentation note.
docs/content/docs/v4/api-reference/workflow/create-hook.mdxMirrors v5 hook.ready API docs and example.
.changeset/hook-ready.mdDeclares minor bumps for @workflow/core and workflow to ship the new API.

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

Comment threadpackages/core/src/workflow/hook.test.ts Outdated
Comment threaddocs/content/docs/v4/api-reference/workflow/create-webhook.mdx Outdated
Comment threaddocs/content/docs/v5/api-reference/workflow/create-webhook.mdx Outdated

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

Copilot reviewed 20 out of 20 changed files in this pull request and generated 1 comment.

Comment threadpackages/core/src/workflow/hook.ts Outdated
Comment threadpackages/core/src/runtime.ts Outdated
pranaygpand others added 3 commits May 18, 2026 21:44
…hook-ready
* origin-https/main: (119 commits)
fix: settle aborted parallel steps before completing abortParallelWorkflow (#2244)
Add native v4 workflow attribute events (#2226)
Version Packages (beta) (#2326)
[ci] Fix flaky windows unit tests (#2359)
Capture Vercel runtime logs when e2e Vercel Prod lanes fail (#2356)
Fix e2e failure reporting under vitest 4 and preserve fetch error causes (#2355)
[core] Fix process crash from rejected waitUntil promises (#2336)
[core] Remove duplicate `waitUntil` for suspension handler async operations (#2345)
Prevent local tests from hanging (#2338)
feat(core): add optional namespace for queue topic prefix (#2305)
[web-shared] Show precise durations in the new trace viewer (#2335)
Validate unique workflow step IDs at build time (#2018)
Move run attributes into their own detail card (#2327)
[core] Forward-port stream reconnect to getReadable level (#2318)
[docs] Add "Step executed multiple times" error page (#2310)
Fix flickering on the detail panel when navigating the trace viewer (#2325)
Version Packages (beta) (#2254)
Patch vulnerable package dependencies (#2301)
Deprecate DurableAgent and update it with WorkflowAgent in v5 docs (#2285)
[codex] Fix partial world-local exclusive writes (#2296)
...
# Conflicts:
#	packages/core/src/runtime.ts
#	packages/core/src/runtime/suspension-handler.ts
- hook.hasConflict resolves true when the token is owned by another
active hook, false once registration is committed — no throw, so
workflows can branch on conflicts early. Awaiting it suspends the
workflow to commit the hook registration (createHook alone does not).
- Chain the already-created fast-path through promiseQueue so
resolution order matches event-log order (review feedback).
- Skip inline step execution when a suspension has an awaited hook
creation so the hasConflict continuation can advance independently
of step execution (review feedback).
- Update unit tests, e2e tests, workbench workflows, and v4/v5 docs.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

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

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

Comment threaddocs/content/docs/v5/api-reference/workflow/create-webhook.mdx Outdated
Comment threaddocs/content/docs/v4/api-reference/workflow/create-webhook.mdx Outdated

@TooTallNateTooTallNate left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Approve — determinism-safe design, strong test coverage; one docs-sequencing flag

I built @workflow/core from this branch and ran the full unit suite locally (1146 tests, matching the PR's claimed validation) plus the three new test files individually (116 tests). All green.

The semantics hold up under determinism scrutiny

This touches the workflow VM's most replay-sensitive machinery, so I traced the settlement paths carefully:

  • Every hasConflict settlement goes through ctx.promiseQueue — both fast-paths (hasCreated / conflict-already-known) and the event-driven paths (hook_created → resolve false, hook_conflict → resolve true). Resolution order always matches event-log order, which is the invariant that keeps replay deterministic.
  • The hook_conflict consumer drains payload and hasConflict waiters synchronously (deferring actual settlement through promiseQueue), so the null-event handler can't see them and raise a spurious suspension — same pattern the existing payload path uses.
  • The suspension trigger condition (hasConflictPromises.length > 0 && !hasCreated && !hasConflict in the null-event handler, plus the eventLogEmpty fast-path in createHasConflictPromise) correctly mirrors the payload-awaiter pattern for both orderings of "awaited before/after log exhaustion." Both are unit-tested.
  • hasAwaitedHookCreation only affects scheduling, never event-log content — it decides which invocation executes steps (queue-all + timeoutSeconds: 0 re-invoke instead of inline), so there's no replay-divergence surface. The reasoning in the runtime.ts comment is exactly right: running a step inline would serialize work the workflow expressed as parallel (hook.hasConflict.then(() => stepB()) racing await stepA()).
  • The EntityConflictError path returning hasAwaitedHookCreation: true is correct and subtle: on a crash-replay where the hook event already exists, the awaiter still needs an immediate re-invocation to consume hook_created and resolve — silently swallowing it would hang the awaiter until something else woke the run.
  • The e2e parallelism proof (hookHasConflictThenStepParallelWorkflow: 100ms stepB triggered via the hasConflict continuation must complete while 10s stepA is still running, asserted via step timestamps) tests the actual claim end-to-end rather than just the mechanism.

My earlier docs comments were both addressed (the create-hook example now explicitly points to awaiting the hook/iterating for payload data).

One sequencing flag: the v4 docs (should-fix before merge, or fast-follow the backport)

hasConflict does not exist on stable — I checked origin/stable:packages/core/src/create-hook.ts and there are zero references. But this PR adds the API to the v4 docs (docs/content/docs/v4/...), and docs deploy from main. The moment this merges, the live v4 documentation advertises a property that no released 4.x SDK has — and it'll stay wrong until a backport merges to stableand ships in a 4.x release.

Two clean options:

  1. Drop the v4 docs pages from this PR and let the backport PR carry them (the backport flow cherry-picks docs/content/ normally, so they'd land together with the code on stable — though note they'd then need a separate main-side commit since v4 docs render from main), or
  2. Keep them but treat the stable backport + 4.x release as an immediate follow-up so the docs-reality gap is measured in days.

Given the changeset is minor (correct for a new API) and this is a self-contained feature — squarely in the auto-backport criteria — option 2 is probably fine, but it should be a deliberate choice, not an accident.

Non-blocking notes

  • get hasConflict() returns a fresh promise per property access (hook.hasConflict !== hook.hasConflict). Each settles deterministically through promiseQueue, so it's correctness-neutral, but identity-unstable promises from a property (vs a method) can surprise — e.g. registering two waiters from what reads like one. Caching the promise per hook after first access would be friendlier and would also make the hasConflictAwaiter flag-set happen exactly once. Fine as-is.
  • The local hasConflict boolean inside createCreateHook now shadows the public property name; a rename (sawConflictEvent?) would help the next reader.

CI

The two red E2E jobs are baseline flakes unrelated to this PR: astro fails AbortController > abortFromStepWorkflow (the abort area #2244 just touched on main) and hono fails resilient start: addTenWorkflow ... run_created returns 500 (known flake). No hook test failed anywhere. The required check needs a re-run to go green before merge.

Nice work — this is a well-reasoned addition in a part of the codebase where it's very easy to be subtly wrong.

TooTallNateand others added 3 commits June 11, 2026 14:29
State both resolution values explicitly (true = token already owned,
false = registered) instead of a parenthetical that only described the
false case.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment threadAGENTS.md
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
expect.anything()
);
expect(result.hasAwaitedHookCreation).toBe(true);
expect(result.timeoutSeconds).toBeUndefined();

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.

Test assertions check non-existent result.timeoutSeconds property, making toBeUndefined() assertions pass trivially without verifying actual behavior.

Fix on Vercel

@github-actions

Copy link
Copy Markdown
Contributor

No backport to stable for e163422 (AI decision).

This is a new minor feature adding the hook.hasConflict API to the public hook interface, intended for the next major release on main. It's a self-contained feature addition (not a bug fix) and the changeset is marked as a minor bump for the beta channel. Per the criteria, new feature additions targeted at the next major release should generally stay on main rather than be backported to stable.

To override, re-run the Backport to stable workflow manually via workflow_dispatch and paste this commit SHA into the ref input:

e163422551e80eda88a15fec3d3d574a00c53e93

@github-actionsgithub-actionsBot mentioned this pull request Jun 11, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Backport PR opened against stable: #2372. Merge conflicts were resolved by AI — please review carefully. (backport job run)

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.

3 participants

@pranaygp@TooTallNate
, '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 `hook.hasConflict` for early hook conflict detection by pranaygp · Pull Request #2015 · vercel/workflow · GitHub
Skip to content

Add hook.hasConflict for early hook conflict detection - #2015

Merged
TooTallNate merged 9 commits into
mainfrom
pranaygp/codex/hook-ready
Jun 11, 2026
Merged

Add hook.hasConflict for early hook conflict detection#2015
TooTallNate merged 9 commits into
mainfrom
pranaygp/codex/hook-ready

Conversation

@pranaygp

@pranaygppranaygp commented May 18, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds hook.hasConflict, a lazy Promise<boolean> getter that lets workflow code commit hook registration and check for token conflicts without waiting for hook payload data.

Calling createHook() alone does not register the hook — registration is only committed when the workflow suspends. Awaiting hook.hasConflict suspends the workflow to commit the registration, then resolves with false once hook_created is recorded, or true if another active hook already owns the token. Unlike awaiting the hook payload (which rejects with HookConflictError on conflict), hasConflict never throws, so workflows can branch on conflicts early:

using hook=createHook({token: `order:${orderId}`});if(awaithook.hasConflict){return;// another run already owns this token}

This gives workflows a way to claim a deterministic hook token for idempotency before running duplicate-sensitive work, and is a prerequisite for #2011, where the idempotency docs/examples should use await hook.hasConflict instead of requiring consumers to wait for or resume hook data just to claim the token.

Changes

  • Add Hook.hasConflict: Promise<boolean> to the public hook interface.
  • Resolve pending hasConflict waiters with false on hook_created and true on hook_conflict; both fast-paths settle through promiseQueue so resolution order matches event-log order.
  • Suspend workflow execution when hook.hasConflict is awaited before hook creation is recorded.
  • Skip inline step execution when a suspension contains an awaited hook creation, so a hasConflict continuation (e.g. hook.hasConflict.then(() => stepB())) can advance in parallel with other steps instead of only after they finish.
  • Document hook.hasConflict in the v4 and v5 hook/webhook docs and event-sourcing pages, including the suspension-commits-registration semantics.
  • Add core unit tests (resolution, suspension, conflict, payload preservation, suspension-handler scheduling) and e2e tests (registration without payload, non-blocking steps, parallel continuation scheduling, conflict resolves true).

Validation

  • cd packages/core && pnpm typecheck && pnpm vitest run src/ (1146 tests)
  • Hook e2e suite against local nextjs-turbopack dev server: pnpm vitest run packages/core/e2e/e2e.test.ts -t 'hook' (20 tests, including the new parallel-continuation test)

Docs Preview

Changed pages on the docs preview deployment:

Pagev4v5
createHook() API referencev4v5
createWebhook() API referencev4v5
Foundations: Hooksv4v5
How It Works: Event Sourcingv4v5

CopilotAI review requested due to automatic review settings May 18, 2026 23:22
@pranaygp
pranaygp requested a review from a team as a code ownerMay 18, 2026 23:22
@changeset-bot

changeset-botBot commented May 18, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 2342dae

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

This PR includes changesets to release 17 packages
NameType
@workflow/coreMinor
workflowMinor
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/webPatch
@workflow/world-testingPatch
@workflow/aiMajor
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch
@workflow/nuxtPatch

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 May 18, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented May 18, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production137602191595
✅ 💻 Local Development181102192030
✅ 📦 Local Production181102192030
✅ 🐘 Local Postgres179702332030
✅ 🪟 Windows14500145
✅ 📋 Other83701781015
Total7777010688845

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro119026
✅ example119026
✅ express119026
✅ fastify119026
✅ hono119026
✅ nextjs-turbopack14302
✅ nextjs-webpack14302
✅ nitro119026
✅ nuxt119026
✅ sveltekit13807
✅ vite119026
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable120025
✅ express-stable120025
✅ fastify-stable120025
✅ hono-stable120025
✅ nextjs-turbopack-canary126019
✅ nextjs-turbopack-stable-lazy-discovery-disabled14500
✅ nextjs-turbopack-stable-lazy-discovery-enabled14500
✅ nextjs-webpack-canary126019
✅ nextjs-webpack-stable-lazy-discovery-disabled14500
✅ nextjs-webpack-stable-lazy-discovery-enabled14500
✅ nitro-stable120025
✅ nuxt-stable120025
✅ sveltekit-stable13906
✅ vite-stable120025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable120025
✅ express-stable120025
✅ fastify-stable120025
✅ hono-stable120025
✅ nextjs-turbopack-canary126019
✅ nextjs-turbopack-stable-lazy-discovery-disabled14500
✅ nextjs-turbopack-stable-lazy-discovery-enabled14500
✅ nextjs-webpack-canary126019
✅ nextjs-webpack-stable-lazy-discovery-disabled14500
✅ nextjs-webpack-stable-lazy-discovery-enabled14500
✅ nitro-stable120025
✅ nuxt-stable120025
✅ sveltekit-stable13906
✅ vite-stable120025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable119026
✅ express-stable119026
✅ fastify-stable119026
✅ hono-stable119026
✅ nextjs-turbopack-canary125020
✅ nextjs-turbopack-stable-lazy-discovery-disabled14401
✅ nextjs-turbopack-stable-lazy-discovery-enabled14401
✅ nextjs-webpack-canary125020
✅ nextjs-webpack-stable-lazy-discovery-disabled14401
✅ nextjs-webpack-stable-lazy-discovery-enabled14401
✅ nitro-stable119026
✅ nuxt-stable119026
✅ sveltekit-stable13807
✅ vite-stable119026
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack14500
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable120025
✅ e2e-local-dev-tanstack-start-120025
✅ e2e-local-postgres-nest-stable119026
✅ e2e-local-postgres-tanstack-start-119026
✅ e2e-local-prod-nest-stable120025
✅ e2e-local-prod-tanstack-start-120025
✅ e2e-vercel-prod-tanstack-start119026

📋 View full workflow run

@github-actions

github-actionsBot commented May 18, 2026

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

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

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro0.040s (-5.4% 🟢)1.005s (~)0.965s101.00x
💻 LocalExpress0.041s (-2.8%)1.005s (~)0.964s101.03x
💻 LocalNext.js (Turbopack)0.060s (-4.0%)1.005s (~)0.945s101.51x
🐘 PostgresNitro0.062s (-5.2% 🟢)1.012s (~)0.950s101.56x
🐘 PostgresExpress0.067s (+9.0% 🔺)1.014s (~)0.947s101.67x
🐘 PostgresNext.js (Turbopack)0.068s (-8.6% 🟢)1.013s (~)0.945s101.71x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express0.414s (+52.9% 🔺)2.276s (-11.8% 🟢)1.863s101.00x
▲ VercelNitro0.420s (+11.1% 🔺)2.271s (+8.1% 🔺)1.851s101.01x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.093s (~)2.006s (~)0.913s101.00x
💻 LocalExpress1.094s (~)2.006s (~)0.912s101.00x
🐘 PostgresNitro1.105s (-0.7%)2.010s (~)0.906s101.01x
🐘 PostgresExpress1.114s (~)2.009s (~)0.895s101.02x
💻 LocalNext.js (Turbopack)1.136s (+0.7%)2.007s (~)0.871s101.04x
🐘 PostgresNext.js (Turbopack)1.150s (+0.6%)2.009s (~)0.859s101.05x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.681s (-4.1%)3.608s (-2.7%)1.927s101.00x
▲ VercelNitro32.051s (+1812.5% 🔺)34.165s (+890.4% 🔺)2.114s1019.06x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express10.500s (~)11.022s (~)0.523s31.00x
🐘 PostgresExpress10.570s (~)11.020s (~)0.451s31.01x
🐘 PostgresNitro10.578s (~)11.023s (~)0.444s31.01x
💻 LocalNitro10.651s (+1.0%)11.022s (~)0.372s31.01x
💻 LocalNext.js (Turbopack)10.792s (~)11.021s (~)0.229s31.03x
🐘 PostgresNext.js (Turbopack)10.937s (-0.7%)11.351s (-2.9%)0.415s31.04x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro13.882s (+4.1%)16.196s (+7.0% 🔺)2.314s21.00x
▲ VercelExpress14.281s (+6.5% 🔺)16.108s (+2.4%)1.827s21.03x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express13.701s (-1.0%)14.026s (~)0.325s51.00x
💻 LocalNitro13.738s (-0.7%)14.027s (-1.4%)0.289s51.00x
🐘 PostgresExpress13.763s (-0.5%)14.022s (~)0.259s51.00x
🐘 PostgresNitro13.921s (+0.7%)14.023s (~)0.102s51.02x
💻 LocalNext.js (Turbopack)14.364s (-0.9%)15.030s (~)0.665s41.05x
🐘 PostgresNext.js (Turbopack)14.456s (~)15.014s (~)0.558s41.06x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro21.579s (-3.8%)23.291s (-3.3%)1.712s31.00x
▲ VercelExpress24.509s (+14.1% 🔺)26.394s (+14.4% 🔺)1.885s31.14x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro12.331s (-1.1%)13.026s (~)0.695s71.00x
💻 LocalExpress12.446s (~)13.025s (~)0.579s71.01x
🐘 PostgresExpress12.526s (-1.7%)13.020s (~)0.494s71.02x
🐘 PostgresNitro12.719s (+1.1%)13.025s (~)0.307s71.03x
💻 LocalNext.js (Turbopack)13.813s (+1.0%)14.169s (+1.0%)0.356s71.12x
🐘 PostgresNext.js (Turbopack)13.852s (-0.9%)14.161s (-1.0%)0.309s71.12x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro29.649s (-6.5% 🟢)31.500s (-5.4% 🟢)1.851s31.00x
▲ VercelExpress31.179s (+5.1% 🔺)33.707s (+8.2% 🔺)2.528s31.05x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express1.208s (+1.5%)2.006s (~)0.798s151.00x
🐘 PostgresNitro1.208s (~)2.009s (~)0.800s151.00x
🐘 PostgresExpress1.231s (+2.4%)2.008s (~)0.777s151.02x
💻 LocalNitro1.279s (+4.6%)2.006s (~)0.726s151.06x
🐘 PostgresNext.js (Turbopack)1.339s (+6.4% 🔺)2.074s (+3.3%)0.734s151.11x
💻 LocalNext.js (Turbopack)1.368s (+1.4%)2.006s (~)0.638s151.13x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.970s (+16.4% 🔺)4.450s (-1.1%)1.480s71.00x
▲ VercelNitro3.506s (-5.2% 🟢)5.306s (+2.4%)1.799s61.18x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.366s (+8.5% 🔺)2.394s (+19.3% 🔺)1.028s131.00x
🐘 PostgresExpress1.389s (+10.0% 🔺)2.508s (+25.0% 🔺)1.119s121.02x
🐘 PostgresNext.js (Turbopack)1.618s (+15.5% 🔺)2.393s (+19.3% 🔺)0.776s131.18x
💻 LocalNitro1.704s (~)2.006s (~)0.302s151.25x
💻 LocalExpress1.729s (-6.1% 🟢)2.006s (-6.7% 🟢)0.277s151.27x
💻 LocalNext.js (Turbopack)1.842s (-2.0%)2.074s (-3.5%)0.232s151.35x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.196s (-27.7% 🟢)4.831s (-21.0% 🟢)1.635s71.00x
▲ VercelNitro3.207s (-48.6% 🟢)4.657s (-39.9% 🟢)1.450s71.00x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.680s (+19.4% 🔺)4.141s (+106.2% 🔺)2.461s81.00x
🐘 PostgresExpress1.718s (+22.5% 🔺)3.889s (+93.6% 🔺)2.172s81.02x
🐘 PostgresNext.js (Turbopack)3.508s (+94.5% 🔺)4.297s (+87.1% 🔺)0.789s72.09x
💻 LocalNitro4.696s (-10.5% 🟢)5.347s (-11.1% 🟢)0.650s62.80x
💻 LocalExpress5.272s (-2.1%)6.013s (-2.7%)0.742s53.14x
💻 LocalNext.js (Turbopack)5.395s (-5.4% 🟢)6.014s (-3.2%)0.619s53.21x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.396s (-51.8% 🟢)6.218s (-45.2% 🟢)1.823s61.00x
▲ VercelNitro4.696s (-72.4% 🟢)6.708s (-63.3% 🟢)2.012s51.07x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.221s (+0.8%)2.074s (+3.3%)0.853s151.00x
🐘 PostgresNitro1.224s (+2.8%)2.008s (~)0.784s151.00x
🐘 PostgresNext.js (Turbopack)1.278s (+1.2%)2.008s (~)0.729s151.05x
💻 LocalNext.js (Turbopack)1.391s (+2.9%)2.006s (~)0.615s151.14x
💻 LocalNitro1.502s (~)2.006s (~)0.505s151.23x
💻 LocalExpress1.539s (-7.2% 🟢)2.006s (-3.3%)0.467s151.26x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.632s (+2.1%)4.218s (~)1.586s81.00x
▲ VercelExpress2.986s (+10.6% 🔺)4.712s (~)1.726s71.13x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.394s (+9.6% 🔺)2.224s (+10.8% 🔺)0.830s141.00x
🐘 PostgresExpress1.424s (+9.8% 🔺)2.317s (+15.3% 🔺)0.892s131.02x
🐘 PostgresNext.js (Turbopack)1.506s (+7.3% 🔺)2.150s (+7.1% 🔺)0.644s141.08x
💻 LocalExpress1.865s (-13.6% 🟢)2.295s (-14.2% 🟢)0.429s141.34x
💻 LocalNext.js (Turbopack)2.055s (~)2.591s (-13.9% 🟢)0.536s121.47x
💻 LocalNitro2.153s (-0.8%)2.676s (+3.2%)0.523s121.55x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.328s (-2.9%)5.132s (+0.6%)1.803s61.00x
▲ VercelNitro3.433s (-28.9% 🟢)5.018s (-21.6% 🟢)1.585s61.03x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.617s (+16.5% 🔺)4.299s (+114.1% 🔺)2.682s71.00x
🐘 PostgresExpress1.837s (+31.1% 🔺)3.891s (+93.5% 🔺)2.053s81.14x
🐘 PostgresNext.js (Turbopack)3.367s (+86.2% 🔺)4.582s (+106.0% 🔺)1.215s72.08x
💻 LocalExpress4.294s (-32.4% 🟢)5.016s (-28.5% 🟢)0.721s62.66x
💻 LocalNitro5.167s (-5.0%)5.851s (-2.7%)0.685s63.19x
💻 LocalNext.js (Turbopack)5.712s (-5.7% 🟢)6.017s (-11.7% 🟢)0.305s53.53x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.990s (-21.8% 🟢)5.843s (-19.3% 🟢)1.853s61.00x
▲ VercelNitro4.324s (-38.2% 🟢)6.347s (-26.2% 🟢)2.023s51.08x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro0.589s (-1.8%)1.005s (~)0.416s601.00x
🐘 PostgresExpress0.603s (~)1.023s (+1.6%)0.421s591.02x
🐘 PostgresNitro0.603s (+4.1%)1.007s (~)0.404s601.02x
💻 LocalExpress0.611s (~)1.039s (+3.4%)0.429s581.04x
🐘 PostgresNext.js (Turbopack)0.811s (-5.0% 🟢)1.006s (-1.8%)0.194s601.38x
💻 LocalNext.js (Turbopack)0.892s (+3.3%)1.057s (+3.5%)0.166s571.51x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express5.921s (-41.6% 🟢)7.609s (-35.4% 🟢)1.688s81.00x
▲ VercelNitro6.497s (+31.0% 🔺)8.122s (+24.7% 🔺)1.625s81.10x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.360s (-5.1% 🟢)2.008s (~)0.647s451.00x
🐘 PostgresNitro1.410s (-1.8%)2.008s (-1.1%)0.598s451.04x
💻 LocalNitro1.508s (~)2.006s (~)0.497s451.11x
💻 LocalExpress1.526s (-1.8%)2.028s (~)0.502s451.12x
🐘 PostgresNext.js (Turbopack)1.894s (-6.7% 🟢)2.053s (-22.7% 🟢)0.159s441.39x
💻 LocalNext.js (Turbopack)2.084s (-0.7%)3.008s (~)0.924s301.53x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro14.267s (+8.9% 🔺)16.133s (+11.6% 🔺)1.866s61.00x
▲ VercelExpress15.632s (+8.8% 🔺)17.362s (+7.5% 🔺)1.729s61.10x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro2.756s (-1.4%)3.059s (-3.4%)0.303s401.00x
🐘 PostgresExpress2.805s (~)3.112s (-0.8%)0.307s391.02x
💻 LocalExpress3.225s (-1.8%)3.977s (-0.8%)0.752s311.17x
💻 LocalNitro3.248s (-1.5%)4.009s (~)0.761s301.18x
🐘 PostgresNext.js (Turbopack)3.766s (-5.5% 🟢)4.043s (-6.7% 🟢)0.276s301.37x
💻 LocalNext.js (Turbopack)4.291s (-1.3%)5.010s (~)0.720s241.56x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express32.130s (+22.5% 🔺)34.248s (+21.4% 🔺)2.117s41.00x
▲ VercelNitro34.856s (+19.7% 🔺)36.875s (+19.7% 🔺)2.019s41.08x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.244s (+11.1% 🔺)1.007s (~)0.763s601.00x
🐘 PostgresExpress0.250s (+7.4% 🔺)1.006s (~)0.756s601.03x
🐘 PostgresNext.js (Turbopack)0.289s (+4.2%)1.006s (~)0.717s601.19x
💻 LocalExpress0.423s (+2.4%)1.004s (~)0.582s601.74x
💻 LocalNitro0.431s (~)1.004s (-1.7%)0.573s601.77x
💻 LocalNext.js (Turbopack)0.569s (+4.1%)1.005s (~)0.436s602.33x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.874s (-27.0% 🟢)3.565s (-13.2% 🟢)1.692s171.00x
▲ VercelNitro2.088s (-26.0% 🟢)3.684s (-14.7% 🟢)1.596s171.11x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.393s (+9.5% 🔺)1.053s (+4.7%)0.661s861.00x
🐘 PostgresExpress0.411s (+9.7% 🔺)1.041s (+3.5%)0.630s871.05x
🐘 PostgresNext.js (Turbopack)0.583s (+16.8% 🔺)1.091s (+8.4% 🔺)0.508s831.48x
💻 LocalNitro2.128s (-2.5%)2.737s (+0.8%)0.609s335.42x
💻 LocalExpress2.226s (+9.2% 🔺)2.766s (+9.1% 🔺)0.540s335.67x
💻 LocalNext.js (Turbopack)2.521s (+2.9%)3.218s (+2.2%)0.697s296.42x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.431s (-62.9% 🟢)4.945s (-54.5% 🟢)1.514s191.00x
▲ VercelExpress21.388s (+295.0% 🔺)23.082s (+226.3% 🔺)1.694s176.23x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.795s (+16.7% 🔺)1.567s (+55.7% 🔺)0.772s771.00x
🐘 PostgresExpress0.805s (+15.6% 🔺)1.419s (+41.0% 🔺)0.614s851.01x
🐘 PostgresNext.js (Turbopack)2.760s (+174.5% 🔺)3.681s (+98.5% 🔺)0.921s333.47x
💻 LocalExpress9.388s (+0.8%)10.195s (+3.3%)0.807s1211.80x
💻 LocalNitro9.416s (+1.5%)9.874s (+0.8%)0.458s1311.84x
💻 LocalNext.js (Turbopack)10.703s (-2.2%)11.576s (+0.8%)0.873s1113.46x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express6.877s (-63.6% 🟢)8.574s (-58.2% 🟢)1.697s141.00x
▲ VercelNitro8.266s (-57.1% 🟢)10.222s (-53.8% 🟢)1.957s121.20x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express1.160s (-0.7%)2.005s (~)0.010s (-14.5% 🟢)2.017s (~)0.857s101.00x
🐘 PostgresNitro1.165s (-0.7%)2.001s (~)0.001s (-15.4% 🟢)2.010s (~)0.846s101.00x
💻 LocalNitro1.167s (~)2.005s (~)0.010s (~)2.018s (~)0.851s101.01x
🐘 PostgresExpress1.172s (-0.6%)2.000s (~)0.001s (-8.3% 🟢)2.010s (~)0.838s101.01x
💻 LocalNext.js (Turbopack)1.211s (~)2.004s (~)0.013s (+0.8%)2.020s (~)0.809s101.04x
🐘 PostgresNext.js (Turbopack)1.234s (-0.9%)2.002s (~)0.001s (+7.7% 🔺)2.011s (~)0.777s101.06x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.797s (+14.3% 🔺)4.260s (+26.7% 🔺)0.638s (-32.8% 🟢)5.340s (+12.0% 🔺)2.543s101.00x
▲ VercelExpress32.861s (+1415.8% 🔺)3.322s (+3.7%)1.152s (+26.8% 🔺)35.409s (+673.0% 🔺)2.548s1011.75x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro | Express

stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.566s (-1.8%)2.009s (~)0.012s (-1.6%)2.023s (~)0.457s301.00x
💻 LocalExpress1.568s (~)2.010s (~)0.013s (+14.2% 🔺)2.026s (~)0.457s301.00x
🐘 PostgresExpress1.579s (-1.4%)2.006s (~)0.005s (+7.1% 🔺)2.027s (~)0.448s301.01x
🐘 PostgresNitro1.616s (~)2.006s (~)0.005s (-8.7% 🟢)2.025s (~)0.408s301.03x
💻 LocalNext.js (Turbopack)1.730s (-0.6%)2.010s (~)0.012s (-1.9%)2.025s (~)0.295s301.10x
🐘 PostgresNext.js (Turbopack)1.755s (-1.7%)2.010s (~)0.005s (+2.7%)2.026s (~)0.270s301.12x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express5.998s (-1.2%)7.151s (-2.0%)0.235s (-10.5% 🟢)7.964s (~)1.966s81.00x
▲ VercelNitro6.812s (+15.1% 🔺)7.759s (+10.4% 🔺)0.245s (-47.7% 🟢)8.558s (+7.1% 🔺)1.745s81.14x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Express | Nitro

10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.798s (+12.6% 🔺)1.068s (+2.4%)0.000s (-66.1% 🟢)1.088s (+2.3%)0.291s561.00x
🐘 PostgresExpress0.805s (+13.8% 🔺)1.084s (+6.9% 🔺)0.000s (+221.8% 🔺)1.099s (+6.8% 🔺)0.293s551.01x
🐘 PostgresNext.js (Turbopack)0.998s (+15.9% 🔺)1.463s (+34.1% 🔺)0.000s (-100.0% 🟢)1.471s (+33.9% 🔺)0.473s411.25x
💻 LocalNitro1.386s (-3.8%)2.013s (~)0.001s (+260.0% 🔺)2.015s (~)0.630s301.74x
💻 LocalExpress1.391s (+3.1%)2.013s (~)0.000s (+175.0% 🔺)2.015s (~)0.624s301.74x
💻 LocalNext.js (Turbopack)1.462s (~)2.013s (~)0.000s (-40.0% 🟢)2.016s (~)0.554s301.83x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.320s (-1.2%)4.629s (+1.2%)0.000s (-100.0% 🟢)5.052s (~)1.731s121.00x
▲ VercelNitro3.595s (-5.5% 🟢)4.764s (-1.3%)0.001s (+Infinity% 🔺)5.183s (-0.7%)1.588s121.08x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Express | Nitro

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

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.508s (+13.1% 🔺)2.032s (~)0.000s (NaN%)2.061s (~)0.553s301.00x
🐘 PostgresExpress1.669s (+9.4% 🔺)2.217s (~)0.000s (-50.0% 🟢)2.252s (~)0.583s271.11x
🐘 PostgresNext.js (Turbopack)2.069s (+19.6% 🔺)2.404s (+6.3% 🔺)0.000s (+Infinity% 🔺)2.432s (+7.1% 🔺)0.363s251.37x
💻 LocalNext.js (Turbopack)2.812s (-1.4%)3.357s (-1.7%)0.001s (+100.0% 🔺)3.361s (-1.7%)0.549s181.86x
💻 LocalNitro2.994s (-5.5% 🟢)3.613s (-5.8% 🟢)0.000s (+119.6% 🔺)3.620s (-5.8% 🟢)0.626s171.98x
💻 LocalExpress3.129s (+1.1%)3.902s (+6.3% 🔺)0.001s (+44.9% 🔺)3.905s (+6.2% 🔺)0.776s162.07x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro5.070s (-31.3% 🟢)6.077s (-23.4% 🟢)0.000s (+Infinity% 🔺)6.502s (-28.0% 🟢)1.433s101.00x
▲ VercelExpress5.299s (~)6.264s (-4.8%)0.000s (NaN%)6.717s (-6.6% 🟢)1.418s91.05x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro | Express

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNitro10/21
🐘 PostgresNitro14/21
▲ VercelExpress13/21
Fastest World by Framework

Winner determined by most benchmark wins

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

Worlds:

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

📋 View full workflow run


Some benchmark jobs failed:

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

Check the workflow run for details.

Comment threaddocs/content/docs/v4/api-reference/workflow/create-hook.mdx Outdated

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

Adds a new hook.ready promise to let workflow code wait for hook registration (hook_created) independently of receiving payloads, enabling deterministic token claiming for idempotency patterns (and consistent conflict handling via HookConflictError).

Changes:

  • Extend the public Hook interface with ready: Promise<void> and implement it in the workflow VM hook runtime.
  • Resolve/reject ready waiters based on hook_created / hook_conflict, including suspension behavior when awaiting readiness before registration is recorded.
  • Add tests and update v4/v5 documentation to describe hook.ready (and webhook readiness via the shared Hook interface).

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 1 comment.

Show a summary per file
FileDescription
packages/core/src/workflow/hook.tsImplements hook.ready tracking/queuing and resolves/rejects it on hook_created/hook_conflict, including suspension when needed.
packages/core/src/workflow/hook.test.tsAdds unit tests covering ready resolution, suspension behavior, conflict rejection, and ensuring payload consumption is unchanged.
packages/core/src/workflow.test.tsAdds integration tests verifying suspension, resolution, and conflict behavior when hook.ready is used inside runWorkflow.
packages/core/src/create-hook.tsExtends the public Hook interface with documented ready: Promise<void>.
docs/content/docs/v5/how-it-works/event-sourcing.mdxDocuments that hook.ready rejects on hook conflicts in addition to payload awaiting.
docs/content/docs/v5/foundations/hooks.mdxAdds a section explaining when/how to use hook.ready to wait for registration.
docs/content/docs/v5/api-reference/workflow/create-webhook.mdxNotes ready availability on returned webhook objects and how to use it before sharing the URL.
docs/content/docs/v5/api-reference/workflow/create-hook.mdxDocuments hook.ready and adds a registration-focused example.
docs/content/docs/v4/how-it-works/event-sourcing.mdxMirrors v5 event-sourcing updates for hook.ready conflict semantics.
docs/content/docs/v4/foundations/hooks.mdxMirrors v5 “Waiting for Hook Registration” guidance.
docs/content/docs/v4/api-reference/workflow/create-webhook.mdxMirrors v5 webhook ready documentation note.
docs/content/docs/v4/api-reference/workflow/create-hook.mdxMirrors v5 hook.ready API docs and example.
.changeset/hook-ready.mdDeclares minor bumps for @workflow/core and workflow to ship the new API.

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

Comment threadpackages/core/src/workflow/hook.test.ts Outdated
Comment threaddocs/content/docs/v4/api-reference/workflow/create-webhook.mdx Outdated
Comment threaddocs/content/docs/v5/api-reference/workflow/create-webhook.mdx Outdated

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

Copilot reviewed 20 out of 20 changed files in this pull request and generated 1 comment.

Comment threadpackages/core/src/workflow/hook.ts Outdated
Comment threadpackages/core/src/runtime.ts Outdated
pranaygpand others added 3 commits May 18, 2026 21:44
…hook-ready
* origin-https/main: (119 commits)
fix: settle aborted parallel steps before completing abortParallelWorkflow (#2244)
Add native v4 workflow attribute events (#2226)
Version Packages (beta) (#2326)
[ci] Fix flaky windows unit tests (#2359)
Capture Vercel runtime logs when e2e Vercel Prod lanes fail (#2356)
Fix e2e failure reporting under vitest 4 and preserve fetch error causes (#2355)
[core] Fix process crash from rejected waitUntil promises (#2336)
[core] Remove duplicate `waitUntil` for suspension handler async operations (#2345)
Prevent local tests from hanging (#2338)
feat(core): add optional namespace for queue topic prefix (#2305)
[web-shared] Show precise durations in the new trace viewer (#2335)
Validate unique workflow step IDs at build time (#2018)
Move run attributes into their own detail card (#2327)
[core] Forward-port stream reconnect to getReadable level (#2318)
[docs] Add "Step executed multiple times" error page (#2310)
Fix flickering on the detail panel when navigating the trace viewer (#2325)
Version Packages (beta) (#2254)
Patch vulnerable package dependencies (#2301)
Deprecate DurableAgent and update it with WorkflowAgent in v5 docs (#2285)
[codex] Fix partial world-local exclusive writes (#2296)
...
# Conflicts:
#	packages/core/src/runtime.ts
#	packages/core/src/runtime/suspension-handler.ts
- hook.hasConflict resolves true when the token is owned by another
active hook, false once registration is committed — no throw, so
workflows can branch on conflicts early. Awaiting it suspends the
workflow to commit the hook registration (createHook alone does not).
- Chain the already-created fast-path through promiseQueue so
resolution order matches event-log order (review feedback).
- Skip inline step execution when a suspension has an awaited hook
creation so the hasConflict continuation can advance independently
of step execution (review feedback).
- Update unit tests, e2e tests, workbench workflows, and v4/v5 docs.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

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

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

Comment threaddocs/content/docs/v5/api-reference/workflow/create-webhook.mdx Outdated
Comment threaddocs/content/docs/v4/api-reference/workflow/create-webhook.mdx Outdated

@TooTallNateTooTallNate left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Approve — determinism-safe design, strong test coverage; one docs-sequencing flag

I built @workflow/core from this branch and ran the full unit suite locally (1146 tests, matching the PR's claimed validation) plus the three new test files individually (116 tests). All green.

The semantics hold up under determinism scrutiny

This touches the workflow VM's most replay-sensitive machinery, so I traced the settlement paths carefully:

  • Every hasConflict settlement goes through ctx.promiseQueue — both fast-paths (hasCreated / conflict-already-known) and the event-driven paths (hook_created → resolve false, hook_conflict → resolve true). Resolution order always matches event-log order, which is the invariant that keeps replay deterministic.
  • The hook_conflict consumer drains payload and hasConflict waiters synchronously (deferring actual settlement through promiseQueue), so the null-event handler can't see them and raise a spurious suspension — same pattern the existing payload path uses.
  • The suspension trigger condition (hasConflictPromises.length > 0 && !hasCreated && !hasConflict in the null-event handler, plus the eventLogEmpty fast-path in createHasConflictPromise) correctly mirrors the payload-awaiter pattern for both orderings of "awaited before/after log exhaustion." Both are unit-tested.
  • hasAwaitedHookCreation only affects scheduling, never event-log content — it decides which invocation executes steps (queue-all + timeoutSeconds: 0 re-invoke instead of inline), so there's no replay-divergence surface. The reasoning in the runtime.ts comment is exactly right: running a step inline would serialize work the workflow expressed as parallel (hook.hasConflict.then(() => stepB()) racing await stepA()).
  • The EntityConflictError path returning hasAwaitedHookCreation: true is correct and subtle: on a crash-replay where the hook event already exists, the awaiter still needs an immediate re-invocation to consume hook_created and resolve — silently swallowing it would hang the awaiter until something else woke the run.
  • The e2e parallelism proof (hookHasConflictThenStepParallelWorkflow: 100ms stepB triggered via the hasConflict continuation must complete while 10s stepA is still running, asserted via step timestamps) tests the actual claim end-to-end rather than just the mechanism.

My earlier docs comments were both addressed (the create-hook example now explicitly points to awaiting the hook/iterating for payload data).

One sequencing flag: the v4 docs (should-fix before merge, or fast-follow the backport)

hasConflict does not exist on stable — I checked origin/stable:packages/core/src/create-hook.ts and there are zero references. But this PR adds the API to the v4 docs (docs/content/docs/v4/...), and docs deploy from main. The moment this merges, the live v4 documentation advertises a property that no released 4.x SDK has — and it'll stay wrong until a backport merges to stableand ships in a 4.x release.

Two clean options:

  1. Drop the v4 docs pages from this PR and let the backport PR carry them (the backport flow cherry-picks docs/content/ normally, so they'd land together with the code on stable — though note they'd then need a separate main-side commit since v4 docs render from main), or
  2. Keep them but treat the stable backport + 4.x release as an immediate follow-up so the docs-reality gap is measured in days.

Given the changeset is minor (correct for a new API) and this is a self-contained feature — squarely in the auto-backport criteria — option 2 is probably fine, but it should be a deliberate choice, not an accident.

Non-blocking notes

  • get hasConflict() returns a fresh promise per property access (hook.hasConflict !== hook.hasConflict). Each settles deterministically through promiseQueue, so it's correctness-neutral, but identity-unstable promises from a property (vs a method) can surprise — e.g. registering two waiters from what reads like one. Caching the promise per hook after first access would be friendlier and would also make the hasConflictAwaiter flag-set happen exactly once. Fine as-is.
  • The local hasConflict boolean inside createCreateHook now shadows the public property name; a rename (sawConflictEvent?) would help the next reader.

CI

The two red E2E jobs are baseline flakes unrelated to this PR: astro fails AbortController > abortFromStepWorkflow (the abort area #2244 just touched on main) and hono fails resilient start: addTenWorkflow ... run_created returns 500 (known flake). No hook test failed anywhere. The required check needs a re-run to go green before merge.

Nice work — this is a well-reasoned addition in a part of the codebase where it's very easy to be subtly wrong.

TooTallNateand others added 3 commits June 11, 2026 14:29
State both resolution values explicitly (true = token already owned,
false = registered) instead of a parenthetical that only described the
false case.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment threadAGENTS.md
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
expect.anything()
);
expect(result.hasAwaitedHookCreation).toBe(true);
expect(result.timeoutSeconds).toBeUndefined();

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.

Test assertions check non-existent result.timeoutSeconds property, making toBeUndefined() assertions pass trivially without verifying actual behavior.

Fix on Vercel

@github-actions

Copy link
Copy Markdown
Contributor

No backport to stable for e163422 (AI decision).

This is a new minor feature adding the hook.hasConflict API to the public hook interface, intended for the next major release on main. It's a self-contained feature addition (not a bug fix) and the changeset is marked as a minor bump for the beta channel. Per the criteria, new feature additions targeted at the next major release should generally stay on main rather than be backported to stable.

To override, re-run the Backport to stable workflow manually via workflow_dispatch and paste this commit SHA into the ref input:

e163422551e80eda88a15fec3d3d574a00c53e93

@github-actionsgithub-actionsBot mentioned this pull request Jun 11, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Backport PR opened against stable: #2372. Merge conflicts were resolved by AI — please review carefully. (backport job run)

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.

3 participants

@pranaygp@TooTallNate
, '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 `hook.hasConflict` for early hook conflict detection by pranaygp · Pull Request #2015 · vercel/workflow · GitHub
Skip to content

Add hook.hasConflict for early hook conflict detection - #2015

Merged
TooTallNate merged 9 commits into
mainfrom
pranaygp/codex/hook-ready
Jun 11, 2026
Merged

Add hook.hasConflict for early hook conflict detection#2015
TooTallNate merged 9 commits into
mainfrom
pranaygp/codex/hook-ready

Conversation

@pranaygp

@pranaygppranaygp commented May 18, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds hook.hasConflict, a lazy Promise<boolean> getter that lets workflow code commit hook registration and check for token conflicts without waiting for hook payload data.

Calling createHook() alone does not register the hook — registration is only committed when the workflow suspends. Awaiting hook.hasConflict suspends the workflow to commit the registration, then resolves with false once hook_created is recorded, or true if another active hook already owns the token. Unlike awaiting the hook payload (which rejects with HookConflictError on conflict), hasConflict never throws, so workflows can branch on conflicts early:

using hook=createHook({token: `order:${orderId}`});if(awaithook.hasConflict){return;// another run already owns this token}

This gives workflows a way to claim a deterministic hook token for idempotency before running duplicate-sensitive work, and is a prerequisite for #2011, where the idempotency docs/examples should use await hook.hasConflict instead of requiring consumers to wait for or resume hook data just to claim the token.

Changes

  • Add Hook.hasConflict: Promise<boolean> to the public hook interface.
  • Resolve pending hasConflict waiters with false on hook_created and true on hook_conflict; both fast-paths settle through promiseQueue so resolution order matches event-log order.
  • Suspend workflow execution when hook.hasConflict is awaited before hook creation is recorded.
  • Skip inline step execution when a suspension contains an awaited hook creation, so a hasConflict continuation (e.g. hook.hasConflict.then(() => stepB())) can advance in parallel with other steps instead of only after they finish.
  • Document hook.hasConflict in the v4 and v5 hook/webhook docs and event-sourcing pages, including the suspension-commits-registration semantics.
  • Add core unit tests (resolution, suspension, conflict, payload preservation, suspension-handler scheduling) and e2e tests (registration without payload, non-blocking steps, parallel continuation scheduling, conflict resolves true).

Validation

  • cd packages/core && pnpm typecheck && pnpm vitest run src/ (1146 tests)
  • Hook e2e suite against local nextjs-turbopack dev server: pnpm vitest run packages/core/e2e/e2e.test.ts -t 'hook' (20 tests, including the new parallel-continuation test)

Docs Preview

Changed pages on the docs preview deployment:

Pagev4v5
createHook() API referencev4v5
createWebhook() API referencev4v5
Foundations: Hooksv4v5
How It Works: Event Sourcingv4v5

CopilotAI review requested due to automatic review settings May 18, 2026 23:22
@pranaygp
pranaygp requested a review from a team as a code ownerMay 18, 2026 23:22
@changeset-bot

changeset-botBot commented May 18, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 2342dae

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

This PR includes changesets to release 17 packages
NameType
@workflow/coreMinor
workflowMinor
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/webPatch
@workflow/world-testingPatch
@workflow/aiMajor
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch
@workflow/nuxtPatch

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 May 18, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented May 18, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production137602191595
✅ 💻 Local Development181102192030
✅ 📦 Local Production181102192030
✅ 🐘 Local Postgres179702332030
✅ 🪟 Windows14500145
✅ 📋 Other83701781015
Total7777010688845

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro119026
✅ example119026
✅ express119026
✅ fastify119026
✅ hono119026
✅ nextjs-turbopack14302
✅ nextjs-webpack14302
✅ nitro119026
✅ nuxt119026
✅ sveltekit13807
✅ vite119026
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable120025
✅ express-stable120025
✅ fastify-stable120025
✅ hono-stable120025
✅ nextjs-turbopack-canary126019
✅ nextjs-turbopack-stable-lazy-discovery-disabled14500
✅ nextjs-turbopack-stable-lazy-discovery-enabled14500
✅ nextjs-webpack-canary126019
✅ nextjs-webpack-stable-lazy-discovery-disabled14500
✅ nextjs-webpack-stable-lazy-discovery-enabled14500
✅ nitro-stable120025
✅ nuxt-stable120025
✅ sveltekit-stable13906
✅ vite-stable120025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable120025
✅ express-stable120025
✅ fastify-stable120025
✅ hono-stable120025
✅ nextjs-turbopack-canary126019
✅ nextjs-turbopack-stable-lazy-discovery-disabled14500
✅ nextjs-turbopack-stable-lazy-discovery-enabled14500
✅ nextjs-webpack-canary126019
✅ nextjs-webpack-stable-lazy-discovery-disabled14500
✅ nextjs-webpack-stable-lazy-discovery-enabled14500
✅ nitro-stable120025
✅ nuxt-stable120025
✅ sveltekit-stable13906
✅ vite-stable120025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable119026
✅ express-stable119026
✅ fastify-stable119026
✅ hono-stable119026
✅ nextjs-turbopack-canary125020
✅ nextjs-turbopack-stable-lazy-discovery-disabled14401
✅ nextjs-turbopack-stable-lazy-discovery-enabled14401
✅ nextjs-webpack-canary125020
✅ nextjs-webpack-stable-lazy-discovery-disabled14401
✅ nextjs-webpack-stable-lazy-discovery-enabled14401
✅ nitro-stable119026
✅ nuxt-stable119026
✅ sveltekit-stable13807
✅ vite-stable119026
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack14500
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable120025
✅ e2e-local-dev-tanstack-start-120025
✅ e2e-local-postgres-nest-stable119026
✅ e2e-local-postgres-tanstack-start-119026
✅ e2e-local-prod-nest-stable120025
✅ e2e-local-prod-tanstack-start-120025
✅ e2e-vercel-prod-tanstack-start119026

📋 View full workflow run

@github-actions

github-actionsBot commented May 18, 2026

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

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

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro0.040s (-5.4% 🟢)1.005s (~)0.965s101.00x
💻 LocalExpress0.041s (-2.8%)1.005s (~)0.964s101.03x
💻 LocalNext.js (Turbopack)0.060s (-4.0%)1.005s (~)0.945s101.51x
🐘 PostgresNitro0.062s (-5.2% 🟢)1.012s (~)0.950s101.56x
🐘 PostgresExpress0.067s (+9.0% 🔺)1.014s (~)0.947s101.67x
🐘 PostgresNext.js (Turbopack)0.068s (-8.6% 🟢)1.013s (~)0.945s101.71x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express0.414s (+52.9% 🔺)2.276s (-11.8% 🟢)1.863s101.00x
▲ VercelNitro0.420s (+11.1% 🔺)2.271s (+8.1% 🔺)1.851s101.01x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.093s (~)2.006s (~)0.913s101.00x
💻 LocalExpress1.094s (~)2.006s (~)0.912s101.00x
🐘 PostgresNitro1.105s (-0.7%)2.010s (~)0.906s101.01x
🐘 PostgresExpress1.114s (~)2.009s (~)0.895s101.02x
💻 LocalNext.js (Turbopack)1.136s (+0.7%)2.007s (~)0.871s101.04x
🐘 PostgresNext.js (Turbopack)1.150s (+0.6%)2.009s (~)0.859s101.05x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.681s (-4.1%)3.608s (-2.7%)1.927s101.00x
▲ VercelNitro32.051s (+1812.5% 🔺)34.165s (+890.4% 🔺)2.114s1019.06x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express10.500s (~)11.022s (~)0.523s31.00x
🐘 PostgresExpress10.570s (~)11.020s (~)0.451s31.01x
🐘 PostgresNitro10.578s (~)11.023s (~)0.444s31.01x
💻 LocalNitro10.651s (+1.0%)11.022s (~)0.372s31.01x
💻 LocalNext.js (Turbopack)10.792s (~)11.021s (~)0.229s31.03x
🐘 PostgresNext.js (Turbopack)10.937s (-0.7%)11.351s (-2.9%)0.415s31.04x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro13.882s (+4.1%)16.196s (+7.0% 🔺)2.314s21.00x
▲ VercelExpress14.281s (+6.5% 🔺)16.108s (+2.4%)1.827s21.03x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express13.701s (-1.0%)14.026s (~)0.325s51.00x
💻 LocalNitro13.738s (-0.7%)14.027s (-1.4%)0.289s51.00x
🐘 PostgresExpress13.763s (-0.5%)14.022s (~)0.259s51.00x
🐘 PostgresNitro13.921s (+0.7%)14.023s (~)0.102s51.02x
💻 LocalNext.js (Turbopack)14.364s (-0.9%)15.030s (~)0.665s41.05x
🐘 PostgresNext.js (Turbopack)14.456s (~)15.014s (~)0.558s41.06x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro21.579s (-3.8%)23.291s (-3.3%)1.712s31.00x
▲ VercelExpress24.509s (+14.1% 🔺)26.394s (+14.4% 🔺)1.885s31.14x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro12.331s (-1.1%)13.026s (~)0.695s71.00x
💻 LocalExpress12.446s (~)13.025s (~)0.579s71.01x
🐘 PostgresExpress12.526s (-1.7%)13.020s (~)0.494s71.02x
🐘 PostgresNitro12.719s (+1.1%)13.025s (~)0.307s71.03x
💻 LocalNext.js (Turbopack)13.813s (+1.0%)14.169s (+1.0%)0.356s71.12x
🐘 PostgresNext.js (Turbopack)13.852s (-0.9%)14.161s (-1.0%)0.309s71.12x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro29.649s (-6.5% 🟢)31.500s (-5.4% 🟢)1.851s31.00x
▲ VercelExpress31.179s (+5.1% 🔺)33.707s (+8.2% 🔺)2.528s31.05x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express1.208s (+1.5%)2.006s (~)0.798s151.00x
🐘 PostgresNitro1.208s (~)2.009s (~)0.800s151.00x
🐘 PostgresExpress1.231s (+2.4%)2.008s (~)0.777s151.02x
💻 LocalNitro1.279s (+4.6%)2.006s (~)0.726s151.06x
🐘 PostgresNext.js (Turbopack)1.339s (+6.4% 🔺)2.074s (+3.3%)0.734s151.11x
💻 LocalNext.js (Turbopack)1.368s (+1.4%)2.006s (~)0.638s151.13x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.970s (+16.4% 🔺)4.450s (-1.1%)1.480s71.00x
▲ VercelNitro3.506s (-5.2% 🟢)5.306s (+2.4%)1.799s61.18x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.366s (+8.5% 🔺)2.394s (+19.3% 🔺)1.028s131.00x
🐘 PostgresExpress1.389s (+10.0% 🔺)2.508s (+25.0% 🔺)1.119s121.02x
🐘 PostgresNext.js (Turbopack)1.618s (+15.5% 🔺)2.393s (+19.3% 🔺)0.776s131.18x
💻 LocalNitro1.704s (~)2.006s (~)0.302s151.25x
💻 LocalExpress1.729s (-6.1% 🟢)2.006s (-6.7% 🟢)0.277s151.27x
💻 LocalNext.js (Turbopack)1.842s (-2.0%)2.074s (-3.5%)0.232s151.35x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.196s (-27.7% 🟢)4.831s (-21.0% 🟢)1.635s71.00x
▲ VercelNitro3.207s (-48.6% 🟢)4.657s (-39.9% 🟢)1.450s71.00x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.680s (+19.4% 🔺)4.141s (+106.2% 🔺)2.461s81.00x
🐘 PostgresExpress1.718s (+22.5% 🔺)3.889s (+93.6% 🔺)2.172s81.02x
🐘 PostgresNext.js (Turbopack)3.508s (+94.5% 🔺)4.297s (+87.1% 🔺)0.789s72.09x
💻 LocalNitro4.696s (-10.5% 🟢)5.347s (-11.1% 🟢)0.650s62.80x
💻 LocalExpress5.272s (-2.1%)6.013s (-2.7%)0.742s53.14x
💻 LocalNext.js (Turbopack)5.395s (-5.4% 🟢)6.014s (-3.2%)0.619s53.21x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.396s (-51.8% 🟢)6.218s (-45.2% 🟢)1.823s61.00x
▲ VercelNitro4.696s (-72.4% 🟢)6.708s (-63.3% 🟢)2.012s51.07x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.221s (+0.8%)2.074s (+3.3%)0.853s151.00x
🐘 PostgresNitro1.224s (+2.8%)2.008s (~)0.784s151.00x
🐘 PostgresNext.js (Turbopack)1.278s (+1.2%)2.008s (~)0.729s151.05x
💻 LocalNext.js (Turbopack)1.391s (+2.9%)2.006s (~)0.615s151.14x
💻 LocalNitro1.502s (~)2.006s (~)0.505s151.23x
💻 LocalExpress1.539s (-7.2% 🟢)2.006s (-3.3%)0.467s151.26x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.632s (+2.1%)4.218s (~)1.586s81.00x
▲ VercelExpress2.986s (+10.6% 🔺)4.712s (~)1.726s71.13x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.394s (+9.6% 🔺)2.224s (+10.8% 🔺)0.830s141.00x
🐘 PostgresExpress1.424s (+9.8% 🔺)2.317s (+15.3% 🔺)0.892s131.02x
🐘 PostgresNext.js (Turbopack)1.506s (+7.3% 🔺)2.150s (+7.1% 🔺)0.644s141.08x
💻 LocalExpress1.865s (-13.6% 🟢)2.295s (-14.2% 🟢)0.429s141.34x
💻 LocalNext.js (Turbopack)2.055s (~)2.591s (-13.9% 🟢)0.536s121.47x
💻 LocalNitro2.153s (-0.8%)2.676s (+3.2%)0.523s121.55x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.328s (-2.9%)5.132s (+0.6%)1.803s61.00x
▲ VercelNitro3.433s (-28.9% 🟢)5.018s (-21.6% 🟢)1.585s61.03x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.617s (+16.5% 🔺)4.299s (+114.1% 🔺)2.682s71.00x
🐘 PostgresExpress1.837s (+31.1% 🔺)3.891s (+93.5% 🔺)2.053s81.14x
🐘 PostgresNext.js (Turbopack)3.367s (+86.2% 🔺)4.582s (+106.0% 🔺)1.215s72.08x
💻 LocalExpress4.294s (-32.4% 🟢)5.016s (-28.5% 🟢)0.721s62.66x
💻 LocalNitro5.167s (-5.0%)5.851s (-2.7%)0.685s63.19x
💻 LocalNext.js (Turbopack)5.712s (-5.7% 🟢)6.017s (-11.7% 🟢)0.305s53.53x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.990s (-21.8% 🟢)5.843s (-19.3% 🟢)1.853s61.00x
▲ VercelNitro4.324s (-38.2% 🟢)6.347s (-26.2% 🟢)2.023s51.08x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro0.589s (-1.8%)1.005s (~)0.416s601.00x
🐘 PostgresExpress0.603s (~)1.023s (+1.6%)0.421s591.02x
🐘 PostgresNitro0.603s (+4.1%)1.007s (~)0.404s601.02x
💻 LocalExpress0.611s (~)1.039s (+3.4%)0.429s581.04x
🐘 PostgresNext.js (Turbopack)0.811s (-5.0% 🟢)1.006s (-1.8%)0.194s601.38x
💻 LocalNext.js (Turbopack)0.892s (+3.3%)1.057s (+3.5%)0.166s571.51x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express5.921s (-41.6% 🟢)7.609s (-35.4% 🟢)1.688s81.00x
▲ VercelNitro6.497s (+31.0% 🔺)8.122s (+24.7% 🔺)1.625s81.10x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.360s (-5.1% 🟢)2.008s (~)0.647s451.00x
🐘 PostgresNitro1.410s (-1.8%)2.008s (-1.1%)0.598s451.04x
💻 LocalNitro1.508s (~)2.006s (~)0.497s451.11x
💻 LocalExpress1.526s (-1.8%)2.028s (~)0.502s451.12x
🐘 PostgresNext.js (Turbopack)1.894s (-6.7% 🟢)2.053s (-22.7% 🟢)0.159s441.39x
💻 LocalNext.js (Turbopack)2.084s (-0.7%)3.008s (~)0.924s301.53x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro14.267s (+8.9% 🔺)16.133s (+11.6% 🔺)1.866s61.00x
▲ VercelExpress15.632s (+8.8% 🔺)17.362s (+7.5% 🔺)1.729s61.10x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro2.756s (-1.4%)3.059s (-3.4%)0.303s401.00x
🐘 PostgresExpress2.805s (~)3.112s (-0.8%)0.307s391.02x
💻 LocalExpress3.225s (-1.8%)3.977s (-0.8%)0.752s311.17x
💻 LocalNitro3.248s (-1.5%)4.009s (~)0.761s301.18x
🐘 PostgresNext.js (Turbopack)3.766s (-5.5% 🟢)4.043s (-6.7% 🟢)0.276s301.37x
💻 LocalNext.js (Turbopack)4.291s (-1.3%)5.010s (~)0.720s241.56x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express32.130s (+22.5% 🔺)34.248s (+21.4% 🔺)2.117s41.00x
▲ VercelNitro34.856s (+19.7% 🔺)36.875s (+19.7% 🔺)2.019s41.08x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.244s (+11.1% 🔺)1.007s (~)0.763s601.00x
🐘 PostgresExpress0.250s (+7.4% 🔺)1.006s (~)0.756s601.03x
🐘 PostgresNext.js (Turbopack)0.289s (+4.2%)1.006s (~)0.717s601.19x
💻 LocalExpress0.423s (+2.4%)1.004s (~)0.582s601.74x
💻 LocalNitro0.431s (~)1.004s (-1.7%)0.573s601.77x
💻 LocalNext.js (Turbopack)0.569s (+4.1%)1.005s (~)0.436s602.33x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.874s (-27.0% 🟢)3.565s (-13.2% 🟢)1.692s171.00x
▲ VercelNitro2.088s (-26.0% 🟢)3.684s (-14.7% 🟢)1.596s171.11x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.393s (+9.5% 🔺)1.053s (+4.7%)0.661s861.00x
🐘 PostgresExpress0.411s (+9.7% 🔺)1.041s (+3.5%)0.630s871.05x
🐘 PostgresNext.js (Turbopack)0.583s (+16.8% 🔺)1.091s (+8.4% 🔺)0.508s831.48x
💻 LocalNitro2.128s (-2.5%)2.737s (+0.8%)0.609s335.42x
💻 LocalExpress2.226s (+9.2% 🔺)2.766s (+9.1% 🔺)0.540s335.67x
💻 LocalNext.js (Turbopack)2.521s (+2.9%)3.218s (+2.2%)0.697s296.42x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.431s (-62.9% 🟢)4.945s (-54.5% 🟢)1.514s191.00x
▲ VercelExpress21.388s (+295.0% 🔺)23.082s (+226.3% 🔺)1.694s176.23x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.795s (+16.7% 🔺)1.567s (+55.7% 🔺)0.772s771.00x
🐘 PostgresExpress0.805s (+15.6% 🔺)1.419s (+41.0% 🔺)0.614s851.01x
🐘 PostgresNext.js (Turbopack)2.760s (+174.5% 🔺)3.681s (+98.5% 🔺)0.921s333.47x
💻 LocalExpress9.388s (+0.8%)10.195s (+3.3%)0.807s1211.80x
💻 LocalNitro9.416s (+1.5%)9.874s (+0.8%)0.458s1311.84x
💻 LocalNext.js (Turbopack)10.703s (-2.2%)11.576s (+0.8%)0.873s1113.46x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express6.877s (-63.6% 🟢)8.574s (-58.2% 🟢)1.697s141.00x
▲ VercelNitro8.266s (-57.1% 🟢)10.222s (-53.8% 🟢)1.957s121.20x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express1.160s (-0.7%)2.005s (~)0.010s (-14.5% 🟢)2.017s (~)0.857s101.00x
🐘 PostgresNitro1.165s (-0.7%)2.001s (~)0.001s (-15.4% 🟢)2.010s (~)0.846s101.00x
💻 LocalNitro1.167s (~)2.005s (~)0.010s (~)2.018s (~)0.851s101.01x
🐘 PostgresExpress1.172s (-0.6%)2.000s (~)0.001s (-8.3% 🟢)2.010s (~)0.838s101.01x
💻 LocalNext.js (Turbopack)1.211s (~)2.004s (~)0.013s (+0.8%)2.020s (~)0.809s101.04x
🐘 PostgresNext.js (Turbopack)1.234s (-0.9%)2.002s (~)0.001s (+7.7% 🔺)2.011s (~)0.777s101.06x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.797s (+14.3% 🔺)4.260s (+26.7% 🔺)0.638s (-32.8% 🟢)5.340s (+12.0% 🔺)2.543s101.00x
▲ VercelExpress32.861s (+1415.8% 🔺)3.322s (+3.7%)1.152s (+26.8% 🔺)35.409s (+673.0% 🔺)2.548s1011.75x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro | Express

stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.566s (-1.8%)2.009s (~)0.012s (-1.6%)2.023s (~)0.457s301.00x
💻 LocalExpress1.568s (~)2.010s (~)0.013s (+14.2% 🔺)2.026s (~)0.457s301.00x
🐘 PostgresExpress1.579s (-1.4%)2.006s (~)0.005s (+7.1% 🔺)2.027s (~)0.448s301.01x
🐘 PostgresNitro1.616s (~)2.006s (~)0.005s (-8.7% 🟢)2.025s (~)0.408s301.03x
💻 LocalNext.js (Turbopack)1.730s (-0.6%)2.010s (~)0.012s (-1.9%)2.025s (~)0.295s301.10x
🐘 PostgresNext.js (Turbopack)1.755s (-1.7%)2.010s (~)0.005s (+2.7%)2.026s (~)0.270s301.12x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express5.998s (-1.2%)7.151s (-2.0%)0.235s (-10.5% 🟢)7.964s (~)1.966s81.00x
▲ VercelNitro6.812s (+15.1% 🔺)7.759s (+10.4% 🔺)0.245s (-47.7% 🟢)8.558s (+7.1% 🔺)1.745s81.14x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Express | Nitro

10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.798s (+12.6% 🔺)1.068s (+2.4%)0.000s (-66.1% 🟢)1.088s (+2.3%)0.291s561.00x
🐘 PostgresExpress0.805s (+13.8% 🔺)1.084s (+6.9% 🔺)0.000s (+221.8% 🔺)1.099s (+6.8% 🔺)0.293s551.01x
🐘 PostgresNext.js (Turbopack)0.998s (+15.9% 🔺)1.463s (+34.1% 🔺)0.000s (-100.0% 🟢)1.471s (+33.9% 🔺)0.473s411.25x
💻 LocalNitro1.386s (-3.8%)2.013s (~)0.001s (+260.0% 🔺)2.015s (~)0.630s301.74x
💻 LocalExpress1.391s (+3.1%)2.013s (~)0.000s (+175.0% 🔺)2.015s (~)0.624s301.74x
💻 LocalNext.js (Turbopack)1.462s (~)2.013s (~)0.000s (-40.0% 🟢)2.016s (~)0.554s301.83x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.320s (-1.2%)4.629s (+1.2%)0.000s (-100.0% 🟢)5.052s (~)1.731s121.00x
▲ VercelNitro3.595s (-5.5% 🟢)4.764s (-1.3%)0.001s (+Infinity% 🔺)5.183s (-0.7%)1.588s121.08x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Express | Nitro

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

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.508s (+13.1% 🔺)2.032s (~)0.000s (NaN%)2.061s (~)0.553s301.00x
🐘 PostgresExpress1.669s (+9.4% 🔺)2.217s (~)0.000s (-50.0% 🟢)2.252s (~)0.583s271.11x
🐘 PostgresNext.js (Turbopack)2.069s (+19.6% 🔺)2.404s (+6.3% 🔺)0.000s (+Infinity% 🔺)2.432s (+7.1% 🔺)0.363s251.37x
💻 LocalNext.js (Turbopack)2.812s (-1.4%)3.357s (-1.7%)0.001s (+100.0% 🔺)3.361s (-1.7%)0.549s181.86x
💻 LocalNitro2.994s (-5.5% 🟢)3.613s (-5.8% 🟢)0.000s (+119.6% 🔺)3.620s (-5.8% 🟢)0.626s171.98x
💻 LocalExpress3.129s (+1.1%)3.902s (+6.3% 🔺)0.001s (+44.9% 🔺)3.905s (+6.2% 🔺)0.776s162.07x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro5.070s (-31.3% 🟢)6.077s (-23.4% 🟢)0.000s (+Infinity% 🔺)6.502s (-28.0% 🟢)1.433s101.00x
▲ VercelExpress5.299s (~)6.264s (-4.8%)0.000s (NaN%)6.717s (-6.6% 🟢)1.418s91.05x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro | Express

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNitro10/21
🐘 PostgresNitro14/21
▲ VercelExpress13/21
Fastest World by Framework

Winner determined by most benchmark wins

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

Worlds:

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

📋 View full workflow run


Some benchmark jobs failed:

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

Check the workflow run for details.

Comment threaddocs/content/docs/v4/api-reference/workflow/create-hook.mdx Outdated

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

Adds a new hook.ready promise to let workflow code wait for hook registration (hook_created) independently of receiving payloads, enabling deterministic token claiming for idempotency patterns (and consistent conflict handling via HookConflictError).

Changes:

  • Extend the public Hook interface with ready: Promise<void> and implement it in the workflow VM hook runtime.
  • Resolve/reject ready waiters based on hook_created / hook_conflict, including suspension behavior when awaiting readiness before registration is recorded.
  • Add tests and update v4/v5 documentation to describe hook.ready (and webhook readiness via the shared Hook interface).

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 1 comment.

Show a summary per file
FileDescription
packages/core/src/workflow/hook.tsImplements hook.ready tracking/queuing and resolves/rejects it on hook_created/hook_conflict, including suspension when needed.
packages/core/src/workflow/hook.test.tsAdds unit tests covering ready resolution, suspension behavior, conflict rejection, and ensuring payload consumption is unchanged.
packages/core/src/workflow.test.tsAdds integration tests verifying suspension, resolution, and conflict behavior when hook.ready is used inside runWorkflow.
packages/core/src/create-hook.tsExtends the public Hook interface with documented ready: Promise<void>.
docs/content/docs/v5/how-it-works/event-sourcing.mdxDocuments that hook.ready rejects on hook conflicts in addition to payload awaiting.
docs/content/docs/v5/foundations/hooks.mdxAdds a section explaining when/how to use hook.ready to wait for registration.
docs/content/docs/v5/api-reference/workflow/create-webhook.mdxNotes ready availability on returned webhook objects and how to use it before sharing the URL.
docs/content/docs/v5/api-reference/workflow/create-hook.mdxDocuments hook.ready and adds a registration-focused example.
docs/content/docs/v4/how-it-works/event-sourcing.mdxMirrors v5 event-sourcing updates for hook.ready conflict semantics.
docs/content/docs/v4/foundations/hooks.mdxMirrors v5 “Waiting for Hook Registration” guidance.
docs/content/docs/v4/api-reference/workflow/create-webhook.mdxMirrors v5 webhook ready documentation note.
docs/content/docs/v4/api-reference/workflow/create-hook.mdxMirrors v5 hook.ready API docs and example.
.changeset/hook-ready.mdDeclares minor bumps for @workflow/core and workflow to ship the new API.

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

Comment threadpackages/core/src/workflow/hook.test.ts Outdated
Comment threaddocs/content/docs/v4/api-reference/workflow/create-webhook.mdx Outdated
Comment threaddocs/content/docs/v5/api-reference/workflow/create-webhook.mdx Outdated

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

Copilot reviewed 20 out of 20 changed files in this pull request and generated 1 comment.

Comment threadpackages/core/src/workflow/hook.ts Outdated
Comment threadpackages/core/src/runtime.ts Outdated
pranaygpand others added 3 commits May 18, 2026 21:44
…hook-ready
* origin-https/main: (119 commits)
fix: settle aborted parallel steps before completing abortParallelWorkflow (#2244)
Add native v4 workflow attribute events (#2226)
Version Packages (beta) (#2326)
[ci] Fix flaky windows unit tests (#2359)
Capture Vercel runtime logs when e2e Vercel Prod lanes fail (#2356)
Fix e2e failure reporting under vitest 4 and preserve fetch error causes (#2355)
[core] Fix process crash from rejected waitUntil promises (#2336)
[core] Remove duplicate `waitUntil` for suspension handler async operations (#2345)
Prevent local tests from hanging (#2338)
feat(core): add optional namespace for queue topic prefix (#2305)
[web-shared] Show precise durations in the new trace viewer (#2335)
Validate unique workflow step IDs at build time (#2018)
Move run attributes into their own detail card (#2327)
[core] Forward-port stream reconnect to getReadable level (#2318)
[docs] Add "Step executed multiple times" error page (#2310)
Fix flickering on the detail panel when navigating the trace viewer (#2325)
Version Packages (beta) (#2254)
Patch vulnerable package dependencies (#2301)
Deprecate DurableAgent and update it with WorkflowAgent in v5 docs (#2285)
[codex] Fix partial world-local exclusive writes (#2296)
...
# Conflicts:
#	packages/core/src/runtime.ts
#	packages/core/src/runtime/suspension-handler.ts
- hook.hasConflict resolves true when the token is owned by another
active hook, false once registration is committed — no throw, so
workflows can branch on conflicts early. Awaiting it suspends the
workflow to commit the hook registration (createHook alone does not).
- Chain the already-created fast-path through promiseQueue so
resolution order matches event-log order (review feedback).
- Skip inline step execution when a suspension has an awaited hook
creation so the hasConflict continuation can advance independently
of step execution (review feedback).
- Update unit tests, e2e tests, workbench workflows, and v4/v5 docs.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

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

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

Comment threaddocs/content/docs/v5/api-reference/workflow/create-webhook.mdx Outdated
Comment threaddocs/content/docs/v4/api-reference/workflow/create-webhook.mdx Outdated

@TooTallNateTooTallNate left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Approve — determinism-safe design, strong test coverage; one docs-sequencing flag

I built @workflow/core from this branch and ran the full unit suite locally (1146 tests, matching the PR's claimed validation) plus the three new test files individually (116 tests). All green.

The semantics hold up under determinism scrutiny

This touches the workflow VM's most replay-sensitive machinery, so I traced the settlement paths carefully:

  • Every hasConflict settlement goes through ctx.promiseQueue — both fast-paths (hasCreated / conflict-already-known) and the event-driven paths (hook_created → resolve false, hook_conflict → resolve true). Resolution order always matches event-log order, which is the invariant that keeps replay deterministic.
  • The hook_conflict consumer drains payload and hasConflict waiters synchronously (deferring actual settlement through promiseQueue), so the null-event handler can't see them and raise a spurious suspension — same pattern the existing payload path uses.
  • The suspension trigger condition (hasConflictPromises.length > 0 && !hasCreated && !hasConflict in the null-event handler, plus the eventLogEmpty fast-path in createHasConflictPromise) correctly mirrors the payload-awaiter pattern for both orderings of "awaited before/after log exhaustion." Both are unit-tested.
  • hasAwaitedHookCreation only affects scheduling, never event-log content — it decides which invocation executes steps (queue-all + timeoutSeconds: 0 re-invoke instead of inline), so there's no replay-divergence surface. The reasoning in the runtime.ts comment is exactly right: running a step inline would serialize work the workflow expressed as parallel (hook.hasConflict.then(() => stepB()) racing await stepA()).
  • The EntityConflictError path returning hasAwaitedHookCreation: true is correct and subtle: on a crash-replay where the hook event already exists, the awaiter still needs an immediate re-invocation to consume hook_created and resolve — silently swallowing it would hang the awaiter until something else woke the run.
  • The e2e parallelism proof (hookHasConflictThenStepParallelWorkflow: 100ms stepB triggered via the hasConflict continuation must complete while 10s stepA is still running, asserted via step timestamps) tests the actual claim end-to-end rather than just the mechanism.

My earlier docs comments were both addressed (the create-hook example now explicitly points to awaiting the hook/iterating for payload data).

One sequencing flag: the v4 docs (should-fix before merge, or fast-follow the backport)

hasConflict does not exist on stable — I checked origin/stable:packages/core/src/create-hook.ts and there are zero references. But this PR adds the API to the v4 docs (docs/content/docs/v4/...), and docs deploy from main. The moment this merges, the live v4 documentation advertises a property that no released 4.x SDK has — and it'll stay wrong until a backport merges to stableand ships in a 4.x release.

Two clean options:

  1. Drop the v4 docs pages from this PR and let the backport PR carry them (the backport flow cherry-picks docs/content/ normally, so they'd land together with the code on stable — though note they'd then need a separate main-side commit since v4 docs render from main), or
  2. Keep them but treat the stable backport + 4.x release as an immediate follow-up so the docs-reality gap is measured in days.

Given the changeset is minor (correct for a new API) and this is a self-contained feature — squarely in the auto-backport criteria — option 2 is probably fine, but it should be a deliberate choice, not an accident.

Non-blocking notes

  • get hasConflict() returns a fresh promise per property access (hook.hasConflict !== hook.hasConflict). Each settles deterministically through promiseQueue, so it's correctness-neutral, but identity-unstable promises from a property (vs a method) can surprise — e.g. registering two waiters from what reads like one. Caching the promise per hook after first access would be friendlier and would also make the hasConflictAwaiter flag-set happen exactly once. Fine as-is.
  • The local hasConflict boolean inside createCreateHook now shadows the public property name; a rename (sawConflictEvent?) would help the next reader.

CI

The two red E2E jobs are baseline flakes unrelated to this PR: astro fails AbortController > abortFromStepWorkflow (the abort area #2244 just touched on main) and hono fails resilient start: addTenWorkflow ... run_created returns 500 (known flake). No hook test failed anywhere. The required check needs a re-run to go green before merge.

Nice work — this is a well-reasoned addition in a part of the codebase where it's very easy to be subtly wrong.

TooTallNateand others added 3 commits June 11, 2026 14:29
State both resolution values explicitly (true = token already owned,
false = registered) instead of a parenthetical that only described the
false case.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment threadAGENTS.md
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
expect.anything()
);
expect(result.hasAwaitedHookCreation).toBe(true);
expect(result.timeoutSeconds).toBeUndefined();

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.

Test assertions check non-existent result.timeoutSeconds property, making toBeUndefined() assertions pass trivially without verifying actual behavior.

Fix on Vercel

@github-actions

Copy link
Copy Markdown
Contributor

No backport to stable for e163422 (AI decision).

This is a new minor feature adding the hook.hasConflict API to the public hook interface, intended for the next major release on main. It's a self-contained feature addition (not a bug fix) and the changeset is marked as a minor bump for the beta channel. Per the criteria, new feature additions targeted at the next major release should generally stay on main rather than be backported to stable.

To override, re-run the Backport to stable workflow manually via workflow_dispatch and paste this commit SHA into the ref input:

e163422551e80eda88a15fec3d3d574a00c53e93

@github-actionsgithub-actionsBot mentioned this pull request Jun 11, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Backport PR opened against stable: #2372. Merge conflicts were resolved by AI — please review carefully. (backport job run)

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.

3 participants

@pranaygp@TooTallNate
, '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 `hook.hasConflict` for early hook conflict detection by pranaygp · Pull Request #2015 · vercel/workflow · GitHub
Skip to content

Add hook.hasConflict for early hook conflict detection - #2015

Merged
TooTallNate merged 9 commits into
mainfrom
pranaygp/codex/hook-ready
Jun 11, 2026
Merged

Add hook.hasConflict for early hook conflict detection#2015
TooTallNate merged 9 commits into
mainfrom
pranaygp/codex/hook-ready

Conversation

@pranaygp

@pranaygppranaygp commented May 18, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds hook.hasConflict, a lazy Promise<boolean> getter that lets workflow code commit hook registration and check for token conflicts without waiting for hook payload data.

Calling createHook() alone does not register the hook — registration is only committed when the workflow suspends. Awaiting hook.hasConflict suspends the workflow to commit the registration, then resolves with false once hook_created is recorded, or true if another active hook already owns the token. Unlike awaiting the hook payload (which rejects with HookConflictError on conflict), hasConflict never throws, so workflows can branch on conflicts early:

using hook=createHook({token: `order:${orderId}`});if(awaithook.hasConflict){return;// another run already owns this token}

This gives workflows a way to claim a deterministic hook token for idempotency before running duplicate-sensitive work, and is a prerequisite for #2011, where the idempotency docs/examples should use await hook.hasConflict instead of requiring consumers to wait for or resume hook data just to claim the token.

Changes

  • Add Hook.hasConflict: Promise<boolean> to the public hook interface.
  • Resolve pending hasConflict waiters with false on hook_created and true on hook_conflict; both fast-paths settle through promiseQueue so resolution order matches event-log order.
  • Suspend workflow execution when hook.hasConflict is awaited before hook creation is recorded.
  • Skip inline step execution when a suspension contains an awaited hook creation, so a hasConflict continuation (e.g. hook.hasConflict.then(() => stepB())) can advance in parallel with other steps instead of only after they finish.
  • Document hook.hasConflict in the v4 and v5 hook/webhook docs and event-sourcing pages, including the suspension-commits-registration semantics.
  • Add core unit tests (resolution, suspension, conflict, payload preservation, suspension-handler scheduling) and e2e tests (registration without payload, non-blocking steps, parallel continuation scheduling, conflict resolves true).

Validation

  • cd packages/core && pnpm typecheck && pnpm vitest run src/ (1146 tests)
  • Hook e2e suite against local nextjs-turbopack dev server: pnpm vitest run packages/core/e2e/e2e.test.ts -t 'hook' (20 tests, including the new parallel-continuation test)

Docs Preview

Changed pages on the docs preview deployment:

Pagev4v5
createHook() API referencev4v5
createWebhook() API referencev4v5
Foundations: Hooksv4v5
How It Works: Event Sourcingv4v5

CopilotAI review requested due to automatic review settings May 18, 2026 23:22
@pranaygp
pranaygp requested a review from a team as a code ownerMay 18, 2026 23:22
@changeset-bot

changeset-botBot commented May 18, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 2342dae

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

This PR includes changesets to release 17 packages
NameType
@workflow/coreMinor
workflowMinor
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/webPatch
@workflow/world-testingPatch
@workflow/aiMajor
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch
@workflow/nuxtPatch

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 May 18, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented May 18, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production137602191595
✅ 💻 Local Development181102192030
✅ 📦 Local Production181102192030
✅ 🐘 Local Postgres179702332030
✅ 🪟 Windows14500145
✅ 📋 Other83701781015
Total7777010688845

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro119026
✅ example119026
✅ express119026
✅ fastify119026
✅ hono119026
✅ nextjs-turbopack14302
✅ nextjs-webpack14302
✅ nitro119026
✅ nuxt119026
✅ sveltekit13807
✅ vite119026
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable120025
✅ express-stable120025
✅ fastify-stable120025
✅ hono-stable120025
✅ nextjs-turbopack-canary126019
✅ nextjs-turbopack-stable-lazy-discovery-disabled14500
✅ nextjs-turbopack-stable-lazy-discovery-enabled14500
✅ nextjs-webpack-canary126019
✅ nextjs-webpack-stable-lazy-discovery-disabled14500
✅ nextjs-webpack-stable-lazy-discovery-enabled14500
✅ nitro-stable120025
✅ nuxt-stable120025
✅ sveltekit-stable13906
✅ vite-stable120025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable120025
✅ express-stable120025
✅ fastify-stable120025
✅ hono-stable120025
✅ nextjs-turbopack-canary126019
✅ nextjs-turbopack-stable-lazy-discovery-disabled14500
✅ nextjs-turbopack-stable-lazy-discovery-enabled14500
✅ nextjs-webpack-canary126019
✅ nextjs-webpack-stable-lazy-discovery-disabled14500
✅ nextjs-webpack-stable-lazy-discovery-enabled14500
✅ nitro-stable120025
✅ nuxt-stable120025
✅ sveltekit-stable13906
✅ vite-stable120025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable119026
✅ express-stable119026
✅ fastify-stable119026
✅ hono-stable119026
✅ nextjs-turbopack-canary125020
✅ nextjs-turbopack-stable-lazy-discovery-disabled14401
✅ nextjs-turbopack-stable-lazy-discovery-enabled14401
✅ nextjs-webpack-canary125020
✅ nextjs-webpack-stable-lazy-discovery-disabled14401
✅ nextjs-webpack-stable-lazy-discovery-enabled14401
✅ nitro-stable119026
✅ nuxt-stable119026
✅ sveltekit-stable13807
✅ vite-stable119026
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack14500
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable120025
✅ e2e-local-dev-tanstack-start-120025
✅ e2e-local-postgres-nest-stable119026
✅ e2e-local-postgres-tanstack-start-119026
✅ e2e-local-prod-nest-stable120025
✅ e2e-local-prod-tanstack-start-120025
✅ e2e-vercel-prod-tanstack-start119026

📋 View full workflow run

@github-actions

github-actionsBot commented May 18, 2026

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

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

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro0.040s (-5.4% 🟢)1.005s (~)0.965s101.00x
💻 LocalExpress0.041s (-2.8%)1.005s (~)0.964s101.03x
💻 LocalNext.js (Turbopack)0.060s (-4.0%)1.005s (~)0.945s101.51x
🐘 PostgresNitro0.062s (-5.2% 🟢)1.012s (~)0.950s101.56x
🐘 PostgresExpress0.067s (+9.0% 🔺)1.014s (~)0.947s101.67x
🐘 PostgresNext.js (Turbopack)0.068s (-8.6% 🟢)1.013s (~)0.945s101.71x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express0.414s (+52.9% 🔺)2.276s (-11.8% 🟢)1.863s101.00x
▲ VercelNitro0.420s (+11.1% 🔺)2.271s (+8.1% 🔺)1.851s101.01x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.093s (~)2.006s (~)0.913s101.00x
💻 LocalExpress1.094s (~)2.006s (~)0.912s101.00x
🐘 PostgresNitro1.105s (-0.7%)2.010s (~)0.906s101.01x
🐘 PostgresExpress1.114s (~)2.009s (~)0.895s101.02x
💻 LocalNext.js (Turbopack)1.136s (+0.7%)2.007s (~)0.871s101.04x
🐘 PostgresNext.js (Turbopack)1.150s (+0.6%)2.009s (~)0.859s101.05x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.681s (-4.1%)3.608s (-2.7%)1.927s101.00x
▲ VercelNitro32.051s (+1812.5% 🔺)34.165s (+890.4% 🔺)2.114s1019.06x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express10.500s (~)11.022s (~)0.523s31.00x
🐘 PostgresExpress10.570s (~)11.020s (~)0.451s31.01x
🐘 PostgresNitro10.578s (~)11.023s (~)0.444s31.01x
💻 LocalNitro10.651s (+1.0%)11.022s (~)0.372s31.01x
💻 LocalNext.js (Turbopack)10.792s (~)11.021s (~)0.229s31.03x
🐘 PostgresNext.js (Turbopack)10.937s (-0.7%)11.351s (-2.9%)0.415s31.04x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro13.882s (+4.1%)16.196s (+7.0% 🔺)2.314s21.00x
▲ VercelExpress14.281s (+6.5% 🔺)16.108s (+2.4%)1.827s21.03x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express13.701s (-1.0%)14.026s (~)0.325s51.00x
💻 LocalNitro13.738s (-0.7%)14.027s (-1.4%)0.289s51.00x
🐘 PostgresExpress13.763s (-0.5%)14.022s (~)0.259s51.00x
🐘 PostgresNitro13.921s (+0.7%)14.023s (~)0.102s51.02x
💻 LocalNext.js (Turbopack)14.364s (-0.9%)15.030s (~)0.665s41.05x
🐘 PostgresNext.js (Turbopack)14.456s (~)15.014s (~)0.558s41.06x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro21.579s (-3.8%)23.291s (-3.3%)1.712s31.00x
▲ VercelExpress24.509s (+14.1% 🔺)26.394s (+14.4% 🔺)1.885s31.14x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro12.331s (-1.1%)13.026s (~)0.695s71.00x
💻 LocalExpress12.446s (~)13.025s (~)0.579s71.01x
🐘 PostgresExpress12.526s (-1.7%)13.020s (~)0.494s71.02x
🐘 PostgresNitro12.719s (+1.1%)13.025s (~)0.307s71.03x
💻 LocalNext.js (Turbopack)13.813s (+1.0%)14.169s (+1.0%)0.356s71.12x
🐘 PostgresNext.js (Turbopack)13.852s (-0.9%)14.161s (-1.0%)0.309s71.12x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro29.649s (-6.5% 🟢)31.500s (-5.4% 🟢)1.851s31.00x
▲ VercelExpress31.179s (+5.1% 🔺)33.707s (+8.2% 🔺)2.528s31.05x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express1.208s (+1.5%)2.006s (~)0.798s151.00x
🐘 PostgresNitro1.208s (~)2.009s (~)0.800s151.00x
🐘 PostgresExpress1.231s (+2.4%)2.008s (~)0.777s151.02x
💻 LocalNitro1.279s (+4.6%)2.006s (~)0.726s151.06x
🐘 PostgresNext.js (Turbopack)1.339s (+6.4% 🔺)2.074s (+3.3%)0.734s151.11x
💻 LocalNext.js (Turbopack)1.368s (+1.4%)2.006s (~)0.638s151.13x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.970s (+16.4% 🔺)4.450s (-1.1%)1.480s71.00x
▲ VercelNitro3.506s (-5.2% 🟢)5.306s (+2.4%)1.799s61.18x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.366s (+8.5% 🔺)2.394s (+19.3% 🔺)1.028s131.00x
🐘 PostgresExpress1.389s (+10.0% 🔺)2.508s (+25.0% 🔺)1.119s121.02x
🐘 PostgresNext.js (Turbopack)1.618s (+15.5% 🔺)2.393s (+19.3% 🔺)0.776s131.18x
💻 LocalNitro1.704s (~)2.006s (~)0.302s151.25x
💻 LocalExpress1.729s (-6.1% 🟢)2.006s (-6.7% 🟢)0.277s151.27x
💻 LocalNext.js (Turbopack)1.842s (-2.0%)2.074s (-3.5%)0.232s151.35x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.196s (-27.7% 🟢)4.831s (-21.0% 🟢)1.635s71.00x
▲ VercelNitro3.207s (-48.6% 🟢)4.657s (-39.9% 🟢)1.450s71.00x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.680s (+19.4% 🔺)4.141s (+106.2% 🔺)2.461s81.00x
🐘 PostgresExpress1.718s (+22.5% 🔺)3.889s (+93.6% 🔺)2.172s81.02x
🐘 PostgresNext.js (Turbopack)3.508s (+94.5% 🔺)4.297s (+87.1% 🔺)0.789s72.09x
💻 LocalNitro4.696s (-10.5% 🟢)5.347s (-11.1% 🟢)0.650s62.80x
💻 LocalExpress5.272s (-2.1%)6.013s (-2.7%)0.742s53.14x
💻 LocalNext.js (Turbopack)5.395s (-5.4% 🟢)6.014s (-3.2%)0.619s53.21x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.396s (-51.8% 🟢)6.218s (-45.2% 🟢)1.823s61.00x
▲ VercelNitro4.696s (-72.4% 🟢)6.708s (-63.3% 🟢)2.012s51.07x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.221s (+0.8%)2.074s (+3.3%)0.853s151.00x
🐘 PostgresNitro1.224s (+2.8%)2.008s (~)0.784s151.00x
🐘 PostgresNext.js (Turbopack)1.278s (+1.2%)2.008s (~)0.729s151.05x
💻 LocalNext.js (Turbopack)1.391s (+2.9%)2.006s (~)0.615s151.14x
💻 LocalNitro1.502s (~)2.006s (~)0.505s151.23x
💻 LocalExpress1.539s (-7.2% 🟢)2.006s (-3.3%)0.467s151.26x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.632s (+2.1%)4.218s (~)1.586s81.00x
▲ VercelExpress2.986s (+10.6% 🔺)4.712s (~)1.726s71.13x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.394s (+9.6% 🔺)2.224s (+10.8% 🔺)0.830s141.00x
🐘 PostgresExpress1.424s (+9.8% 🔺)2.317s (+15.3% 🔺)0.892s131.02x
🐘 PostgresNext.js (Turbopack)1.506s (+7.3% 🔺)2.150s (+7.1% 🔺)0.644s141.08x
💻 LocalExpress1.865s (-13.6% 🟢)2.295s (-14.2% 🟢)0.429s141.34x
💻 LocalNext.js (Turbopack)2.055s (~)2.591s (-13.9% 🟢)0.536s121.47x
💻 LocalNitro2.153s (-0.8%)2.676s (+3.2%)0.523s121.55x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.328s (-2.9%)5.132s (+0.6%)1.803s61.00x
▲ VercelNitro3.433s (-28.9% 🟢)5.018s (-21.6% 🟢)1.585s61.03x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.617s (+16.5% 🔺)4.299s (+114.1% 🔺)2.682s71.00x
🐘 PostgresExpress1.837s (+31.1% 🔺)3.891s (+93.5% 🔺)2.053s81.14x
🐘 PostgresNext.js (Turbopack)3.367s (+86.2% 🔺)4.582s (+106.0% 🔺)1.215s72.08x
💻 LocalExpress4.294s (-32.4% 🟢)5.016s (-28.5% 🟢)0.721s62.66x
💻 LocalNitro5.167s (-5.0%)5.851s (-2.7%)0.685s63.19x
💻 LocalNext.js (Turbopack)5.712s (-5.7% 🟢)6.017s (-11.7% 🟢)0.305s53.53x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.990s (-21.8% 🟢)5.843s (-19.3% 🟢)1.853s61.00x
▲ VercelNitro4.324s (-38.2% 🟢)6.347s (-26.2% 🟢)2.023s51.08x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro0.589s (-1.8%)1.005s (~)0.416s601.00x
🐘 PostgresExpress0.603s (~)1.023s (+1.6%)0.421s591.02x
🐘 PostgresNitro0.603s (+4.1%)1.007s (~)0.404s601.02x
💻 LocalExpress0.611s (~)1.039s (+3.4%)0.429s581.04x
🐘 PostgresNext.js (Turbopack)0.811s (-5.0% 🟢)1.006s (-1.8%)0.194s601.38x
💻 LocalNext.js (Turbopack)0.892s (+3.3%)1.057s (+3.5%)0.166s571.51x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express5.921s (-41.6% 🟢)7.609s (-35.4% 🟢)1.688s81.00x
▲ VercelNitro6.497s (+31.0% 🔺)8.122s (+24.7% 🔺)1.625s81.10x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.360s (-5.1% 🟢)2.008s (~)0.647s451.00x
🐘 PostgresNitro1.410s (-1.8%)2.008s (-1.1%)0.598s451.04x
💻 LocalNitro1.508s (~)2.006s (~)0.497s451.11x
💻 LocalExpress1.526s (-1.8%)2.028s (~)0.502s451.12x
🐘 PostgresNext.js (Turbopack)1.894s (-6.7% 🟢)2.053s (-22.7% 🟢)0.159s441.39x
💻 LocalNext.js (Turbopack)2.084s (-0.7%)3.008s (~)0.924s301.53x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro14.267s (+8.9% 🔺)16.133s (+11.6% 🔺)1.866s61.00x
▲ VercelExpress15.632s (+8.8% 🔺)17.362s (+7.5% 🔺)1.729s61.10x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro2.756s (-1.4%)3.059s (-3.4%)0.303s401.00x
🐘 PostgresExpress2.805s (~)3.112s (-0.8%)0.307s391.02x
💻 LocalExpress3.225s (-1.8%)3.977s (-0.8%)0.752s311.17x
💻 LocalNitro3.248s (-1.5%)4.009s (~)0.761s301.18x
🐘 PostgresNext.js (Turbopack)3.766s (-5.5% 🟢)4.043s (-6.7% 🟢)0.276s301.37x
💻 LocalNext.js (Turbopack)4.291s (-1.3%)5.010s (~)0.720s241.56x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express32.130s (+22.5% 🔺)34.248s (+21.4% 🔺)2.117s41.00x
▲ VercelNitro34.856s (+19.7% 🔺)36.875s (+19.7% 🔺)2.019s41.08x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.244s (+11.1% 🔺)1.007s (~)0.763s601.00x
🐘 PostgresExpress0.250s (+7.4% 🔺)1.006s (~)0.756s601.03x
🐘 PostgresNext.js (Turbopack)0.289s (+4.2%)1.006s (~)0.717s601.19x
💻 LocalExpress0.423s (+2.4%)1.004s (~)0.582s601.74x
💻 LocalNitro0.431s (~)1.004s (-1.7%)0.573s601.77x
💻 LocalNext.js (Turbopack)0.569s (+4.1%)1.005s (~)0.436s602.33x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.874s (-27.0% 🟢)3.565s (-13.2% 🟢)1.692s171.00x
▲ VercelNitro2.088s (-26.0% 🟢)3.684s (-14.7% 🟢)1.596s171.11x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.393s (+9.5% 🔺)1.053s (+4.7%)0.661s861.00x
🐘 PostgresExpress0.411s (+9.7% 🔺)1.041s (+3.5%)0.630s871.05x
🐘 PostgresNext.js (Turbopack)0.583s (+16.8% 🔺)1.091s (+8.4% 🔺)0.508s831.48x
💻 LocalNitro2.128s (-2.5%)2.737s (+0.8%)0.609s335.42x
💻 LocalExpress2.226s (+9.2% 🔺)2.766s (+9.1% 🔺)0.540s335.67x
💻 LocalNext.js (Turbopack)2.521s (+2.9%)3.218s (+2.2%)0.697s296.42x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.431s (-62.9% 🟢)4.945s (-54.5% 🟢)1.514s191.00x
▲ VercelExpress21.388s (+295.0% 🔺)23.082s (+226.3% 🔺)1.694s176.23x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.795s (+16.7% 🔺)1.567s (+55.7% 🔺)0.772s771.00x
🐘 PostgresExpress0.805s (+15.6% 🔺)1.419s (+41.0% 🔺)0.614s851.01x
🐘 PostgresNext.js (Turbopack)2.760s (+174.5% 🔺)3.681s (+98.5% 🔺)0.921s333.47x
💻 LocalExpress9.388s (+0.8%)10.195s (+3.3%)0.807s1211.80x
💻 LocalNitro9.416s (+1.5%)9.874s (+0.8%)0.458s1311.84x
💻 LocalNext.js (Turbopack)10.703s (-2.2%)11.576s (+0.8%)0.873s1113.46x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express6.877s (-63.6% 🟢)8.574s (-58.2% 🟢)1.697s141.00x
▲ VercelNitro8.266s (-57.1% 🟢)10.222s (-53.8% 🟢)1.957s121.20x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express1.160s (-0.7%)2.005s (~)0.010s (-14.5% 🟢)2.017s (~)0.857s101.00x
🐘 PostgresNitro1.165s (-0.7%)2.001s (~)0.001s (-15.4% 🟢)2.010s (~)0.846s101.00x
💻 LocalNitro1.167s (~)2.005s (~)0.010s (~)2.018s (~)0.851s101.01x
🐘 PostgresExpress1.172s (-0.6%)2.000s (~)0.001s (-8.3% 🟢)2.010s (~)0.838s101.01x
💻 LocalNext.js (Turbopack)1.211s (~)2.004s (~)0.013s (+0.8%)2.020s (~)0.809s101.04x
🐘 PostgresNext.js (Turbopack)1.234s (-0.9%)2.002s (~)0.001s (+7.7% 🔺)2.011s (~)0.777s101.06x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.797s (+14.3% 🔺)4.260s (+26.7% 🔺)0.638s (-32.8% 🟢)5.340s (+12.0% 🔺)2.543s101.00x
▲ VercelExpress32.861s (+1415.8% 🔺)3.322s (+3.7%)1.152s (+26.8% 🔺)35.409s (+673.0% 🔺)2.548s1011.75x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro | Express

stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.566s (-1.8%)2.009s (~)0.012s (-1.6%)2.023s (~)0.457s301.00x
💻 LocalExpress1.568s (~)2.010s (~)0.013s (+14.2% 🔺)2.026s (~)0.457s301.00x
🐘 PostgresExpress1.579s (-1.4%)2.006s (~)0.005s (+7.1% 🔺)2.027s (~)0.448s301.01x
🐘 PostgresNitro1.616s (~)2.006s (~)0.005s (-8.7% 🟢)2.025s (~)0.408s301.03x
💻 LocalNext.js (Turbopack)1.730s (-0.6%)2.010s (~)0.012s (-1.9%)2.025s (~)0.295s301.10x
🐘 PostgresNext.js (Turbopack)1.755s (-1.7%)2.010s (~)0.005s (+2.7%)2.026s (~)0.270s301.12x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express5.998s (-1.2%)7.151s (-2.0%)0.235s (-10.5% 🟢)7.964s (~)1.966s81.00x
▲ VercelNitro6.812s (+15.1% 🔺)7.759s (+10.4% 🔺)0.245s (-47.7% 🟢)8.558s (+7.1% 🔺)1.745s81.14x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Express | Nitro

10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.798s (+12.6% 🔺)1.068s (+2.4%)0.000s (-66.1% 🟢)1.088s (+2.3%)0.291s561.00x
🐘 PostgresExpress0.805s (+13.8% 🔺)1.084s (+6.9% 🔺)0.000s (+221.8% 🔺)1.099s (+6.8% 🔺)0.293s551.01x
🐘 PostgresNext.js (Turbopack)0.998s (+15.9% 🔺)1.463s (+34.1% 🔺)0.000s (-100.0% 🟢)1.471s (+33.9% 🔺)0.473s411.25x
💻 LocalNitro1.386s (-3.8%)2.013s (~)0.001s (+260.0% 🔺)2.015s (~)0.630s301.74x
💻 LocalExpress1.391s (+3.1%)2.013s (~)0.000s (+175.0% 🔺)2.015s (~)0.624s301.74x
💻 LocalNext.js (Turbopack)1.462s (~)2.013s (~)0.000s (-40.0% 🟢)2.016s (~)0.554s301.83x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.320s (-1.2%)4.629s (+1.2%)0.000s (-100.0% 🟢)5.052s (~)1.731s121.00x
▲ VercelNitro3.595s (-5.5% 🟢)4.764s (-1.3%)0.001s (+Infinity% 🔺)5.183s (-0.7%)1.588s121.08x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Express | Nitro

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

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.508s (+13.1% 🔺)2.032s (~)0.000s (NaN%)2.061s (~)0.553s301.00x
🐘 PostgresExpress1.669s (+9.4% 🔺)2.217s (~)0.000s (-50.0% 🟢)2.252s (~)0.583s271.11x
🐘 PostgresNext.js (Turbopack)2.069s (+19.6% 🔺)2.404s (+6.3% 🔺)0.000s (+Infinity% 🔺)2.432s (+7.1% 🔺)0.363s251.37x
💻 LocalNext.js (Turbopack)2.812s (-1.4%)3.357s (-1.7%)0.001s (+100.0% 🔺)3.361s (-1.7%)0.549s181.86x
💻 LocalNitro2.994s (-5.5% 🟢)3.613s (-5.8% 🟢)0.000s (+119.6% 🔺)3.620s (-5.8% 🟢)0.626s171.98x
💻 LocalExpress3.129s (+1.1%)3.902s (+6.3% 🔺)0.001s (+44.9% 🔺)3.905s (+6.2% 🔺)0.776s162.07x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro5.070s (-31.3% 🟢)6.077s (-23.4% 🟢)0.000s (+Infinity% 🔺)6.502s (-28.0% 🟢)1.433s101.00x
▲ VercelExpress5.299s (~)6.264s (-4.8%)0.000s (NaN%)6.717s (-6.6% 🟢)1.418s91.05x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro | Express

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNitro10/21
🐘 PostgresNitro14/21
▲ VercelExpress13/21
Fastest World by Framework

Winner determined by most benchmark wins

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

Worlds:

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

📋 View full workflow run


Some benchmark jobs failed:

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

Check the workflow run for details.

Comment threaddocs/content/docs/v4/api-reference/workflow/create-hook.mdx Outdated

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

Adds a new hook.ready promise to let workflow code wait for hook registration (hook_created) independently of receiving payloads, enabling deterministic token claiming for idempotency patterns (and consistent conflict handling via HookConflictError).

Changes:

  • Extend the public Hook interface with ready: Promise<void> and implement it in the workflow VM hook runtime.
  • Resolve/reject ready waiters based on hook_created / hook_conflict, including suspension behavior when awaiting readiness before registration is recorded.
  • Add tests and update v4/v5 documentation to describe hook.ready (and webhook readiness via the shared Hook interface).

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 1 comment.

Show a summary per file
FileDescription
packages/core/src/workflow/hook.tsImplements hook.ready tracking/queuing and resolves/rejects it on hook_created/hook_conflict, including suspension when needed.
packages/core/src/workflow/hook.test.tsAdds unit tests covering ready resolution, suspension behavior, conflict rejection, and ensuring payload consumption is unchanged.
packages/core/src/workflow.test.tsAdds integration tests verifying suspension, resolution, and conflict behavior when hook.ready is used inside runWorkflow.
packages/core/src/create-hook.tsExtends the public Hook interface with documented ready: Promise<void>.
docs/content/docs/v5/how-it-works/event-sourcing.mdxDocuments that hook.ready rejects on hook conflicts in addition to payload awaiting.
docs/content/docs/v5/foundations/hooks.mdxAdds a section explaining when/how to use hook.ready to wait for registration.
docs/content/docs/v5/api-reference/workflow/create-webhook.mdxNotes ready availability on returned webhook objects and how to use it before sharing the URL.
docs/content/docs/v5/api-reference/workflow/create-hook.mdxDocuments hook.ready and adds a registration-focused example.
docs/content/docs/v4/how-it-works/event-sourcing.mdxMirrors v5 event-sourcing updates for hook.ready conflict semantics.
docs/content/docs/v4/foundations/hooks.mdxMirrors v5 “Waiting for Hook Registration” guidance.
docs/content/docs/v4/api-reference/workflow/create-webhook.mdxMirrors v5 webhook ready documentation note.
docs/content/docs/v4/api-reference/workflow/create-hook.mdxMirrors v5 hook.ready API docs and example.
.changeset/hook-ready.mdDeclares minor bumps for @workflow/core and workflow to ship the new API.

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

Comment threadpackages/core/src/workflow/hook.test.ts Outdated
Comment threaddocs/content/docs/v4/api-reference/workflow/create-webhook.mdx Outdated
Comment threaddocs/content/docs/v5/api-reference/workflow/create-webhook.mdx Outdated

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

Copilot reviewed 20 out of 20 changed files in this pull request and generated 1 comment.

Comment threadpackages/core/src/workflow/hook.ts Outdated
Comment threadpackages/core/src/runtime.ts Outdated
pranaygpand others added 3 commits May 18, 2026 21:44
…hook-ready
* origin-https/main: (119 commits)
fix: settle aborted parallel steps before completing abortParallelWorkflow (#2244)
Add native v4 workflow attribute events (#2226)
Version Packages (beta) (#2326)
[ci] Fix flaky windows unit tests (#2359)
Capture Vercel runtime logs when e2e Vercel Prod lanes fail (#2356)
Fix e2e failure reporting under vitest 4 and preserve fetch error causes (#2355)
[core] Fix process crash from rejected waitUntil promises (#2336)
[core] Remove duplicate `waitUntil` for suspension handler async operations (#2345)
Prevent local tests from hanging (#2338)
feat(core): add optional namespace for queue topic prefix (#2305)
[web-shared] Show precise durations in the new trace viewer (#2335)
Validate unique workflow step IDs at build time (#2018)
Move run attributes into their own detail card (#2327)
[core] Forward-port stream reconnect to getReadable level (#2318)
[docs] Add "Step executed multiple times" error page (#2310)
Fix flickering on the detail panel when navigating the trace viewer (#2325)
Version Packages (beta) (#2254)
Patch vulnerable package dependencies (#2301)
Deprecate DurableAgent and update it with WorkflowAgent in v5 docs (#2285)
[codex] Fix partial world-local exclusive writes (#2296)
...
# Conflicts:
#	packages/core/src/runtime.ts
#	packages/core/src/runtime/suspension-handler.ts
- hook.hasConflict resolves true when the token is owned by another
active hook, false once registration is committed — no throw, so
workflows can branch on conflicts early. Awaiting it suspends the
workflow to commit the hook registration (createHook alone does not).
- Chain the already-created fast-path through promiseQueue so
resolution order matches event-log order (review feedback).
- Skip inline step execution when a suspension has an awaited hook
creation so the hasConflict continuation can advance independently
of step execution (review feedback).
- Update unit tests, e2e tests, workbench workflows, and v4/v5 docs.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

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

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

Comment threaddocs/content/docs/v5/api-reference/workflow/create-webhook.mdx Outdated
Comment threaddocs/content/docs/v4/api-reference/workflow/create-webhook.mdx Outdated

@TooTallNateTooTallNate left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Approve — determinism-safe design, strong test coverage; one docs-sequencing flag

I built @workflow/core from this branch and ran the full unit suite locally (1146 tests, matching the PR's claimed validation) plus the three new test files individually (116 tests). All green.

The semantics hold up under determinism scrutiny

This touches the workflow VM's most replay-sensitive machinery, so I traced the settlement paths carefully:

  • Every hasConflict settlement goes through ctx.promiseQueue — both fast-paths (hasCreated / conflict-already-known) and the event-driven paths (hook_created → resolve false, hook_conflict → resolve true). Resolution order always matches event-log order, which is the invariant that keeps replay deterministic.
  • The hook_conflict consumer drains payload and hasConflict waiters synchronously (deferring actual settlement through promiseQueue), so the null-event handler can't see them and raise a spurious suspension — same pattern the existing payload path uses.
  • The suspension trigger condition (hasConflictPromises.length > 0 && !hasCreated && !hasConflict in the null-event handler, plus the eventLogEmpty fast-path in createHasConflictPromise) correctly mirrors the payload-awaiter pattern for both orderings of "awaited before/after log exhaustion." Both are unit-tested.
  • hasAwaitedHookCreation only affects scheduling, never event-log content — it decides which invocation executes steps (queue-all + timeoutSeconds: 0 re-invoke instead of inline), so there's no replay-divergence surface. The reasoning in the runtime.ts comment is exactly right: running a step inline would serialize work the workflow expressed as parallel (hook.hasConflict.then(() => stepB()) racing await stepA()).
  • The EntityConflictError path returning hasAwaitedHookCreation: true is correct and subtle: on a crash-replay where the hook event already exists, the awaiter still needs an immediate re-invocation to consume hook_created and resolve — silently swallowing it would hang the awaiter until something else woke the run.
  • The e2e parallelism proof (hookHasConflictThenStepParallelWorkflow: 100ms stepB triggered via the hasConflict continuation must complete while 10s stepA is still running, asserted via step timestamps) tests the actual claim end-to-end rather than just the mechanism.

My earlier docs comments were both addressed (the create-hook example now explicitly points to awaiting the hook/iterating for payload data).

One sequencing flag: the v4 docs (should-fix before merge, or fast-follow the backport)

hasConflict does not exist on stable — I checked origin/stable:packages/core/src/create-hook.ts and there are zero references. But this PR adds the API to the v4 docs (docs/content/docs/v4/...), and docs deploy from main. The moment this merges, the live v4 documentation advertises a property that no released 4.x SDK has — and it'll stay wrong until a backport merges to stableand ships in a 4.x release.

Two clean options:

  1. Drop the v4 docs pages from this PR and let the backport PR carry them (the backport flow cherry-picks docs/content/ normally, so they'd land together with the code on stable — though note they'd then need a separate main-side commit since v4 docs render from main), or
  2. Keep them but treat the stable backport + 4.x release as an immediate follow-up so the docs-reality gap is measured in days.

Given the changeset is minor (correct for a new API) and this is a self-contained feature — squarely in the auto-backport criteria — option 2 is probably fine, but it should be a deliberate choice, not an accident.

Non-blocking notes

  • get hasConflict() returns a fresh promise per property access (hook.hasConflict !== hook.hasConflict). Each settles deterministically through promiseQueue, so it's correctness-neutral, but identity-unstable promises from a property (vs a method) can surprise — e.g. registering two waiters from what reads like one. Caching the promise per hook after first access would be friendlier and would also make the hasConflictAwaiter flag-set happen exactly once. Fine as-is.
  • The local hasConflict boolean inside createCreateHook now shadows the public property name; a rename (sawConflictEvent?) would help the next reader.

CI

The two red E2E jobs are baseline flakes unrelated to this PR: astro fails AbortController > abortFromStepWorkflow (the abort area #2244 just touched on main) and hono fails resilient start: addTenWorkflow ... run_created returns 500 (known flake). No hook test failed anywhere. The required check needs a re-run to go green before merge.

Nice work — this is a well-reasoned addition in a part of the codebase where it's very easy to be subtly wrong.

TooTallNateand others added 3 commits June 11, 2026 14:29
State both resolution values explicitly (true = token already owned,
false = registered) instead of a parenthetical that only described the
false case.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment threadAGENTS.md
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
expect.anything()
);
expect(result.hasAwaitedHookCreation).toBe(true);
expect(result.timeoutSeconds).toBeUndefined();

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.

Test assertions check non-existent result.timeoutSeconds property, making toBeUndefined() assertions pass trivially without verifying actual behavior.

Fix on Vercel

@github-actions

Copy link
Copy Markdown
Contributor

No backport to stable for e163422 (AI decision).

This is a new minor feature adding the hook.hasConflict API to the public hook interface, intended for the next major release on main. It's a self-contained feature addition (not a bug fix) and the changeset is marked as a minor bump for the beta channel. Per the criteria, new feature additions targeted at the next major release should generally stay on main rather than be backported to stable.

To override, re-run the Backport to stable workflow manually via workflow_dispatch and paste this commit SHA into the ref input:

e163422551e80eda88a15fec3d3d574a00c53e93

@github-actionsgithub-actionsBot mentioned this pull request Jun 11, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Backport PR opened against stable: #2372. Merge conflicts were resolved by AI — please review carefully. (backport job run)

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.

3 participants

@pranaygp@TooTallNate
, '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 `hook.hasConflict` for early hook conflict detection by pranaygp · Pull Request #2015 · vercel/workflow · GitHub
Skip to content

Add hook.hasConflict for early hook conflict detection - #2015

Merged
TooTallNate merged 9 commits into
mainfrom
pranaygp/codex/hook-ready
Jun 11, 2026
Merged

Add hook.hasConflict for early hook conflict detection#2015
TooTallNate merged 9 commits into
mainfrom
pranaygp/codex/hook-ready

Conversation

@pranaygp

@pranaygppranaygp commented May 18, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds hook.hasConflict, a lazy Promise<boolean> getter that lets workflow code commit hook registration and check for token conflicts without waiting for hook payload data.

Calling createHook() alone does not register the hook — registration is only committed when the workflow suspends. Awaiting hook.hasConflict suspends the workflow to commit the registration, then resolves with false once hook_created is recorded, or true if another active hook already owns the token. Unlike awaiting the hook payload (which rejects with HookConflictError on conflict), hasConflict never throws, so workflows can branch on conflicts early:

using hook=createHook({token: `order:${orderId}`});if(awaithook.hasConflict){return;// another run already owns this token}

This gives workflows a way to claim a deterministic hook token for idempotency before running duplicate-sensitive work, and is a prerequisite for #2011, where the idempotency docs/examples should use await hook.hasConflict instead of requiring consumers to wait for or resume hook data just to claim the token.

Changes

  • Add Hook.hasConflict: Promise<boolean> to the public hook interface.
  • Resolve pending hasConflict waiters with false on hook_created and true on hook_conflict; both fast-paths settle through promiseQueue so resolution order matches event-log order.
  • Suspend workflow execution when hook.hasConflict is awaited before hook creation is recorded.
  • Skip inline step execution when a suspension contains an awaited hook creation, so a hasConflict continuation (e.g. hook.hasConflict.then(() => stepB())) can advance in parallel with other steps instead of only after they finish.
  • Document hook.hasConflict in the v4 and v5 hook/webhook docs and event-sourcing pages, including the suspension-commits-registration semantics.
  • Add core unit tests (resolution, suspension, conflict, payload preservation, suspension-handler scheduling) and e2e tests (registration without payload, non-blocking steps, parallel continuation scheduling, conflict resolves true).

Validation

  • cd packages/core && pnpm typecheck && pnpm vitest run src/ (1146 tests)
  • Hook e2e suite against local nextjs-turbopack dev server: pnpm vitest run packages/core/e2e/e2e.test.ts -t 'hook' (20 tests, including the new parallel-continuation test)

Docs Preview

Changed pages on the docs preview deployment:

Pagev4v5
createHook() API referencev4v5
createWebhook() API referencev4v5
Foundations: Hooksv4v5
How It Works: Event Sourcingv4v5

CopilotAI review requested due to automatic review settings May 18, 2026 23:22
@pranaygp
pranaygp requested a review from a team as a code ownerMay 18, 2026 23:22
@changeset-bot

changeset-botBot commented May 18, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 2342dae

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

This PR includes changesets to release 17 packages
NameType
@workflow/coreMinor
workflowMinor
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/webPatch
@workflow/world-testingPatch
@workflow/aiMajor
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch
@workflow/nuxtPatch

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 May 18, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented May 18, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production137602191595
✅ 💻 Local Development181102192030
✅ 📦 Local Production181102192030
✅ 🐘 Local Postgres179702332030
✅ 🪟 Windows14500145
✅ 📋 Other83701781015
Total7777010688845

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro119026
✅ example119026
✅ express119026
✅ fastify119026
✅ hono119026
✅ nextjs-turbopack14302
✅ nextjs-webpack14302
✅ nitro119026
✅ nuxt119026
✅ sveltekit13807
✅ vite119026
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable120025
✅ express-stable120025
✅ fastify-stable120025
✅ hono-stable120025
✅ nextjs-turbopack-canary126019
✅ nextjs-turbopack-stable-lazy-discovery-disabled14500
✅ nextjs-turbopack-stable-lazy-discovery-enabled14500
✅ nextjs-webpack-canary126019
✅ nextjs-webpack-stable-lazy-discovery-disabled14500
✅ nextjs-webpack-stable-lazy-discovery-enabled14500
✅ nitro-stable120025
✅ nuxt-stable120025
✅ sveltekit-stable13906
✅ vite-stable120025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable120025
✅ express-stable120025
✅ fastify-stable120025
✅ hono-stable120025
✅ nextjs-turbopack-canary126019
✅ nextjs-turbopack-stable-lazy-discovery-disabled14500
✅ nextjs-turbopack-stable-lazy-discovery-enabled14500
✅ nextjs-webpack-canary126019
✅ nextjs-webpack-stable-lazy-discovery-disabled14500
✅ nextjs-webpack-stable-lazy-discovery-enabled14500
✅ nitro-stable120025
✅ nuxt-stable120025
✅ sveltekit-stable13906
✅ vite-stable120025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable119026
✅ express-stable119026
✅ fastify-stable119026
✅ hono-stable119026
✅ nextjs-turbopack-canary125020
✅ nextjs-turbopack-stable-lazy-discovery-disabled14401
✅ nextjs-turbopack-stable-lazy-discovery-enabled14401
✅ nextjs-webpack-canary125020
✅ nextjs-webpack-stable-lazy-discovery-disabled14401
✅ nextjs-webpack-stable-lazy-discovery-enabled14401
✅ nitro-stable119026
✅ nuxt-stable119026
✅ sveltekit-stable13807
✅ vite-stable119026
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack14500
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable120025
✅ e2e-local-dev-tanstack-start-120025
✅ e2e-local-postgres-nest-stable119026
✅ e2e-local-postgres-tanstack-start-119026
✅ e2e-local-prod-nest-stable120025
✅ e2e-local-prod-tanstack-start-120025
✅ e2e-vercel-prod-tanstack-start119026

📋 View full workflow run

@github-actions

github-actionsBot commented May 18, 2026

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

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

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro0.040s (-5.4% 🟢)1.005s (~)0.965s101.00x
💻 LocalExpress0.041s (-2.8%)1.005s (~)0.964s101.03x
💻 LocalNext.js (Turbopack)0.060s (-4.0%)1.005s (~)0.945s101.51x
🐘 PostgresNitro0.062s (-5.2% 🟢)1.012s (~)0.950s101.56x
🐘 PostgresExpress0.067s (+9.0% 🔺)1.014s (~)0.947s101.67x
🐘 PostgresNext.js (Turbopack)0.068s (-8.6% 🟢)1.013s (~)0.945s101.71x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express0.414s (+52.9% 🔺)2.276s (-11.8% 🟢)1.863s101.00x
▲ VercelNitro0.420s (+11.1% 🔺)2.271s (+8.1% 🔺)1.851s101.01x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.093s (~)2.006s (~)0.913s101.00x
💻 LocalExpress1.094s (~)2.006s (~)0.912s101.00x
🐘 PostgresNitro1.105s (-0.7%)2.010s (~)0.906s101.01x
🐘 PostgresExpress1.114s (~)2.009s (~)0.895s101.02x
💻 LocalNext.js (Turbopack)1.136s (+0.7%)2.007s (~)0.871s101.04x
🐘 PostgresNext.js (Turbopack)1.150s (+0.6%)2.009s (~)0.859s101.05x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.681s (-4.1%)3.608s (-2.7%)1.927s101.00x
▲ VercelNitro32.051s (+1812.5% 🔺)34.165s (+890.4% 🔺)2.114s1019.06x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express10.500s (~)11.022s (~)0.523s31.00x
🐘 PostgresExpress10.570s (~)11.020s (~)0.451s31.01x
🐘 PostgresNitro10.578s (~)11.023s (~)0.444s31.01x
💻 LocalNitro10.651s (+1.0%)11.022s (~)0.372s31.01x
💻 LocalNext.js (Turbopack)10.792s (~)11.021s (~)0.229s31.03x
🐘 PostgresNext.js (Turbopack)10.937s (-0.7%)11.351s (-2.9%)0.415s31.04x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro13.882s (+4.1%)16.196s (+7.0% 🔺)2.314s21.00x
▲ VercelExpress14.281s (+6.5% 🔺)16.108s (+2.4%)1.827s21.03x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express13.701s (-1.0%)14.026s (~)0.325s51.00x
💻 LocalNitro13.738s (-0.7%)14.027s (-1.4%)0.289s51.00x
🐘 PostgresExpress13.763s (-0.5%)14.022s (~)0.259s51.00x
🐘 PostgresNitro13.921s (+0.7%)14.023s (~)0.102s51.02x
💻 LocalNext.js (Turbopack)14.364s (-0.9%)15.030s (~)0.665s41.05x
🐘 PostgresNext.js (Turbopack)14.456s (~)15.014s (~)0.558s41.06x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro21.579s (-3.8%)23.291s (-3.3%)1.712s31.00x
▲ VercelExpress24.509s (+14.1% 🔺)26.394s (+14.4% 🔺)1.885s31.14x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro12.331s (-1.1%)13.026s (~)0.695s71.00x
💻 LocalExpress12.446s (~)13.025s (~)0.579s71.01x
🐘 PostgresExpress12.526s (-1.7%)13.020s (~)0.494s71.02x
🐘 PostgresNitro12.719s (+1.1%)13.025s (~)0.307s71.03x
💻 LocalNext.js (Turbopack)13.813s (+1.0%)14.169s (+1.0%)0.356s71.12x
🐘 PostgresNext.js (Turbopack)13.852s (-0.9%)14.161s (-1.0%)0.309s71.12x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro29.649s (-6.5% 🟢)31.500s (-5.4% 🟢)1.851s31.00x
▲ VercelExpress31.179s (+5.1% 🔺)33.707s (+8.2% 🔺)2.528s31.05x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express1.208s (+1.5%)2.006s (~)0.798s151.00x
🐘 PostgresNitro1.208s (~)2.009s (~)0.800s151.00x
🐘 PostgresExpress1.231s (+2.4%)2.008s (~)0.777s151.02x
💻 LocalNitro1.279s (+4.6%)2.006s (~)0.726s151.06x
🐘 PostgresNext.js (Turbopack)1.339s (+6.4% 🔺)2.074s (+3.3%)0.734s151.11x
💻 LocalNext.js (Turbopack)1.368s (+1.4%)2.006s (~)0.638s151.13x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.970s (+16.4% 🔺)4.450s (-1.1%)1.480s71.00x
▲ VercelNitro3.506s (-5.2% 🟢)5.306s (+2.4%)1.799s61.18x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.366s (+8.5% 🔺)2.394s (+19.3% 🔺)1.028s131.00x
🐘 PostgresExpress1.389s (+10.0% 🔺)2.508s (+25.0% 🔺)1.119s121.02x
🐘 PostgresNext.js (Turbopack)1.618s (+15.5% 🔺)2.393s (+19.3% 🔺)0.776s131.18x
💻 LocalNitro1.704s (~)2.006s (~)0.302s151.25x
💻 LocalExpress1.729s (-6.1% 🟢)2.006s (-6.7% 🟢)0.277s151.27x
💻 LocalNext.js (Turbopack)1.842s (-2.0%)2.074s (-3.5%)0.232s151.35x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.196s (-27.7% 🟢)4.831s (-21.0% 🟢)1.635s71.00x
▲ VercelNitro3.207s (-48.6% 🟢)4.657s (-39.9% 🟢)1.450s71.00x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.680s (+19.4% 🔺)4.141s (+106.2% 🔺)2.461s81.00x
🐘 PostgresExpress1.718s (+22.5% 🔺)3.889s (+93.6% 🔺)2.172s81.02x
🐘 PostgresNext.js (Turbopack)3.508s (+94.5% 🔺)4.297s (+87.1% 🔺)0.789s72.09x
💻 LocalNitro4.696s (-10.5% 🟢)5.347s (-11.1% 🟢)0.650s62.80x
💻 LocalExpress5.272s (-2.1%)6.013s (-2.7%)0.742s53.14x
💻 LocalNext.js (Turbopack)5.395s (-5.4% 🟢)6.014s (-3.2%)0.619s53.21x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.396s (-51.8% 🟢)6.218s (-45.2% 🟢)1.823s61.00x
▲ VercelNitro4.696s (-72.4% 🟢)6.708s (-63.3% 🟢)2.012s51.07x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.221s (+0.8%)2.074s (+3.3%)0.853s151.00x
🐘 PostgresNitro1.224s (+2.8%)2.008s (~)0.784s151.00x
🐘 PostgresNext.js (Turbopack)1.278s (+1.2%)2.008s (~)0.729s151.05x
💻 LocalNext.js (Turbopack)1.391s (+2.9%)2.006s (~)0.615s151.14x
💻 LocalNitro1.502s (~)2.006s (~)0.505s151.23x
💻 LocalExpress1.539s (-7.2% 🟢)2.006s (-3.3%)0.467s151.26x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.632s (+2.1%)4.218s (~)1.586s81.00x
▲ VercelExpress2.986s (+10.6% 🔺)4.712s (~)1.726s71.13x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.394s (+9.6% 🔺)2.224s (+10.8% 🔺)0.830s141.00x
🐘 PostgresExpress1.424s (+9.8% 🔺)2.317s (+15.3% 🔺)0.892s131.02x
🐘 PostgresNext.js (Turbopack)1.506s (+7.3% 🔺)2.150s (+7.1% 🔺)0.644s141.08x
💻 LocalExpress1.865s (-13.6% 🟢)2.295s (-14.2% 🟢)0.429s141.34x
💻 LocalNext.js (Turbopack)2.055s (~)2.591s (-13.9% 🟢)0.536s121.47x
💻 LocalNitro2.153s (-0.8%)2.676s (+3.2%)0.523s121.55x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.328s (-2.9%)5.132s (+0.6%)1.803s61.00x
▲ VercelNitro3.433s (-28.9% 🟢)5.018s (-21.6% 🟢)1.585s61.03x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.617s (+16.5% 🔺)4.299s (+114.1% 🔺)2.682s71.00x
🐘 PostgresExpress1.837s (+31.1% 🔺)3.891s (+93.5% 🔺)2.053s81.14x
🐘 PostgresNext.js (Turbopack)3.367s (+86.2% 🔺)4.582s (+106.0% 🔺)1.215s72.08x
💻 LocalExpress4.294s (-32.4% 🟢)5.016s (-28.5% 🟢)0.721s62.66x
💻 LocalNitro5.167s (-5.0%)5.851s (-2.7%)0.685s63.19x
💻 LocalNext.js (Turbopack)5.712s (-5.7% 🟢)6.017s (-11.7% 🟢)0.305s53.53x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.990s (-21.8% 🟢)5.843s (-19.3% 🟢)1.853s61.00x
▲ VercelNitro4.324s (-38.2% 🟢)6.347s (-26.2% 🟢)2.023s51.08x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro0.589s (-1.8%)1.005s (~)0.416s601.00x
🐘 PostgresExpress0.603s (~)1.023s (+1.6%)0.421s591.02x
🐘 PostgresNitro0.603s (+4.1%)1.007s (~)0.404s601.02x
💻 LocalExpress0.611s (~)1.039s (+3.4%)0.429s581.04x
🐘 PostgresNext.js (Turbopack)0.811s (-5.0% 🟢)1.006s (-1.8%)0.194s601.38x
💻 LocalNext.js (Turbopack)0.892s (+3.3%)1.057s (+3.5%)0.166s571.51x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express5.921s (-41.6% 🟢)7.609s (-35.4% 🟢)1.688s81.00x
▲ VercelNitro6.497s (+31.0% 🔺)8.122s (+24.7% 🔺)1.625s81.10x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.360s (-5.1% 🟢)2.008s (~)0.647s451.00x
🐘 PostgresNitro1.410s (-1.8%)2.008s (-1.1%)0.598s451.04x
💻 LocalNitro1.508s (~)2.006s (~)0.497s451.11x
💻 LocalExpress1.526s (-1.8%)2.028s (~)0.502s451.12x
🐘 PostgresNext.js (Turbopack)1.894s (-6.7% 🟢)2.053s (-22.7% 🟢)0.159s441.39x
💻 LocalNext.js (Turbopack)2.084s (-0.7%)3.008s (~)0.924s301.53x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro14.267s (+8.9% 🔺)16.133s (+11.6% 🔺)1.866s61.00x
▲ VercelExpress15.632s (+8.8% 🔺)17.362s (+7.5% 🔺)1.729s61.10x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro2.756s (-1.4%)3.059s (-3.4%)0.303s401.00x
🐘 PostgresExpress2.805s (~)3.112s (-0.8%)0.307s391.02x
💻 LocalExpress3.225s (-1.8%)3.977s (-0.8%)0.752s311.17x
💻 LocalNitro3.248s (-1.5%)4.009s (~)0.761s301.18x
🐘 PostgresNext.js (Turbopack)3.766s (-5.5% 🟢)4.043s (-6.7% 🟢)0.276s301.37x
💻 LocalNext.js (Turbopack)4.291s (-1.3%)5.010s (~)0.720s241.56x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express32.130s (+22.5% 🔺)34.248s (+21.4% 🔺)2.117s41.00x
▲ VercelNitro34.856s (+19.7% 🔺)36.875s (+19.7% 🔺)2.019s41.08x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.244s (+11.1% 🔺)1.007s (~)0.763s601.00x
🐘 PostgresExpress0.250s (+7.4% 🔺)1.006s (~)0.756s601.03x
🐘 PostgresNext.js (Turbopack)0.289s (+4.2%)1.006s (~)0.717s601.19x
💻 LocalExpress0.423s (+2.4%)1.004s (~)0.582s601.74x
💻 LocalNitro0.431s (~)1.004s (-1.7%)0.573s601.77x
💻 LocalNext.js (Turbopack)0.569s (+4.1%)1.005s (~)0.436s602.33x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.874s (-27.0% 🟢)3.565s (-13.2% 🟢)1.692s171.00x
▲ VercelNitro2.088s (-26.0% 🟢)3.684s (-14.7% 🟢)1.596s171.11x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.393s (+9.5% 🔺)1.053s (+4.7%)0.661s861.00x
🐘 PostgresExpress0.411s (+9.7% 🔺)1.041s (+3.5%)0.630s871.05x
🐘 PostgresNext.js (Turbopack)0.583s (+16.8% 🔺)1.091s (+8.4% 🔺)0.508s831.48x
💻 LocalNitro2.128s (-2.5%)2.737s (+0.8%)0.609s335.42x
💻 LocalExpress2.226s (+9.2% 🔺)2.766s (+9.1% 🔺)0.540s335.67x
💻 LocalNext.js (Turbopack)2.521s (+2.9%)3.218s (+2.2%)0.697s296.42x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.431s (-62.9% 🟢)4.945s (-54.5% 🟢)1.514s191.00x
▲ VercelExpress21.388s (+295.0% 🔺)23.082s (+226.3% 🔺)1.694s176.23x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.795s (+16.7% 🔺)1.567s (+55.7% 🔺)0.772s771.00x
🐘 PostgresExpress0.805s (+15.6% 🔺)1.419s (+41.0% 🔺)0.614s851.01x
🐘 PostgresNext.js (Turbopack)2.760s (+174.5% 🔺)3.681s (+98.5% 🔺)0.921s333.47x
💻 LocalExpress9.388s (+0.8%)10.195s (+3.3%)0.807s1211.80x
💻 LocalNitro9.416s (+1.5%)9.874s (+0.8%)0.458s1311.84x
💻 LocalNext.js (Turbopack)10.703s (-2.2%)11.576s (+0.8%)0.873s1113.46x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express6.877s (-63.6% 🟢)8.574s (-58.2% 🟢)1.697s141.00x
▲ VercelNitro8.266s (-57.1% 🟢)10.222s (-53.8% 🟢)1.957s121.20x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express1.160s (-0.7%)2.005s (~)0.010s (-14.5% 🟢)2.017s (~)0.857s101.00x
🐘 PostgresNitro1.165s (-0.7%)2.001s (~)0.001s (-15.4% 🟢)2.010s (~)0.846s101.00x
💻 LocalNitro1.167s (~)2.005s (~)0.010s (~)2.018s (~)0.851s101.01x
🐘 PostgresExpress1.172s (-0.6%)2.000s (~)0.001s (-8.3% 🟢)2.010s (~)0.838s101.01x
💻 LocalNext.js (Turbopack)1.211s (~)2.004s (~)0.013s (+0.8%)2.020s (~)0.809s101.04x
🐘 PostgresNext.js (Turbopack)1.234s (-0.9%)2.002s (~)0.001s (+7.7% 🔺)2.011s (~)0.777s101.06x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.797s (+14.3% 🔺)4.260s (+26.7% 🔺)0.638s (-32.8% 🟢)5.340s (+12.0% 🔺)2.543s101.00x
▲ VercelExpress32.861s (+1415.8% 🔺)3.322s (+3.7%)1.152s (+26.8% 🔺)35.409s (+673.0% 🔺)2.548s1011.75x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro | Express

stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.566s (-1.8%)2.009s (~)0.012s (-1.6%)2.023s (~)0.457s301.00x
💻 LocalExpress1.568s (~)2.010s (~)0.013s (+14.2% 🔺)2.026s (~)0.457s301.00x
🐘 PostgresExpress1.579s (-1.4%)2.006s (~)0.005s (+7.1% 🔺)2.027s (~)0.448s301.01x
🐘 PostgresNitro1.616s (~)2.006s (~)0.005s (-8.7% 🟢)2.025s (~)0.408s301.03x
💻 LocalNext.js (Turbopack)1.730s (-0.6%)2.010s (~)0.012s (-1.9%)2.025s (~)0.295s301.10x
🐘 PostgresNext.js (Turbopack)1.755s (-1.7%)2.010s (~)0.005s (+2.7%)2.026s (~)0.270s301.12x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express5.998s (-1.2%)7.151s (-2.0%)0.235s (-10.5% 🟢)7.964s (~)1.966s81.00x
▲ VercelNitro6.812s (+15.1% 🔺)7.759s (+10.4% 🔺)0.245s (-47.7% 🟢)8.558s (+7.1% 🔺)1.745s81.14x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Express | Nitro

10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.798s (+12.6% 🔺)1.068s (+2.4%)0.000s (-66.1% 🟢)1.088s (+2.3%)0.291s561.00x
🐘 PostgresExpress0.805s (+13.8% 🔺)1.084s (+6.9% 🔺)0.000s (+221.8% 🔺)1.099s (+6.8% 🔺)0.293s551.01x
🐘 PostgresNext.js (Turbopack)0.998s (+15.9% 🔺)1.463s (+34.1% 🔺)0.000s (-100.0% 🟢)1.471s (+33.9% 🔺)0.473s411.25x
💻 LocalNitro1.386s (-3.8%)2.013s (~)0.001s (+260.0% 🔺)2.015s (~)0.630s301.74x
💻 LocalExpress1.391s (+3.1%)2.013s (~)0.000s (+175.0% 🔺)2.015s (~)0.624s301.74x
💻 LocalNext.js (Turbopack)1.462s (~)2.013s (~)0.000s (-40.0% 🟢)2.016s (~)0.554s301.83x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.320s (-1.2%)4.629s (+1.2%)0.000s (-100.0% 🟢)5.052s (~)1.731s121.00x
▲ VercelNitro3.595s (-5.5% 🟢)4.764s (-1.3%)0.001s (+Infinity% 🔺)5.183s (-0.7%)1.588s121.08x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Express | Nitro

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

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.508s (+13.1% 🔺)2.032s (~)0.000s (NaN%)2.061s (~)0.553s301.00x
🐘 PostgresExpress1.669s (+9.4% 🔺)2.217s (~)0.000s (-50.0% 🟢)2.252s (~)0.583s271.11x
🐘 PostgresNext.js (Turbopack)2.069s (+19.6% 🔺)2.404s (+6.3% 🔺)0.000s (+Infinity% 🔺)2.432s (+7.1% 🔺)0.363s251.37x
💻 LocalNext.js (Turbopack)2.812s (-1.4%)3.357s (-1.7%)0.001s (+100.0% 🔺)3.361s (-1.7%)0.549s181.86x
💻 LocalNitro2.994s (-5.5% 🟢)3.613s (-5.8% 🟢)0.000s (+119.6% 🔺)3.620s (-5.8% 🟢)0.626s171.98x
💻 LocalExpress3.129s (+1.1%)3.902s (+6.3% 🔺)0.001s (+44.9% 🔺)3.905s (+6.2% 🔺)0.776s162.07x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro5.070s (-31.3% 🟢)6.077s (-23.4% 🟢)0.000s (+Infinity% 🔺)6.502s (-28.0% 🟢)1.433s101.00x
▲ VercelExpress5.299s (~)6.264s (-4.8%)0.000s (NaN%)6.717s (-6.6% 🟢)1.418s91.05x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro | Express

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNitro10/21
🐘 PostgresNitro14/21
▲ VercelExpress13/21
Fastest World by Framework

Winner determined by most benchmark wins

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

Worlds:

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

📋 View full workflow run


Some benchmark jobs failed:

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

Check the workflow run for details.

Comment threaddocs/content/docs/v4/api-reference/workflow/create-hook.mdx Outdated

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

Adds a new hook.ready promise to let workflow code wait for hook registration (hook_created) independently of receiving payloads, enabling deterministic token claiming for idempotency patterns (and consistent conflict handling via HookConflictError).

Changes:

  • Extend the public Hook interface with ready: Promise<void> and implement it in the workflow VM hook runtime.
  • Resolve/reject ready waiters based on hook_created / hook_conflict, including suspension behavior when awaiting readiness before registration is recorded.
  • Add tests and update v4/v5 documentation to describe hook.ready (and webhook readiness via the shared Hook interface).

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 1 comment.

Show a summary per file
FileDescription
packages/core/src/workflow/hook.tsImplements hook.ready tracking/queuing and resolves/rejects it on hook_created/hook_conflict, including suspension when needed.
packages/core/src/workflow/hook.test.tsAdds unit tests covering ready resolution, suspension behavior, conflict rejection, and ensuring payload consumption is unchanged.
packages/core/src/workflow.test.tsAdds integration tests verifying suspension, resolution, and conflict behavior when hook.ready is used inside runWorkflow.
packages/core/src/create-hook.tsExtends the public Hook interface with documented ready: Promise<void>.
docs/content/docs/v5/how-it-works/event-sourcing.mdxDocuments that hook.ready rejects on hook conflicts in addition to payload awaiting.
docs/content/docs/v5/foundations/hooks.mdxAdds a section explaining when/how to use hook.ready to wait for registration.
docs/content/docs/v5/api-reference/workflow/create-webhook.mdxNotes ready availability on returned webhook objects and how to use it before sharing the URL.
docs/content/docs/v5/api-reference/workflow/create-hook.mdxDocuments hook.ready and adds a registration-focused example.
docs/content/docs/v4/how-it-works/event-sourcing.mdxMirrors v5 event-sourcing updates for hook.ready conflict semantics.
docs/content/docs/v4/foundations/hooks.mdxMirrors v5 “Waiting for Hook Registration” guidance.
docs/content/docs/v4/api-reference/workflow/create-webhook.mdxMirrors v5 webhook ready documentation note.
docs/content/docs/v4/api-reference/workflow/create-hook.mdxMirrors v5 hook.ready API docs and example.
.changeset/hook-ready.mdDeclares minor bumps for @workflow/core and workflow to ship the new API.

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

Comment threadpackages/core/src/workflow/hook.test.ts Outdated
Comment threaddocs/content/docs/v4/api-reference/workflow/create-webhook.mdx Outdated
Comment threaddocs/content/docs/v5/api-reference/workflow/create-webhook.mdx Outdated

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

Copilot reviewed 20 out of 20 changed files in this pull request and generated 1 comment.

Comment threadpackages/core/src/workflow/hook.ts Outdated
Comment threadpackages/core/src/runtime.ts Outdated
pranaygpand others added 3 commits May 18, 2026 21:44
…hook-ready
* origin-https/main: (119 commits)
fix: settle aborted parallel steps before completing abortParallelWorkflow (#2244)
Add native v4 workflow attribute events (#2226)
Version Packages (beta) (#2326)
[ci] Fix flaky windows unit tests (#2359)
Capture Vercel runtime logs when e2e Vercel Prod lanes fail (#2356)
Fix e2e failure reporting under vitest 4 and preserve fetch error causes (#2355)
[core] Fix process crash from rejected waitUntil promises (#2336)
[core] Remove duplicate `waitUntil` for suspension handler async operations (#2345)
Prevent local tests from hanging (#2338)
feat(core): add optional namespace for queue topic prefix (#2305)
[web-shared] Show precise durations in the new trace viewer (#2335)
Validate unique workflow step IDs at build time (#2018)
Move run attributes into their own detail card (#2327)
[core] Forward-port stream reconnect to getReadable level (#2318)
[docs] Add "Step executed multiple times" error page (#2310)
Fix flickering on the detail panel when navigating the trace viewer (#2325)
Version Packages (beta) (#2254)
Patch vulnerable package dependencies (#2301)
Deprecate DurableAgent and update it with WorkflowAgent in v5 docs (#2285)
[codex] Fix partial world-local exclusive writes (#2296)
...
# Conflicts:
#	packages/core/src/runtime.ts
#	packages/core/src/runtime/suspension-handler.ts
- hook.hasConflict resolves true when the token is owned by another
active hook, false once registration is committed — no throw, so
workflows can branch on conflicts early. Awaiting it suspends the
workflow to commit the hook registration (createHook alone does not).
- Chain the already-created fast-path through promiseQueue so
resolution order matches event-log order (review feedback).
- Skip inline step execution when a suspension has an awaited hook
creation so the hasConflict continuation can advance independently
of step execution (review feedback).
- Update unit tests, e2e tests, workbench workflows, and v4/v5 docs.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

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

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

Comment threaddocs/content/docs/v5/api-reference/workflow/create-webhook.mdx Outdated
Comment threaddocs/content/docs/v4/api-reference/workflow/create-webhook.mdx Outdated

@TooTallNateTooTallNate left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Approve — determinism-safe design, strong test coverage; one docs-sequencing flag

I built @workflow/core from this branch and ran the full unit suite locally (1146 tests, matching the PR's claimed validation) plus the three new test files individually (116 tests). All green.

The semantics hold up under determinism scrutiny

This touches the workflow VM's most replay-sensitive machinery, so I traced the settlement paths carefully:

  • Every hasConflict settlement goes through ctx.promiseQueue — both fast-paths (hasCreated / conflict-already-known) and the event-driven paths (hook_created → resolve false, hook_conflict → resolve true). Resolution order always matches event-log order, which is the invariant that keeps replay deterministic.
  • The hook_conflict consumer drains payload and hasConflict waiters synchronously (deferring actual settlement through promiseQueue), so the null-event handler can't see them and raise a spurious suspension — same pattern the existing payload path uses.
  • The suspension trigger condition (hasConflictPromises.length > 0 && !hasCreated && !hasConflict in the null-event handler, plus the eventLogEmpty fast-path in createHasConflictPromise) correctly mirrors the payload-awaiter pattern for both orderings of "awaited before/after log exhaustion." Both are unit-tested.
  • hasAwaitedHookCreation only affects scheduling, never event-log content — it decides which invocation executes steps (queue-all + timeoutSeconds: 0 re-invoke instead of inline), so there's no replay-divergence surface. The reasoning in the runtime.ts comment is exactly right: running a step inline would serialize work the workflow expressed as parallel (hook.hasConflict.then(() => stepB()) racing await stepA()).
  • The EntityConflictError path returning hasAwaitedHookCreation: true is correct and subtle: on a crash-replay where the hook event already exists, the awaiter still needs an immediate re-invocation to consume hook_created and resolve — silently swallowing it would hang the awaiter until something else woke the run.
  • The e2e parallelism proof (hookHasConflictThenStepParallelWorkflow: 100ms stepB triggered via the hasConflict continuation must complete while 10s stepA is still running, asserted via step timestamps) tests the actual claim end-to-end rather than just the mechanism.

My earlier docs comments were both addressed (the create-hook example now explicitly points to awaiting the hook/iterating for payload data).

One sequencing flag: the v4 docs (should-fix before merge, or fast-follow the backport)

hasConflict does not exist on stable — I checked origin/stable:packages/core/src/create-hook.ts and there are zero references. But this PR adds the API to the v4 docs (docs/content/docs/v4/...), and docs deploy from main. The moment this merges, the live v4 documentation advertises a property that no released 4.x SDK has — and it'll stay wrong until a backport merges to stableand ships in a 4.x release.

Two clean options:

  1. Drop the v4 docs pages from this PR and let the backport PR carry them (the backport flow cherry-picks docs/content/ normally, so they'd land together with the code on stable — though note they'd then need a separate main-side commit since v4 docs render from main), or
  2. Keep them but treat the stable backport + 4.x release as an immediate follow-up so the docs-reality gap is measured in days.

Given the changeset is minor (correct for a new API) and this is a self-contained feature — squarely in the auto-backport criteria — option 2 is probably fine, but it should be a deliberate choice, not an accident.

Non-blocking notes

  • get hasConflict() returns a fresh promise per property access (hook.hasConflict !== hook.hasConflict). Each settles deterministically through promiseQueue, so it's correctness-neutral, but identity-unstable promises from a property (vs a method) can surprise — e.g. registering two waiters from what reads like one. Caching the promise per hook after first access would be friendlier and would also make the hasConflictAwaiter flag-set happen exactly once. Fine as-is.
  • The local hasConflict boolean inside createCreateHook now shadows the public property name; a rename (sawConflictEvent?) would help the next reader.

CI

The two red E2E jobs are baseline flakes unrelated to this PR: astro fails AbortController > abortFromStepWorkflow (the abort area #2244 just touched on main) and hono fails resilient start: addTenWorkflow ... run_created returns 500 (known flake). No hook test failed anywhere. The required check needs a re-run to go green before merge.

Nice work — this is a well-reasoned addition in a part of the codebase where it's very easy to be subtly wrong.

TooTallNateand others added 3 commits June 11, 2026 14:29
State both resolution values explicitly (true = token already owned,
false = registered) instead of a parenthetical that only described the
false case.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment threadAGENTS.md
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
expect.anything()
);
expect(result.hasAwaitedHookCreation).toBe(true);
expect(result.timeoutSeconds).toBeUndefined();

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.

Test assertions check non-existent result.timeoutSeconds property, making toBeUndefined() assertions pass trivially without verifying actual behavior.

Fix on Vercel

@github-actions

Copy link
Copy Markdown
Contributor

No backport to stable for e163422 (AI decision).

This is a new minor feature adding the hook.hasConflict API to the public hook interface, intended for the next major release on main. It's a self-contained feature addition (not a bug fix) and the changeset is marked as a minor bump for the beta channel. Per the criteria, new feature additions targeted at the next major release should generally stay on main rather than be backported to stable.

To override, re-run the Backport to stable workflow manually via workflow_dispatch and paste this commit SHA into the ref input:

e163422551e80eda88a15fec3d3d574a00c53e93

@github-actionsgithub-actionsBot mentioned this pull request Jun 11, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Backport PR opened against stable: #2372. Merge conflicts were resolved by AI — please review carefully. (backport job run)

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.

3 participants

@pranaygp@TooTallNate
, '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 `hook.hasConflict` for early hook conflict detection by pranaygp · Pull Request #2015 · vercel/workflow · GitHub
Skip to content

Add hook.hasConflict for early hook conflict detection - #2015

Merged
TooTallNate merged 9 commits into
mainfrom
pranaygp/codex/hook-ready
Jun 11, 2026
Merged

Add hook.hasConflict for early hook conflict detection#2015
TooTallNate merged 9 commits into
mainfrom
pranaygp/codex/hook-ready

Conversation

@pranaygp

@pranaygppranaygp commented May 18, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds hook.hasConflict, a lazy Promise<boolean> getter that lets workflow code commit hook registration and check for token conflicts without waiting for hook payload data.

Calling createHook() alone does not register the hook — registration is only committed when the workflow suspends. Awaiting hook.hasConflict suspends the workflow to commit the registration, then resolves with false once hook_created is recorded, or true if another active hook already owns the token. Unlike awaiting the hook payload (which rejects with HookConflictError on conflict), hasConflict never throws, so workflows can branch on conflicts early:

using hook=createHook({token: `order:${orderId}`});if(awaithook.hasConflict){return;// another run already owns this token}

This gives workflows a way to claim a deterministic hook token for idempotency before running duplicate-sensitive work, and is a prerequisite for #2011, where the idempotency docs/examples should use await hook.hasConflict instead of requiring consumers to wait for or resume hook data just to claim the token.

Changes

  • Add Hook.hasConflict: Promise<boolean> to the public hook interface.
  • Resolve pending hasConflict waiters with false on hook_created and true on hook_conflict; both fast-paths settle through promiseQueue so resolution order matches event-log order.
  • Suspend workflow execution when hook.hasConflict is awaited before hook creation is recorded.
  • Skip inline step execution when a suspension contains an awaited hook creation, so a hasConflict continuation (e.g. hook.hasConflict.then(() => stepB())) can advance in parallel with other steps instead of only after they finish.
  • Document hook.hasConflict in the v4 and v5 hook/webhook docs and event-sourcing pages, including the suspension-commits-registration semantics.
  • Add core unit tests (resolution, suspension, conflict, payload preservation, suspension-handler scheduling) and e2e tests (registration without payload, non-blocking steps, parallel continuation scheduling, conflict resolves true).

Validation

  • cd packages/core && pnpm typecheck && pnpm vitest run src/ (1146 tests)
  • Hook e2e suite against local nextjs-turbopack dev server: pnpm vitest run packages/core/e2e/e2e.test.ts -t 'hook' (20 tests, including the new parallel-continuation test)

Docs Preview

Changed pages on the docs preview deployment:

Pagev4v5
createHook() API referencev4v5
createWebhook() API referencev4v5
Foundations: Hooksv4v5
How It Works: Event Sourcingv4v5

CopilotAI review requested due to automatic review settings May 18, 2026 23:22
@pranaygp
pranaygp requested a review from a team as a code ownerMay 18, 2026 23:22
@changeset-bot

changeset-botBot commented May 18, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 2342dae

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

This PR includes changesets to release 17 packages
NameType
@workflow/coreMinor
workflowMinor
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/webPatch
@workflow/world-testingPatch
@workflow/aiMajor
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch
@workflow/nuxtPatch

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 May 18, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented May 18, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production137602191595
✅ 💻 Local Development181102192030
✅ 📦 Local Production181102192030
✅ 🐘 Local Postgres179702332030
✅ 🪟 Windows14500145
✅ 📋 Other83701781015
Total7777010688845

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro119026
✅ example119026
✅ express119026
✅ fastify119026
✅ hono119026
✅ nextjs-turbopack14302
✅ nextjs-webpack14302
✅ nitro119026
✅ nuxt119026
✅ sveltekit13807
✅ vite119026
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable120025
✅ express-stable120025
✅ fastify-stable120025
✅ hono-stable120025
✅ nextjs-turbopack-canary126019
✅ nextjs-turbopack-stable-lazy-discovery-disabled14500
✅ nextjs-turbopack-stable-lazy-discovery-enabled14500
✅ nextjs-webpack-canary126019
✅ nextjs-webpack-stable-lazy-discovery-disabled14500
✅ nextjs-webpack-stable-lazy-discovery-enabled14500
✅ nitro-stable120025
✅ nuxt-stable120025
✅ sveltekit-stable13906
✅ vite-stable120025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable120025
✅ express-stable120025
✅ fastify-stable120025
✅ hono-stable120025
✅ nextjs-turbopack-canary126019
✅ nextjs-turbopack-stable-lazy-discovery-disabled14500
✅ nextjs-turbopack-stable-lazy-discovery-enabled14500
✅ nextjs-webpack-canary126019
✅ nextjs-webpack-stable-lazy-discovery-disabled14500
✅ nextjs-webpack-stable-lazy-discovery-enabled14500
✅ nitro-stable120025
✅ nuxt-stable120025
✅ sveltekit-stable13906
✅ vite-stable120025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable119026
✅ express-stable119026
✅ fastify-stable119026
✅ hono-stable119026
✅ nextjs-turbopack-canary125020
✅ nextjs-turbopack-stable-lazy-discovery-disabled14401
✅ nextjs-turbopack-stable-lazy-discovery-enabled14401
✅ nextjs-webpack-canary125020
✅ nextjs-webpack-stable-lazy-discovery-disabled14401
✅ nextjs-webpack-stable-lazy-discovery-enabled14401
✅ nitro-stable119026
✅ nuxt-stable119026
✅ sveltekit-stable13807
✅ vite-stable119026
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack14500
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable120025
✅ e2e-local-dev-tanstack-start-120025
✅ e2e-local-postgres-nest-stable119026
✅ e2e-local-postgres-tanstack-start-119026
✅ e2e-local-prod-nest-stable120025
✅ e2e-local-prod-tanstack-start-120025
✅ e2e-vercel-prod-tanstack-start119026

📋 View full workflow run

@github-actions

github-actionsBot commented May 18, 2026

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

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

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro0.040s (-5.4% 🟢)1.005s (~)0.965s101.00x
💻 LocalExpress0.041s (-2.8%)1.005s (~)0.964s101.03x
💻 LocalNext.js (Turbopack)0.060s (-4.0%)1.005s (~)0.945s101.51x
🐘 PostgresNitro0.062s (-5.2% 🟢)1.012s (~)0.950s101.56x
🐘 PostgresExpress0.067s (+9.0% 🔺)1.014s (~)0.947s101.67x
🐘 PostgresNext.js (Turbopack)0.068s (-8.6% 🟢)1.013s (~)0.945s101.71x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express0.414s (+52.9% 🔺)2.276s (-11.8% 🟢)1.863s101.00x
▲ VercelNitro0.420s (+11.1% 🔺)2.271s (+8.1% 🔺)1.851s101.01x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.093s (~)2.006s (~)0.913s101.00x
💻 LocalExpress1.094s (~)2.006s (~)0.912s101.00x
🐘 PostgresNitro1.105s (-0.7%)2.010s (~)0.906s101.01x
🐘 PostgresExpress1.114s (~)2.009s (~)0.895s101.02x
💻 LocalNext.js (Turbopack)1.136s (+0.7%)2.007s (~)0.871s101.04x
🐘 PostgresNext.js (Turbopack)1.150s (+0.6%)2.009s (~)0.859s101.05x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.681s (-4.1%)3.608s (-2.7%)1.927s101.00x
▲ VercelNitro32.051s (+1812.5% 🔺)34.165s (+890.4% 🔺)2.114s1019.06x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express10.500s (~)11.022s (~)0.523s31.00x
🐘 PostgresExpress10.570s (~)11.020s (~)0.451s31.01x
🐘 PostgresNitro10.578s (~)11.023s (~)0.444s31.01x
💻 LocalNitro10.651s (+1.0%)11.022s (~)0.372s31.01x
💻 LocalNext.js (Turbopack)10.792s (~)11.021s (~)0.229s31.03x
🐘 PostgresNext.js (Turbopack)10.937s (-0.7%)11.351s (-2.9%)0.415s31.04x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro13.882s (+4.1%)16.196s (+7.0% 🔺)2.314s21.00x
▲ VercelExpress14.281s (+6.5% 🔺)16.108s (+2.4%)1.827s21.03x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express13.701s (-1.0%)14.026s (~)0.325s51.00x
💻 LocalNitro13.738s (-0.7%)14.027s (-1.4%)0.289s51.00x
🐘 PostgresExpress13.763s (-0.5%)14.022s (~)0.259s51.00x
🐘 PostgresNitro13.921s (+0.7%)14.023s (~)0.102s51.02x
💻 LocalNext.js (Turbopack)14.364s (-0.9%)15.030s (~)0.665s41.05x
🐘 PostgresNext.js (Turbopack)14.456s (~)15.014s (~)0.558s41.06x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro21.579s (-3.8%)23.291s (-3.3%)1.712s31.00x
▲ VercelExpress24.509s (+14.1% 🔺)26.394s (+14.4% 🔺)1.885s31.14x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro12.331s (-1.1%)13.026s (~)0.695s71.00x
💻 LocalExpress12.446s (~)13.025s (~)0.579s71.01x
🐘 PostgresExpress12.526s (-1.7%)13.020s (~)0.494s71.02x
🐘 PostgresNitro12.719s (+1.1%)13.025s (~)0.307s71.03x
💻 LocalNext.js (Turbopack)13.813s (+1.0%)14.169s (+1.0%)0.356s71.12x
🐘 PostgresNext.js (Turbopack)13.852s (-0.9%)14.161s (-1.0%)0.309s71.12x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro29.649s (-6.5% 🟢)31.500s (-5.4% 🟢)1.851s31.00x
▲ VercelExpress31.179s (+5.1% 🔺)33.707s (+8.2% 🔺)2.528s31.05x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express1.208s (+1.5%)2.006s (~)0.798s151.00x
🐘 PostgresNitro1.208s (~)2.009s (~)0.800s151.00x
🐘 PostgresExpress1.231s (+2.4%)2.008s (~)0.777s151.02x
💻 LocalNitro1.279s (+4.6%)2.006s (~)0.726s151.06x
🐘 PostgresNext.js (Turbopack)1.339s (+6.4% 🔺)2.074s (+3.3%)0.734s151.11x
💻 LocalNext.js (Turbopack)1.368s (+1.4%)2.006s (~)0.638s151.13x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.970s (+16.4% 🔺)4.450s (-1.1%)1.480s71.00x
▲ VercelNitro3.506s (-5.2% 🟢)5.306s (+2.4%)1.799s61.18x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.366s (+8.5% 🔺)2.394s (+19.3% 🔺)1.028s131.00x
🐘 PostgresExpress1.389s (+10.0% 🔺)2.508s (+25.0% 🔺)1.119s121.02x
🐘 PostgresNext.js (Turbopack)1.618s (+15.5% 🔺)2.393s (+19.3% 🔺)0.776s131.18x
💻 LocalNitro1.704s (~)2.006s (~)0.302s151.25x
💻 LocalExpress1.729s (-6.1% 🟢)2.006s (-6.7% 🟢)0.277s151.27x
💻 LocalNext.js (Turbopack)1.842s (-2.0%)2.074s (-3.5%)0.232s151.35x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.196s (-27.7% 🟢)4.831s (-21.0% 🟢)1.635s71.00x
▲ VercelNitro3.207s (-48.6% 🟢)4.657s (-39.9% 🟢)1.450s71.00x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.680s (+19.4% 🔺)4.141s (+106.2% 🔺)2.461s81.00x
🐘 PostgresExpress1.718s (+22.5% 🔺)3.889s (+93.6% 🔺)2.172s81.02x
🐘 PostgresNext.js (Turbopack)3.508s (+94.5% 🔺)4.297s (+87.1% 🔺)0.789s72.09x
💻 LocalNitro4.696s (-10.5% 🟢)5.347s (-11.1% 🟢)0.650s62.80x
💻 LocalExpress5.272s (-2.1%)6.013s (-2.7%)0.742s53.14x
💻 LocalNext.js (Turbopack)5.395s (-5.4% 🟢)6.014s (-3.2%)0.619s53.21x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.396s (-51.8% 🟢)6.218s (-45.2% 🟢)1.823s61.00x
▲ VercelNitro4.696s (-72.4% 🟢)6.708s (-63.3% 🟢)2.012s51.07x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.221s (+0.8%)2.074s (+3.3%)0.853s151.00x
🐘 PostgresNitro1.224s (+2.8%)2.008s (~)0.784s151.00x
🐘 PostgresNext.js (Turbopack)1.278s (+1.2%)2.008s (~)0.729s151.05x
💻 LocalNext.js (Turbopack)1.391s (+2.9%)2.006s (~)0.615s151.14x
💻 LocalNitro1.502s (~)2.006s (~)0.505s151.23x
💻 LocalExpress1.539s (-7.2% 🟢)2.006s (-3.3%)0.467s151.26x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.632s (+2.1%)4.218s (~)1.586s81.00x
▲ VercelExpress2.986s (+10.6% 🔺)4.712s (~)1.726s71.13x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.394s (+9.6% 🔺)2.224s (+10.8% 🔺)0.830s141.00x
🐘 PostgresExpress1.424s (+9.8% 🔺)2.317s (+15.3% 🔺)0.892s131.02x
🐘 PostgresNext.js (Turbopack)1.506s (+7.3% 🔺)2.150s (+7.1% 🔺)0.644s141.08x
💻 LocalExpress1.865s (-13.6% 🟢)2.295s (-14.2% 🟢)0.429s141.34x
💻 LocalNext.js (Turbopack)2.055s (~)2.591s (-13.9% 🟢)0.536s121.47x
💻 LocalNitro2.153s (-0.8%)2.676s (+3.2%)0.523s121.55x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.328s (-2.9%)5.132s (+0.6%)1.803s61.00x
▲ VercelNitro3.433s (-28.9% 🟢)5.018s (-21.6% 🟢)1.585s61.03x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.617s (+16.5% 🔺)4.299s (+114.1% 🔺)2.682s71.00x
🐘 PostgresExpress1.837s (+31.1% 🔺)3.891s (+93.5% 🔺)2.053s81.14x
🐘 PostgresNext.js (Turbopack)3.367s (+86.2% 🔺)4.582s (+106.0% 🔺)1.215s72.08x
💻 LocalExpress4.294s (-32.4% 🟢)5.016s (-28.5% 🟢)0.721s62.66x
💻 LocalNitro5.167s (-5.0%)5.851s (-2.7%)0.685s63.19x
💻 LocalNext.js (Turbopack)5.712s (-5.7% 🟢)6.017s (-11.7% 🟢)0.305s53.53x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.990s (-21.8% 🟢)5.843s (-19.3% 🟢)1.853s61.00x
▲ VercelNitro4.324s (-38.2% 🟢)6.347s (-26.2% 🟢)2.023s51.08x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro0.589s (-1.8%)1.005s (~)0.416s601.00x
🐘 PostgresExpress0.603s (~)1.023s (+1.6%)0.421s591.02x
🐘 PostgresNitro0.603s (+4.1%)1.007s (~)0.404s601.02x
💻 LocalExpress0.611s (~)1.039s (+3.4%)0.429s581.04x
🐘 PostgresNext.js (Turbopack)0.811s (-5.0% 🟢)1.006s (-1.8%)0.194s601.38x
💻 LocalNext.js (Turbopack)0.892s (+3.3%)1.057s (+3.5%)0.166s571.51x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express5.921s (-41.6% 🟢)7.609s (-35.4% 🟢)1.688s81.00x
▲ VercelNitro6.497s (+31.0% 🔺)8.122s (+24.7% 🔺)1.625s81.10x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.360s (-5.1% 🟢)2.008s (~)0.647s451.00x
🐘 PostgresNitro1.410s (-1.8%)2.008s (-1.1%)0.598s451.04x
💻 LocalNitro1.508s (~)2.006s (~)0.497s451.11x
💻 LocalExpress1.526s (-1.8%)2.028s (~)0.502s451.12x
🐘 PostgresNext.js (Turbopack)1.894s (-6.7% 🟢)2.053s (-22.7% 🟢)0.159s441.39x
💻 LocalNext.js (Turbopack)2.084s (-0.7%)3.008s (~)0.924s301.53x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro14.267s (+8.9% 🔺)16.133s (+11.6% 🔺)1.866s61.00x
▲ VercelExpress15.632s (+8.8% 🔺)17.362s (+7.5% 🔺)1.729s61.10x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro2.756s (-1.4%)3.059s (-3.4%)0.303s401.00x
🐘 PostgresExpress2.805s (~)3.112s (-0.8%)0.307s391.02x
💻 LocalExpress3.225s (-1.8%)3.977s (-0.8%)0.752s311.17x
💻 LocalNitro3.248s (-1.5%)4.009s (~)0.761s301.18x
🐘 PostgresNext.js (Turbopack)3.766s (-5.5% 🟢)4.043s (-6.7% 🟢)0.276s301.37x
💻 LocalNext.js (Turbopack)4.291s (-1.3%)5.010s (~)0.720s241.56x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express32.130s (+22.5% 🔺)34.248s (+21.4% 🔺)2.117s41.00x
▲ VercelNitro34.856s (+19.7% 🔺)36.875s (+19.7% 🔺)2.019s41.08x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.244s (+11.1% 🔺)1.007s (~)0.763s601.00x
🐘 PostgresExpress0.250s (+7.4% 🔺)1.006s (~)0.756s601.03x
🐘 PostgresNext.js (Turbopack)0.289s (+4.2%)1.006s (~)0.717s601.19x
💻 LocalExpress0.423s (+2.4%)1.004s (~)0.582s601.74x
💻 LocalNitro0.431s (~)1.004s (-1.7%)0.573s601.77x
💻 LocalNext.js (Turbopack)0.569s (+4.1%)1.005s (~)0.436s602.33x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.874s (-27.0% 🟢)3.565s (-13.2% 🟢)1.692s171.00x
▲ VercelNitro2.088s (-26.0% 🟢)3.684s (-14.7% 🟢)1.596s171.11x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.393s (+9.5% 🔺)1.053s (+4.7%)0.661s861.00x
🐘 PostgresExpress0.411s (+9.7% 🔺)1.041s (+3.5%)0.630s871.05x
🐘 PostgresNext.js (Turbopack)0.583s (+16.8% 🔺)1.091s (+8.4% 🔺)0.508s831.48x
💻 LocalNitro2.128s (-2.5%)2.737s (+0.8%)0.609s335.42x
💻 LocalExpress2.226s (+9.2% 🔺)2.766s (+9.1% 🔺)0.540s335.67x
💻 LocalNext.js (Turbopack)2.521s (+2.9%)3.218s (+2.2%)0.697s296.42x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.431s (-62.9% 🟢)4.945s (-54.5% 🟢)1.514s191.00x
▲ VercelExpress21.388s (+295.0% 🔺)23.082s (+226.3% 🔺)1.694s176.23x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.795s (+16.7% 🔺)1.567s (+55.7% 🔺)0.772s771.00x
🐘 PostgresExpress0.805s (+15.6% 🔺)1.419s (+41.0% 🔺)0.614s851.01x
🐘 PostgresNext.js (Turbopack)2.760s (+174.5% 🔺)3.681s (+98.5% 🔺)0.921s333.47x
💻 LocalExpress9.388s (+0.8%)10.195s (+3.3%)0.807s1211.80x
💻 LocalNitro9.416s (+1.5%)9.874s (+0.8%)0.458s1311.84x
💻 LocalNext.js (Turbopack)10.703s (-2.2%)11.576s (+0.8%)0.873s1113.46x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express6.877s (-63.6% 🟢)8.574s (-58.2% 🟢)1.697s141.00x
▲ VercelNitro8.266s (-57.1% 🟢)10.222s (-53.8% 🟢)1.957s121.20x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express1.160s (-0.7%)2.005s (~)0.010s (-14.5% 🟢)2.017s (~)0.857s101.00x
🐘 PostgresNitro1.165s (-0.7%)2.001s (~)0.001s (-15.4% 🟢)2.010s (~)0.846s101.00x
💻 LocalNitro1.167s (~)2.005s (~)0.010s (~)2.018s (~)0.851s101.01x
🐘 PostgresExpress1.172s (-0.6%)2.000s (~)0.001s (-8.3% 🟢)2.010s (~)0.838s101.01x
💻 LocalNext.js (Turbopack)1.211s (~)2.004s (~)0.013s (+0.8%)2.020s (~)0.809s101.04x
🐘 PostgresNext.js (Turbopack)1.234s (-0.9%)2.002s (~)0.001s (+7.7% 🔺)2.011s (~)0.777s101.06x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.797s (+14.3% 🔺)4.260s (+26.7% 🔺)0.638s (-32.8% 🟢)5.340s (+12.0% 🔺)2.543s101.00x
▲ VercelExpress32.861s (+1415.8% 🔺)3.322s (+3.7%)1.152s (+26.8% 🔺)35.409s (+673.0% 🔺)2.548s1011.75x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro | Express

stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.566s (-1.8%)2.009s (~)0.012s (-1.6%)2.023s (~)0.457s301.00x
💻 LocalExpress1.568s (~)2.010s (~)0.013s (+14.2% 🔺)2.026s (~)0.457s301.00x
🐘 PostgresExpress1.579s (-1.4%)2.006s (~)0.005s (+7.1% 🔺)2.027s (~)0.448s301.01x
🐘 PostgresNitro1.616s (~)2.006s (~)0.005s (-8.7% 🟢)2.025s (~)0.408s301.03x
💻 LocalNext.js (Turbopack)1.730s (-0.6%)2.010s (~)0.012s (-1.9%)2.025s (~)0.295s301.10x
🐘 PostgresNext.js (Turbopack)1.755s (-1.7%)2.010s (~)0.005s (+2.7%)2.026s (~)0.270s301.12x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express5.998s (-1.2%)7.151s (-2.0%)0.235s (-10.5% 🟢)7.964s (~)1.966s81.00x
▲ VercelNitro6.812s (+15.1% 🔺)7.759s (+10.4% 🔺)0.245s (-47.7% 🟢)8.558s (+7.1% 🔺)1.745s81.14x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Express | Nitro

10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.798s (+12.6% 🔺)1.068s (+2.4%)0.000s (-66.1% 🟢)1.088s (+2.3%)0.291s561.00x
🐘 PostgresExpress0.805s (+13.8% 🔺)1.084s (+6.9% 🔺)0.000s (+221.8% 🔺)1.099s (+6.8% 🔺)0.293s551.01x
🐘 PostgresNext.js (Turbopack)0.998s (+15.9% 🔺)1.463s (+34.1% 🔺)0.000s (-100.0% 🟢)1.471s (+33.9% 🔺)0.473s411.25x
💻 LocalNitro1.386s (-3.8%)2.013s (~)0.001s (+260.0% 🔺)2.015s (~)0.630s301.74x
💻 LocalExpress1.391s (+3.1%)2.013s (~)0.000s (+175.0% 🔺)2.015s (~)0.624s301.74x
💻 LocalNext.js (Turbopack)1.462s (~)2.013s (~)0.000s (-40.0% 🟢)2.016s (~)0.554s301.83x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.320s (-1.2%)4.629s (+1.2%)0.000s (-100.0% 🟢)5.052s (~)1.731s121.00x
▲ VercelNitro3.595s (-5.5% 🟢)4.764s (-1.3%)0.001s (+Infinity% 🔺)5.183s (-0.7%)1.588s121.08x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Express | Nitro

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

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.508s (+13.1% 🔺)2.032s (~)0.000s (NaN%)2.061s (~)0.553s301.00x
🐘 PostgresExpress1.669s (+9.4% 🔺)2.217s (~)0.000s (-50.0% 🟢)2.252s (~)0.583s271.11x
🐘 PostgresNext.js (Turbopack)2.069s (+19.6% 🔺)2.404s (+6.3% 🔺)0.000s (+Infinity% 🔺)2.432s (+7.1% 🔺)0.363s251.37x
💻 LocalNext.js (Turbopack)2.812s (-1.4%)3.357s (-1.7%)0.001s (+100.0% 🔺)3.361s (-1.7%)0.549s181.86x
💻 LocalNitro2.994s (-5.5% 🟢)3.613s (-5.8% 🟢)0.000s (+119.6% 🔺)3.620s (-5.8% 🟢)0.626s171.98x
💻 LocalExpress3.129s (+1.1%)3.902s (+6.3% 🔺)0.001s (+44.9% 🔺)3.905s (+6.2% 🔺)0.776s162.07x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro5.070s (-31.3% 🟢)6.077s (-23.4% 🟢)0.000s (+Infinity% 🔺)6.502s (-28.0% 🟢)1.433s101.00x
▲ VercelExpress5.299s (~)6.264s (-4.8%)0.000s (NaN%)6.717s (-6.6% 🟢)1.418s91.05x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro | Express

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNitro10/21
🐘 PostgresNitro14/21
▲ VercelExpress13/21
Fastest World by Framework

Winner determined by most benchmark wins

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

Worlds:

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

📋 View full workflow run


Some benchmark jobs failed:

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

Check the workflow run for details.

Comment threaddocs/content/docs/v4/api-reference/workflow/create-hook.mdx Outdated

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

Adds a new hook.ready promise to let workflow code wait for hook registration (hook_created) independently of receiving payloads, enabling deterministic token claiming for idempotency patterns (and consistent conflict handling via HookConflictError).

Changes:

  • Extend the public Hook interface with ready: Promise<void> and implement it in the workflow VM hook runtime.
  • Resolve/reject ready waiters based on hook_created / hook_conflict, including suspension behavior when awaiting readiness before registration is recorded.
  • Add tests and update v4/v5 documentation to describe hook.ready (and webhook readiness via the shared Hook interface).

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 1 comment.

Show a summary per file
FileDescription
packages/core/src/workflow/hook.tsImplements hook.ready tracking/queuing and resolves/rejects it on hook_created/hook_conflict, including suspension when needed.
packages/core/src/workflow/hook.test.tsAdds unit tests covering ready resolution, suspension behavior, conflict rejection, and ensuring payload consumption is unchanged.
packages/core/src/workflow.test.tsAdds integration tests verifying suspension, resolution, and conflict behavior when hook.ready is used inside runWorkflow.
packages/core/src/create-hook.tsExtends the public Hook interface with documented ready: Promise<void>.
docs/content/docs/v5/how-it-works/event-sourcing.mdxDocuments that hook.ready rejects on hook conflicts in addition to payload awaiting.
docs/content/docs/v5/foundations/hooks.mdxAdds a section explaining when/how to use hook.ready to wait for registration.
docs/content/docs/v5/api-reference/workflow/create-webhook.mdxNotes ready availability on returned webhook objects and how to use it before sharing the URL.
docs/content/docs/v5/api-reference/workflow/create-hook.mdxDocuments hook.ready and adds a registration-focused example.
docs/content/docs/v4/how-it-works/event-sourcing.mdxMirrors v5 event-sourcing updates for hook.ready conflict semantics.
docs/content/docs/v4/foundations/hooks.mdxMirrors v5 “Waiting for Hook Registration” guidance.
docs/content/docs/v4/api-reference/workflow/create-webhook.mdxMirrors v5 webhook ready documentation note.
docs/content/docs/v4/api-reference/workflow/create-hook.mdxMirrors v5 hook.ready API docs and example.
.changeset/hook-ready.mdDeclares minor bumps for @workflow/core and workflow to ship the new API.

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

Comment threadpackages/core/src/workflow/hook.test.ts Outdated
Comment threaddocs/content/docs/v4/api-reference/workflow/create-webhook.mdx Outdated
Comment threaddocs/content/docs/v5/api-reference/workflow/create-webhook.mdx Outdated

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

Copilot reviewed 20 out of 20 changed files in this pull request and generated 1 comment.

Comment threadpackages/core/src/workflow/hook.ts Outdated
Comment threadpackages/core/src/runtime.ts Outdated
pranaygpand others added 3 commits May 18, 2026 21:44
…hook-ready
* origin-https/main: (119 commits)
fix: settle aborted parallel steps before completing abortParallelWorkflow (#2244)
Add native v4 workflow attribute events (#2226)
Version Packages (beta) (#2326)
[ci] Fix flaky windows unit tests (#2359)
Capture Vercel runtime logs when e2e Vercel Prod lanes fail (#2356)
Fix e2e failure reporting under vitest 4 and preserve fetch error causes (#2355)
[core] Fix process crash from rejected waitUntil promises (#2336)
[core] Remove duplicate `waitUntil` for suspension handler async operations (#2345)
Prevent local tests from hanging (#2338)
feat(core): add optional namespace for queue topic prefix (#2305)
[web-shared] Show precise durations in the new trace viewer (#2335)
Validate unique workflow step IDs at build time (#2018)
Move run attributes into their own detail card (#2327)
[core] Forward-port stream reconnect to getReadable level (#2318)
[docs] Add "Step executed multiple times" error page (#2310)
Fix flickering on the detail panel when navigating the trace viewer (#2325)
Version Packages (beta) (#2254)
Patch vulnerable package dependencies (#2301)
Deprecate DurableAgent and update it with WorkflowAgent in v5 docs (#2285)
[codex] Fix partial world-local exclusive writes (#2296)
...
# Conflicts:
#	packages/core/src/runtime.ts
#	packages/core/src/runtime/suspension-handler.ts
- hook.hasConflict resolves true when the token is owned by another
active hook, false once registration is committed — no throw, so
workflows can branch on conflicts early. Awaiting it suspends the
workflow to commit the hook registration (createHook alone does not).
- Chain the already-created fast-path through promiseQueue so
resolution order matches event-log order (review feedback).
- Skip inline step execution when a suspension has an awaited hook
creation so the hasConflict continuation can advance independently
of step execution (review feedback).
- Update unit tests, e2e tests, workbench workflows, and v4/v5 docs.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

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

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

Comment threaddocs/content/docs/v5/api-reference/workflow/create-webhook.mdx Outdated
Comment threaddocs/content/docs/v4/api-reference/workflow/create-webhook.mdx Outdated

@TooTallNateTooTallNate left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Approve — determinism-safe design, strong test coverage; one docs-sequencing flag

I built @workflow/core from this branch and ran the full unit suite locally (1146 tests, matching the PR's claimed validation) plus the three new test files individually (116 tests). All green.

The semantics hold up under determinism scrutiny

This touches the workflow VM's most replay-sensitive machinery, so I traced the settlement paths carefully:

  • Every hasConflict settlement goes through ctx.promiseQueue — both fast-paths (hasCreated / conflict-already-known) and the event-driven paths (hook_created → resolve false, hook_conflict → resolve true). Resolution order always matches event-log order, which is the invariant that keeps replay deterministic.
  • The hook_conflict consumer drains payload and hasConflict waiters synchronously (deferring actual settlement through promiseQueue), so the null-event handler can't see them and raise a spurious suspension — same pattern the existing payload path uses.
  • The suspension trigger condition (hasConflictPromises.length > 0 && !hasCreated && !hasConflict in the null-event handler, plus the eventLogEmpty fast-path in createHasConflictPromise) correctly mirrors the payload-awaiter pattern for both orderings of "awaited before/after log exhaustion." Both are unit-tested.
  • hasAwaitedHookCreation only affects scheduling, never event-log content — it decides which invocation executes steps (queue-all + timeoutSeconds: 0 re-invoke instead of inline), so there's no replay-divergence surface. The reasoning in the runtime.ts comment is exactly right: running a step inline would serialize work the workflow expressed as parallel (hook.hasConflict.then(() => stepB()) racing await stepA()).
  • The EntityConflictError path returning hasAwaitedHookCreation: true is correct and subtle: on a crash-replay where the hook event already exists, the awaiter still needs an immediate re-invocation to consume hook_created and resolve — silently swallowing it would hang the awaiter until something else woke the run.
  • The e2e parallelism proof (hookHasConflictThenStepParallelWorkflow: 100ms stepB triggered via the hasConflict continuation must complete while 10s stepA is still running, asserted via step timestamps) tests the actual claim end-to-end rather than just the mechanism.

My earlier docs comments were both addressed (the create-hook example now explicitly points to awaiting the hook/iterating for payload data).

One sequencing flag: the v4 docs (should-fix before merge, or fast-follow the backport)

hasConflict does not exist on stable — I checked origin/stable:packages/core/src/create-hook.ts and there are zero references. But this PR adds the API to the v4 docs (docs/content/docs/v4/...), and docs deploy from main. The moment this merges, the live v4 documentation advertises a property that no released 4.x SDK has — and it'll stay wrong until a backport merges to stableand ships in a 4.x release.

Two clean options:

  1. Drop the v4 docs pages from this PR and let the backport PR carry them (the backport flow cherry-picks docs/content/ normally, so they'd land together with the code on stable — though note they'd then need a separate main-side commit since v4 docs render from main), or
  2. Keep them but treat the stable backport + 4.x release as an immediate follow-up so the docs-reality gap is measured in days.

Given the changeset is minor (correct for a new API) and this is a self-contained feature — squarely in the auto-backport criteria — option 2 is probably fine, but it should be a deliberate choice, not an accident.

Non-blocking notes

  • get hasConflict() returns a fresh promise per property access (hook.hasConflict !== hook.hasConflict). Each settles deterministically through promiseQueue, so it's correctness-neutral, but identity-unstable promises from a property (vs a method) can surprise — e.g. registering two waiters from what reads like one. Caching the promise per hook after first access would be friendlier and would also make the hasConflictAwaiter flag-set happen exactly once. Fine as-is.
  • The local hasConflict boolean inside createCreateHook now shadows the public property name; a rename (sawConflictEvent?) would help the next reader.

CI

The two red E2E jobs are baseline flakes unrelated to this PR: astro fails AbortController > abortFromStepWorkflow (the abort area #2244 just touched on main) and hono fails resilient start: addTenWorkflow ... run_created returns 500 (known flake). No hook test failed anywhere. The required check needs a re-run to go green before merge.

Nice work — this is a well-reasoned addition in a part of the codebase where it's very easy to be subtly wrong.

TooTallNateand others added 3 commits June 11, 2026 14:29
State both resolution values explicitly (true = token already owned,
false = registered) instead of a parenthetical that only described the
false case.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment threadAGENTS.md
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
expect.anything()
);
expect(result.hasAwaitedHookCreation).toBe(true);
expect(result.timeoutSeconds).toBeUndefined();

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.

Test assertions check non-existent result.timeoutSeconds property, making toBeUndefined() assertions pass trivially without verifying actual behavior.

Fix on Vercel

@github-actions

Copy link
Copy Markdown
Contributor

No backport to stable for e163422 (AI decision).

This is a new minor feature adding the hook.hasConflict API to the public hook interface, intended for the next major release on main. It's a self-contained feature addition (not a bug fix) and the changeset is marked as a minor bump for the beta channel. Per the criteria, new feature additions targeted at the next major release should generally stay on main rather than be backported to stable.

To override, re-run the Backport to stable workflow manually via workflow_dispatch and paste this commit SHA into the ref input:

e163422551e80eda88a15fec3d3d574a00c53e93

@github-actionsgithub-actionsBot mentioned this pull request Jun 11, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Backport PR opened against stable: #2372. Merge conflicts were resolved by AI — please review carefully. (backport job run)

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.

3 participants

@pranaygp@TooTallNate
, '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 `hook.hasConflict` for early hook conflict detection by pranaygp · Pull Request #2015 · vercel/workflow · GitHub
Skip to content

Add hook.hasConflict for early hook conflict detection - #2015

Merged
TooTallNate merged 9 commits into
mainfrom
pranaygp/codex/hook-ready
Jun 11, 2026
Merged

Add hook.hasConflict for early hook conflict detection#2015
TooTallNate merged 9 commits into
mainfrom
pranaygp/codex/hook-ready

Conversation

@pranaygp

@pranaygppranaygp commented May 18, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds hook.hasConflict, a lazy Promise<boolean> getter that lets workflow code commit hook registration and check for token conflicts without waiting for hook payload data.

Calling createHook() alone does not register the hook — registration is only committed when the workflow suspends. Awaiting hook.hasConflict suspends the workflow to commit the registration, then resolves with false once hook_created is recorded, or true if another active hook already owns the token. Unlike awaiting the hook payload (which rejects with HookConflictError on conflict), hasConflict never throws, so workflows can branch on conflicts early:

using hook=createHook({token: `order:${orderId}`});if(awaithook.hasConflict){return;// another run already owns this token}

This gives workflows a way to claim a deterministic hook token for idempotency before running duplicate-sensitive work, and is a prerequisite for #2011, where the idempotency docs/examples should use await hook.hasConflict instead of requiring consumers to wait for or resume hook data just to claim the token.

Changes

  • Add Hook.hasConflict: Promise<boolean> to the public hook interface.
  • Resolve pending hasConflict waiters with false on hook_created and true on hook_conflict; both fast-paths settle through promiseQueue so resolution order matches event-log order.
  • Suspend workflow execution when hook.hasConflict is awaited before hook creation is recorded.
  • Skip inline step execution when a suspension contains an awaited hook creation, so a hasConflict continuation (e.g. hook.hasConflict.then(() => stepB())) can advance in parallel with other steps instead of only after they finish.
  • Document hook.hasConflict in the v4 and v5 hook/webhook docs and event-sourcing pages, including the suspension-commits-registration semantics.
  • Add core unit tests (resolution, suspension, conflict, payload preservation, suspension-handler scheduling) and e2e tests (registration without payload, non-blocking steps, parallel continuation scheduling, conflict resolves true).

Validation

  • cd packages/core && pnpm typecheck && pnpm vitest run src/ (1146 tests)
  • Hook e2e suite against local nextjs-turbopack dev server: pnpm vitest run packages/core/e2e/e2e.test.ts -t 'hook' (20 tests, including the new parallel-continuation test)

Docs Preview

Changed pages on the docs preview deployment:

Pagev4v5
createHook() API referencev4v5
createWebhook() API referencev4v5
Foundations: Hooksv4v5
How It Works: Event Sourcingv4v5

CopilotAI review requested due to automatic review settings May 18, 2026 23:22
@pranaygp
pranaygp requested a review from a team as a code ownerMay 18, 2026 23:22
@changeset-bot

changeset-botBot commented May 18, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 2342dae

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

This PR includes changesets to release 17 packages
NameType
@workflow/coreMinor
workflowMinor
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/webPatch
@workflow/world-testingPatch
@workflow/aiMajor
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch
@workflow/nuxtPatch

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 May 18, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented May 18, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production137602191595
✅ 💻 Local Development181102192030
✅ 📦 Local Production181102192030
✅ 🐘 Local Postgres179702332030
✅ 🪟 Windows14500145
✅ 📋 Other83701781015
Total7777010688845

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro119026
✅ example119026
✅ express119026
✅ fastify119026
✅ hono119026
✅ nextjs-turbopack14302
✅ nextjs-webpack14302
✅ nitro119026
✅ nuxt119026
✅ sveltekit13807
✅ vite119026
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable120025
✅ express-stable120025
✅ fastify-stable120025
✅ hono-stable120025
✅ nextjs-turbopack-canary126019
✅ nextjs-turbopack-stable-lazy-discovery-disabled14500
✅ nextjs-turbopack-stable-lazy-discovery-enabled14500
✅ nextjs-webpack-canary126019
✅ nextjs-webpack-stable-lazy-discovery-disabled14500
✅ nextjs-webpack-stable-lazy-discovery-enabled14500
✅ nitro-stable120025
✅ nuxt-stable120025
✅ sveltekit-stable13906
✅ vite-stable120025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable120025
✅ express-stable120025
✅ fastify-stable120025
✅ hono-stable120025
✅ nextjs-turbopack-canary126019
✅ nextjs-turbopack-stable-lazy-discovery-disabled14500
✅ nextjs-turbopack-stable-lazy-discovery-enabled14500
✅ nextjs-webpack-canary126019
✅ nextjs-webpack-stable-lazy-discovery-disabled14500
✅ nextjs-webpack-stable-lazy-discovery-enabled14500
✅ nitro-stable120025
✅ nuxt-stable120025
✅ sveltekit-stable13906
✅ vite-stable120025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable119026
✅ express-stable119026
✅ fastify-stable119026
✅ hono-stable119026
✅ nextjs-turbopack-canary125020
✅ nextjs-turbopack-stable-lazy-discovery-disabled14401
✅ nextjs-turbopack-stable-lazy-discovery-enabled14401
✅ nextjs-webpack-canary125020
✅ nextjs-webpack-stable-lazy-discovery-disabled14401
✅ nextjs-webpack-stable-lazy-discovery-enabled14401
✅ nitro-stable119026
✅ nuxt-stable119026
✅ sveltekit-stable13807
✅ vite-stable119026
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack14500
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable120025
✅ e2e-local-dev-tanstack-start-120025
✅ e2e-local-postgres-nest-stable119026
✅ e2e-local-postgres-tanstack-start-119026
✅ e2e-local-prod-nest-stable120025
✅ e2e-local-prod-tanstack-start-120025
✅ e2e-vercel-prod-tanstack-start119026

📋 View full workflow run

@github-actions

github-actionsBot commented May 18, 2026

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

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

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro0.040s (-5.4% 🟢)1.005s (~)0.965s101.00x
💻 LocalExpress0.041s (-2.8%)1.005s (~)0.964s101.03x
💻 LocalNext.js (Turbopack)0.060s (-4.0%)1.005s (~)0.945s101.51x
🐘 PostgresNitro0.062s (-5.2% 🟢)1.012s (~)0.950s101.56x
🐘 PostgresExpress0.067s (+9.0% 🔺)1.014s (~)0.947s101.67x
🐘 PostgresNext.js (Turbopack)0.068s (-8.6% 🟢)1.013s (~)0.945s101.71x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express0.414s (+52.9% 🔺)2.276s (-11.8% 🟢)1.863s101.00x
▲ VercelNitro0.420s (+11.1% 🔺)2.271s (+8.1% 🔺)1.851s101.01x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.093s (~)2.006s (~)0.913s101.00x
💻 LocalExpress1.094s (~)2.006s (~)0.912s101.00x
🐘 PostgresNitro1.105s (-0.7%)2.010s (~)0.906s101.01x
🐘 PostgresExpress1.114s (~)2.009s (~)0.895s101.02x
💻 LocalNext.js (Turbopack)1.136s (+0.7%)2.007s (~)0.871s101.04x
🐘 PostgresNext.js (Turbopack)1.150s (+0.6%)2.009s (~)0.859s101.05x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.681s (-4.1%)3.608s (-2.7%)1.927s101.00x
▲ VercelNitro32.051s (+1812.5% 🔺)34.165s (+890.4% 🔺)2.114s1019.06x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express10.500s (~)11.022s (~)0.523s31.00x
🐘 PostgresExpress10.570s (~)11.020s (~)0.451s31.01x
🐘 PostgresNitro10.578s (~)11.023s (~)0.444s31.01x
💻 LocalNitro10.651s (+1.0%)11.022s (~)0.372s31.01x
💻 LocalNext.js (Turbopack)10.792s (~)11.021s (~)0.229s31.03x
🐘 PostgresNext.js (Turbopack)10.937s (-0.7%)11.351s (-2.9%)0.415s31.04x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro13.882s (+4.1%)16.196s (+7.0% 🔺)2.314s21.00x
▲ VercelExpress14.281s (+6.5% 🔺)16.108s (+2.4%)1.827s21.03x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express13.701s (-1.0%)14.026s (~)0.325s51.00x
💻 LocalNitro13.738s (-0.7%)14.027s (-1.4%)0.289s51.00x
🐘 PostgresExpress13.763s (-0.5%)14.022s (~)0.259s51.00x
🐘 PostgresNitro13.921s (+0.7%)14.023s (~)0.102s51.02x
💻 LocalNext.js (Turbopack)14.364s (-0.9%)15.030s (~)0.665s41.05x
🐘 PostgresNext.js (Turbopack)14.456s (~)15.014s (~)0.558s41.06x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro21.579s (-3.8%)23.291s (-3.3%)1.712s31.00x
▲ VercelExpress24.509s (+14.1% 🔺)26.394s (+14.4% 🔺)1.885s31.14x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro12.331s (-1.1%)13.026s (~)0.695s71.00x
💻 LocalExpress12.446s (~)13.025s (~)0.579s71.01x
🐘 PostgresExpress12.526s (-1.7%)13.020s (~)0.494s71.02x
🐘 PostgresNitro12.719s (+1.1%)13.025s (~)0.307s71.03x
💻 LocalNext.js (Turbopack)13.813s (+1.0%)14.169s (+1.0%)0.356s71.12x
🐘 PostgresNext.js (Turbopack)13.852s (-0.9%)14.161s (-1.0%)0.309s71.12x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro29.649s (-6.5% 🟢)31.500s (-5.4% 🟢)1.851s31.00x
▲ VercelExpress31.179s (+5.1% 🔺)33.707s (+8.2% 🔺)2.528s31.05x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express1.208s (+1.5%)2.006s (~)0.798s151.00x
🐘 PostgresNitro1.208s (~)2.009s (~)0.800s151.00x
🐘 PostgresExpress1.231s (+2.4%)2.008s (~)0.777s151.02x
💻 LocalNitro1.279s (+4.6%)2.006s (~)0.726s151.06x
🐘 PostgresNext.js (Turbopack)1.339s (+6.4% 🔺)2.074s (+3.3%)0.734s151.11x
💻 LocalNext.js (Turbopack)1.368s (+1.4%)2.006s (~)0.638s151.13x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.970s (+16.4% 🔺)4.450s (-1.1%)1.480s71.00x
▲ VercelNitro3.506s (-5.2% 🟢)5.306s (+2.4%)1.799s61.18x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.366s (+8.5% 🔺)2.394s (+19.3% 🔺)1.028s131.00x
🐘 PostgresExpress1.389s (+10.0% 🔺)2.508s (+25.0% 🔺)1.119s121.02x
🐘 PostgresNext.js (Turbopack)1.618s (+15.5% 🔺)2.393s (+19.3% 🔺)0.776s131.18x
💻 LocalNitro1.704s (~)2.006s (~)0.302s151.25x
💻 LocalExpress1.729s (-6.1% 🟢)2.006s (-6.7% 🟢)0.277s151.27x
💻 LocalNext.js (Turbopack)1.842s (-2.0%)2.074s (-3.5%)0.232s151.35x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.196s (-27.7% 🟢)4.831s (-21.0% 🟢)1.635s71.00x
▲ VercelNitro3.207s (-48.6% 🟢)4.657s (-39.9% 🟢)1.450s71.00x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.680s (+19.4% 🔺)4.141s (+106.2% 🔺)2.461s81.00x
🐘 PostgresExpress1.718s (+22.5% 🔺)3.889s (+93.6% 🔺)2.172s81.02x
🐘 PostgresNext.js (Turbopack)3.508s (+94.5% 🔺)4.297s (+87.1% 🔺)0.789s72.09x
💻 LocalNitro4.696s (-10.5% 🟢)5.347s (-11.1% 🟢)0.650s62.80x
💻 LocalExpress5.272s (-2.1%)6.013s (-2.7%)0.742s53.14x
💻 LocalNext.js (Turbopack)5.395s (-5.4% 🟢)6.014s (-3.2%)0.619s53.21x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.396s (-51.8% 🟢)6.218s (-45.2% 🟢)1.823s61.00x
▲ VercelNitro4.696s (-72.4% 🟢)6.708s (-63.3% 🟢)2.012s51.07x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.221s (+0.8%)2.074s (+3.3%)0.853s151.00x
🐘 PostgresNitro1.224s (+2.8%)2.008s (~)0.784s151.00x
🐘 PostgresNext.js (Turbopack)1.278s (+1.2%)2.008s (~)0.729s151.05x
💻 LocalNext.js (Turbopack)1.391s (+2.9%)2.006s (~)0.615s151.14x
💻 LocalNitro1.502s (~)2.006s (~)0.505s151.23x
💻 LocalExpress1.539s (-7.2% 🟢)2.006s (-3.3%)0.467s151.26x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.632s (+2.1%)4.218s (~)1.586s81.00x
▲ VercelExpress2.986s (+10.6% 🔺)4.712s (~)1.726s71.13x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.394s (+9.6% 🔺)2.224s (+10.8% 🔺)0.830s141.00x
🐘 PostgresExpress1.424s (+9.8% 🔺)2.317s (+15.3% 🔺)0.892s131.02x
🐘 PostgresNext.js (Turbopack)1.506s (+7.3% 🔺)2.150s (+7.1% 🔺)0.644s141.08x
💻 LocalExpress1.865s (-13.6% 🟢)2.295s (-14.2% 🟢)0.429s141.34x
💻 LocalNext.js (Turbopack)2.055s (~)2.591s (-13.9% 🟢)0.536s121.47x
💻 LocalNitro2.153s (-0.8%)2.676s (+3.2%)0.523s121.55x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.328s (-2.9%)5.132s (+0.6%)1.803s61.00x
▲ VercelNitro3.433s (-28.9% 🟢)5.018s (-21.6% 🟢)1.585s61.03x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.617s (+16.5% 🔺)4.299s (+114.1% 🔺)2.682s71.00x
🐘 PostgresExpress1.837s (+31.1% 🔺)3.891s (+93.5% 🔺)2.053s81.14x
🐘 PostgresNext.js (Turbopack)3.367s (+86.2% 🔺)4.582s (+106.0% 🔺)1.215s72.08x
💻 LocalExpress4.294s (-32.4% 🟢)5.016s (-28.5% 🟢)0.721s62.66x
💻 LocalNitro5.167s (-5.0%)5.851s (-2.7%)0.685s63.19x
💻 LocalNext.js (Turbopack)5.712s (-5.7% 🟢)6.017s (-11.7% 🟢)0.305s53.53x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.990s (-21.8% 🟢)5.843s (-19.3% 🟢)1.853s61.00x
▲ VercelNitro4.324s (-38.2% 🟢)6.347s (-26.2% 🟢)2.023s51.08x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro0.589s (-1.8%)1.005s (~)0.416s601.00x
🐘 PostgresExpress0.603s (~)1.023s (+1.6%)0.421s591.02x
🐘 PostgresNitro0.603s (+4.1%)1.007s (~)0.404s601.02x
💻 LocalExpress0.611s (~)1.039s (+3.4%)0.429s581.04x
🐘 PostgresNext.js (Turbopack)0.811s (-5.0% 🟢)1.006s (-1.8%)0.194s601.38x
💻 LocalNext.js (Turbopack)0.892s (+3.3%)1.057s (+3.5%)0.166s571.51x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express5.921s (-41.6% 🟢)7.609s (-35.4% 🟢)1.688s81.00x
▲ VercelNitro6.497s (+31.0% 🔺)8.122s (+24.7% 🔺)1.625s81.10x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.360s (-5.1% 🟢)2.008s (~)0.647s451.00x
🐘 PostgresNitro1.410s (-1.8%)2.008s (-1.1%)0.598s451.04x
💻 LocalNitro1.508s (~)2.006s (~)0.497s451.11x
💻 LocalExpress1.526s (-1.8%)2.028s (~)0.502s451.12x
🐘 PostgresNext.js (Turbopack)1.894s (-6.7% 🟢)2.053s (-22.7% 🟢)0.159s441.39x
💻 LocalNext.js (Turbopack)2.084s (-0.7%)3.008s (~)0.924s301.53x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro14.267s (+8.9% 🔺)16.133s (+11.6% 🔺)1.866s61.00x
▲ VercelExpress15.632s (+8.8% 🔺)17.362s (+7.5% 🔺)1.729s61.10x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro2.756s (-1.4%)3.059s (-3.4%)0.303s401.00x
🐘 PostgresExpress2.805s (~)3.112s (-0.8%)0.307s391.02x
💻 LocalExpress3.225s (-1.8%)3.977s (-0.8%)0.752s311.17x
💻 LocalNitro3.248s (-1.5%)4.009s (~)0.761s301.18x
🐘 PostgresNext.js (Turbopack)3.766s (-5.5% 🟢)4.043s (-6.7% 🟢)0.276s301.37x
💻 LocalNext.js (Turbopack)4.291s (-1.3%)5.010s (~)0.720s241.56x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express32.130s (+22.5% 🔺)34.248s (+21.4% 🔺)2.117s41.00x
▲ VercelNitro34.856s (+19.7% 🔺)36.875s (+19.7% 🔺)2.019s41.08x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.244s (+11.1% 🔺)1.007s (~)0.763s601.00x
🐘 PostgresExpress0.250s (+7.4% 🔺)1.006s (~)0.756s601.03x
🐘 PostgresNext.js (Turbopack)0.289s (+4.2%)1.006s (~)0.717s601.19x
💻 LocalExpress0.423s (+2.4%)1.004s (~)0.582s601.74x
💻 LocalNitro0.431s (~)1.004s (-1.7%)0.573s601.77x
💻 LocalNext.js (Turbopack)0.569s (+4.1%)1.005s (~)0.436s602.33x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.874s (-27.0% 🟢)3.565s (-13.2% 🟢)1.692s171.00x
▲ VercelNitro2.088s (-26.0% 🟢)3.684s (-14.7% 🟢)1.596s171.11x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.393s (+9.5% 🔺)1.053s (+4.7%)0.661s861.00x
🐘 PostgresExpress0.411s (+9.7% 🔺)1.041s (+3.5%)0.630s871.05x
🐘 PostgresNext.js (Turbopack)0.583s (+16.8% 🔺)1.091s (+8.4% 🔺)0.508s831.48x
💻 LocalNitro2.128s (-2.5%)2.737s (+0.8%)0.609s335.42x
💻 LocalExpress2.226s (+9.2% 🔺)2.766s (+9.1% 🔺)0.540s335.67x
💻 LocalNext.js (Turbopack)2.521s (+2.9%)3.218s (+2.2%)0.697s296.42x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.431s (-62.9% 🟢)4.945s (-54.5% 🟢)1.514s191.00x
▲ VercelExpress21.388s (+295.0% 🔺)23.082s (+226.3% 🔺)1.694s176.23x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro | Express

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.795s (+16.7% 🔺)1.567s (+55.7% 🔺)0.772s771.00x
🐘 PostgresExpress0.805s (+15.6% 🔺)1.419s (+41.0% 🔺)0.614s851.01x
🐘 PostgresNext.js (Turbopack)2.760s (+174.5% 🔺)3.681s (+98.5% 🔺)0.921s333.47x
💻 LocalExpress9.388s (+0.8%)10.195s (+3.3%)0.807s1211.80x
💻 LocalNitro9.416s (+1.5%)9.874s (+0.8%)0.458s1311.84x
💻 LocalNext.js (Turbopack)10.703s (-2.2%)11.576s (+0.8%)0.873s1113.46x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express6.877s (-63.6% 🟢)8.574s (-58.2% 🟢)1.697s141.00x
▲ VercelNitro8.266s (-57.1% 🟢)10.222s (-53.8% 🟢)1.957s121.20x
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Express | Nitro

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express1.160s (-0.7%)2.005s (~)0.010s (-14.5% 🟢)2.017s (~)0.857s101.00x
🐘 PostgresNitro1.165s (-0.7%)2.001s (~)0.001s (-15.4% 🟢)2.010s (~)0.846s101.00x
💻 LocalNitro1.167s (~)2.005s (~)0.010s (~)2.018s (~)0.851s101.01x
🐘 PostgresExpress1.172s (-0.6%)2.000s (~)0.001s (-8.3% 🟢)2.010s (~)0.838s101.01x
💻 LocalNext.js (Turbopack)1.211s (~)2.004s (~)0.013s (+0.8%)2.020s (~)0.809s101.04x
🐘 PostgresNext.js (Turbopack)1.234s (-0.9%)2.002s (~)0.001s (+7.7% 🔺)2.011s (~)0.777s101.06x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.797s (+14.3% 🔺)4.260s (+26.7% 🔺)0.638s (-32.8% 🟢)5.340s (+12.0% 🔺)2.543s101.00x
▲ VercelExpress32.861s (+1415.8% 🔺)3.322s (+3.7%)1.152s (+26.8% 🔺)35.409s (+673.0% 🔺)2.548s1011.75x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro | Express

stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.566s (-1.8%)2.009s (~)0.012s (-1.6%)2.023s (~)0.457s301.00x
💻 LocalExpress1.568s (~)2.010s (~)0.013s (+14.2% 🔺)2.026s (~)0.457s301.00x
🐘 PostgresExpress1.579s (-1.4%)2.006s (~)0.005s (+7.1% 🔺)2.027s (~)0.448s301.01x
🐘 PostgresNitro1.616s (~)2.006s (~)0.005s (-8.7% 🟢)2.025s (~)0.408s301.03x
💻 LocalNext.js (Turbopack)1.730s (-0.6%)2.010s (~)0.012s (-1.9%)2.025s (~)0.295s301.10x
🐘 PostgresNext.js (Turbopack)1.755s (-1.7%)2.010s (~)0.005s (+2.7%)2.026s (~)0.270s301.12x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express5.998s (-1.2%)7.151s (-2.0%)0.235s (-10.5% 🟢)7.964s (~)1.966s81.00x
▲ VercelNitro6.812s (+15.1% 🔺)7.759s (+10.4% 🔺)0.245s (-47.7% 🟢)8.558s (+7.1% 🔺)1.745s81.14x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Express | Nitro

10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.798s (+12.6% 🔺)1.068s (+2.4%)0.000s (-66.1% 🟢)1.088s (+2.3%)0.291s561.00x
🐘 PostgresExpress0.805s (+13.8% 🔺)1.084s (+6.9% 🔺)0.000s (+221.8% 🔺)1.099s (+6.8% 🔺)0.293s551.01x
🐘 PostgresNext.js (Turbopack)0.998s (+15.9% 🔺)1.463s (+34.1% 🔺)0.000s (-100.0% 🟢)1.471s (+33.9% 🔺)0.473s411.25x
💻 LocalNitro1.386s (-3.8%)2.013s (~)0.001s (+260.0% 🔺)2.015s (~)0.630s301.74x
💻 LocalExpress1.391s (+3.1%)2.013s (~)0.000s (+175.0% 🔺)2.015s (~)0.624s301.74x
💻 LocalNext.js (Turbopack)1.462s (~)2.013s (~)0.000s (-40.0% 🟢)2.016s (~)0.554s301.83x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.320s (-1.2%)4.629s (+1.2%)0.000s (-100.0% 🟢)5.052s (~)1.731s121.00x
▲ VercelNitro3.595s (-5.5% 🟢)4.764s (-1.3%)0.001s (+Infinity% 🔺)5.183s (-0.7%)1.588s121.08x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Express | Nitro

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

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.508s (+13.1% 🔺)2.032s (~)0.000s (NaN%)2.061s (~)0.553s301.00x
🐘 PostgresExpress1.669s (+9.4% 🔺)2.217s (~)0.000s (-50.0% 🟢)2.252s (~)0.583s271.11x
🐘 PostgresNext.js (Turbopack)2.069s (+19.6% 🔺)2.404s (+6.3% 🔺)0.000s (+Infinity% 🔺)2.432s (+7.1% 🔺)0.363s251.37x
💻 LocalNext.js (Turbopack)2.812s (-1.4%)3.357s (-1.7%)0.001s (+100.0% 🔺)3.361s (-1.7%)0.549s181.86x
💻 LocalNitro2.994s (-5.5% 🟢)3.613s (-5.8% 🟢)0.000s (+119.6% 🔺)3.620s (-5.8% 🟢)0.626s171.98x
💻 LocalExpress3.129s (+1.1%)3.902s (+6.3% 🔺)0.001s (+44.9% 🔺)3.905s (+6.2% 🔺)0.776s162.07x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro5.070s (-31.3% 🟢)6.077s (-23.4% 🟢)0.000s (+Infinity% 🔺)6.502s (-28.0% 🟢)1.433s101.00x
▲ VercelExpress5.299s (~)6.264s (-4.8%)0.000s (NaN%)6.717s (-6.6% 🟢)1.418s91.05x
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro | Express

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNitro10/21
🐘 PostgresNitro14/21
▲ VercelExpress13/21
Fastest World by Framework

Winner determined by most benchmark wins

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

Worlds:

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

📋 View full workflow run


Some benchmark jobs failed:

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

Check the workflow run for details.

Comment threaddocs/content/docs/v4/api-reference/workflow/create-hook.mdx Outdated

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

Adds a new hook.ready promise to let workflow code wait for hook registration (hook_created) independently of receiving payloads, enabling deterministic token claiming for idempotency patterns (and consistent conflict handling via HookConflictError).

Changes:

  • Extend the public Hook interface with ready: Promise<void> and implement it in the workflow VM hook runtime.
  • Resolve/reject ready waiters based on hook_created / hook_conflict, including suspension behavior when awaiting readiness before registration is recorded.
  • Add tests and update v4/v5 documentation to describe hook.ready (and webhook readiness via the shared Hook interface).

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 1 comment.

Show a summary per file
FileDescription
packages/core/src/workflow/hook.tsImplements hook.ready tracking/queuing and resolves/rejects it on hook_created/hook_conflict, including suspension when needed.
packages/core/src/workflow/hook.test.tsAdds unit tests covering ready resolution, suspension behavior, conflict rejection, and ensuring payload consumption is unchanged.
packages/core/src/workflow.test.tsAdds integration tests verifying suspension, resolution, and conflict behavior when hook.ready is used inside runWorkflow.
packages/core/src/create-hook.tsExtends the public Hook interface with documented ready: Promise<void>.
docs/content/docs/v5/how-it-works/event-sourcing.mdxDocuments that hook.ready rejects on hook conflicts in addition to payload awaiting.
docs/content/docs/v5/foundations/hooks.mdxAdds a section explaining when/how to use hook.ready to wait for registration.
docs/content/docs/v5/api-reference/workflow/create-webhook.mdxNotes ready availability on returned webhook objects and how to use it before sharing the URL.
docs/content/docs/v5/api-reference/workflow/create-hook.mdxDocuments hook.ready and adds a registration-focused example.
docs/content/docs/v4/how-it-works/event-sourcing.mdxMirrors v5 event-sourcing updates for hook.ready conflict semantics.
docs/content/docs/v4/foundations/hooks.mdxMirrors v5 “Waiting for Hook Registration” guidance.
docs/content/docs/v4/api-reference/workflow/create-webhook.mdxMirrors v5 webhook ready documentation note.
docs/content/docs/v4/api-reference/workflow/create-hook.mdxMirrors v5 hook.ready API docs and example.
.changeset/hook-ready.mdDeclares minor bumps for @workflow/core and workflow to ship the new API.

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

Comment threadpackages/core/src/workflow/hook.test.ts Outdated
Comment threaddocs/content/docs/v4/api-reference/workflow/create-webhook.mdx Outdated
Comment threaddocs/content/docs/v5/api-reference/workflow/create-webhook.mdx Outdated

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

Copilot reviewed 20 out of 20 changed files in this pull request and generated 1 comment.

Comment threadpackages/core/src/workflow/hook.ts Outdated
Comment threadpackages/core/src/runtime.ts Outdated
pranaygpand others added 3 commits May 18, 2026 21:44
…hook-ready
* origin-https/main: (119 commits)
fix: settle aborted parallel steps before completing abortParallelWorkflow (#2244)
Add native v4 workflow attribute events (#2226)
Version Packages (beta) (#2326)
[ci] Fix flaky windows unit tests (#2359)
Capture Vercel runtime logs when e2e Vercel Prod lanes fail (#2356)
Fix e2e failure reporting under vitest 4 and preserve fetch error causes (#2355)
[core] Fix process crash from rejected waitUntil promises (#2336)
[core] Remove duplicate `waitUntil` for suspension handler async operations (#2345)
Prevent local tests from hanging (#2338)
feat(core): add optional namespace for queue topic prefix (#2305)
[web-shared] Show precise durations in the new trace viewer (#2335)
Validate unique workflow step IDs at build time (#2018)
Move run attributes into their own detail card (#2327)
[core] Forward-port stream reconnect to getReadable level (#2318)
[docs] Add "Step executed multiple times" error page (#2310)
Fix flickering on the detail panel when navigating the trace viewer (#2325)
Version Packages (beta) (#2254)
Patch vulnerable package dependencies (#2301)
Deprecate DurableAgent and update it with WorkflowAgent in v5 docs (#2285)
[codex] Fix partial world-local exclusive writes (#2296)
...
# Conflicts:
#	packages/core/src/runtime.ts
#	packages/core/src/runtime/suspension-handler.ts
- hook.hasConflict resolves true when the token is owned by another
active hook, false once registration is committed — no throw, so
workflows can branch on conflicts early. Awaiting it suspends the
workflow to commit the hook registration (createHook alone does not).
- Chain the already-created fast-path through promiseQueue so
resolution order matches event-log order (review feedback).
- Skip inline step execution when a suspension has an awaited hook
creation so the hasConflict continuation can advance independently
of step execution (review feedback).
- Update unit tests, e2e tests, workbench workflows, and v4/v5 docs.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

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

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

Comment threaddocs/content/docs/v5/api-reference/workflow/create-webhook.mdx Outdated
Comment threaddocs/content/docs/v4/api-reference/workflow/create-webhook.mdx Outdated

@TooTallNateTooTallNate left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Approve — determinism-safe design, strong test coverage; one docs-sequencing flag

I built @workflow/core from this branch and ran the full unit suite locally (1146 tests, matching the PR's claimed validation) plus the three new test files individually (116 tests). All green.

The semantics hold up under determinism scrutiny

This touches the workflow VM's most replay-sensitive machinery, so I traced the settlement paths carefully:

  • Every hasConflict settlement goes through ctx.promiseQueue — both fast-paths (hasCreated / conflict-already-known) and the event-driven paths (hook_created → resolve false, hook_conflict → resolve true). Resolution order always matches event-log order, which is the invariant that keeps replay deterministic.
  • The hook_conflict consumer drains payload and hasConflict waiters synchronously (deferring actual settlement through promiseQueue), so the null-event handler can't see them and raise a spurious suspension — same pattern the existing payload path uses.
  • The suspension trigger condition (hasConflictPromises.length > 0 && !hasCreated && !hasConflict in the null-event handler, plus the eventLogEmpty fast-path in createHasConflictPromise) correctly mirrors the payload-awaiter pattern for both orderings of "awaited before/after log exhaustion." Both are unit-tested.
  • hasAwaitedHookCreation only affects scheduling, never event-log content — it decides which invocation executes steps (queue-all + timeoutSeconds: 0 re-invoke instead of inline), so there's no replay-divergence surface. The reasoning in the runtime.ts comment is exactly right: running a step inline would serialize work the workflow expressed as parallel (hook.hasConflict.then(() => stepB()) racing await stepA()).
  • The EntityConflictError path returning hasAwaitedHookCreation: true is correct and subtle: on a crash-replay where the hook event already exists, the awaiter still needs an immediate re-invocation to consume hook_created and resolve — silently swallowing it would hang the awaiter until something else woke the run.
  • The e2e parallelism proof (hookHasConflictThenStepParallelWorkflow: 100ms stepB triggered via the hasConflict continuation must complete while 10s stepA is still running, asserted via step timestamps) tests the actual claim end-to-end rather than just the mechanism.

My earlier docs comments were both addressed (the create-hook example now explicitly points to awaiting the hook/iterating for payload data).

One sequencing flag: the v4 docs (should-fix before merge, or fast-follow the backport)

hasConflict does not exist on stable — I checked origin/stable:packages/core/src/create-hook.ts and there are zero references. But this PR adds the API to the v4 docs (docs/content/docs/v4/...), and docs deploy from main. The moment this merges, the live v4 documentation advertises a property that no released 4.x SDK has — and it'll stay wrong until a backport merges to stableand ships in a 4.x release.

Two clean options:

  1. Drop the v4 docs pages from this PR and let the backport PR carry them (the backport flow cherry-picks docs/content/ normally, so they'd land together with the code on stable — though note they'd then need a separate main-side commit since v4 docs render from main), or
  2. Keep them but treat the stable backport + 4.x release as an immediate follow-up so the docs-reality gap is measured in days.

Given the changeset is minor (correct for a new API) and this is a self-contained feature — squarely in the auto-backport criteria — option 2 is probably fine, but it should be a deliberate choice, not an accident.

Non-blocking notes

  • get hasConflict() returns a fresh promise per property access (hook.hasConflict !== hook.hasConflict). Each settles deterministically through promiseQueue, so it's correctness-neutral, but identity-unstable promises from a property (vs a method) can surprise — e.g. registering two waiters from what reads like one. Caching the promise per hook after first access would be friendlier and would also make the hasConflictAwaiter flag-set happen exactly once. Fine as-is.
  • The local hasConflict boolean inside createCreateHook now shadows the public property name; a rename (sawConflictEvent?) would help the next reader.

CI

The two red E2E jobs are baseline flakes unrelated to this PR: astro fails AbortController > abortFromStepWorkflow (the abort area #2244 just touched on main) and hono fails resilient start: addTenWorkflow ... run_created returns 500 (known flake). No hook test failed anywhere. The required check needs a re-run to go green before merge.

Nice work — this is a well-reasoned addition in a part of the codebase where it's very easy to be subtly wrong.

TooTallNateand others added 3 commits June 11, 2026 14:29
State both resolution values explicitly (true = token already owned,
false = registered) instead of a parenthetical that only described the
false case.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment threadAGENTS.md
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
expect.anything()
);
expect(result.hasAwaitedHookCreation).toBe(true);
expect(result.timeoutSeconds).toBeUndefined();

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.

Test assertions check non-existent result.timeoutSeconds property, making toBeUndefined() assertions pass trivially without verifying actual behavior.

Fix on Vercel

@github-actions

Copy link
Copy Markdown
Contributor

No backport to stable for e163422 (AI decision).

This is a new minor feature adding the hook.hasConflict API to the public hook interface, intended for the next major release on main. It's a self-contained feature addition (not a bug fix) and the changeset is marked as a minor bump for the beta channel. Per the criteria, new feature additions targeted at the next major release should generally stay on main rather than be backported to stable.

To override, re-run the Backport to stable workflow manually via workflow_dispatch and paste this commit SHA into the ref input:

e163422551e80eda88a15fec3d3d574a00c53e93

@github-actionsgithub-actionsBot mentioned this pull request Jun 11, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Backport PR opened against stable: #2372. Merge conflicts were resolved by AI — please review carefully. (backport job run)

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.

3 participants

@pranaygp@TooTallNate