') + ')', '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('^' + ".*" + ', '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" + ', '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('^' + ".*" + ', '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); } })(); })(); feat(world-vercel): support Hook minimum retention by NathanColosimo · Pull Request #3286 · vercel/workflow · GitHub
Skip to content

feat(world-vercel): support Hook minimum retention - #3286

Merged
NathanColosimo merged 2 commits into
mainfrom
codex/hook-retention-vercel
Aug 7, 2026
Merged

feat(world-vercel): support Hook minimum retention#3286
NathanColosimo merged 2 commits into
mainfrom
codex/hook-retention-vercel

Conversation

@NathanColosimo

@NathanColosimoNathanColosimo commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

The backend PR must be merged and deployed first

Summary

  • advertise Hook minimum-retention support from the Vercel World
  • run the existing Hook-retention end-to-end case against Vercel deployments
  • document the Vercel World's fixed 30-day maximum
  • turn deterministic World validation failures into FatalError so workflows do not retry invalid Hook requests

The SDK remains unaware of the 30-day limit. workflow-server enforces it at the event-persistence boundary and returns HTTP 400. Core recognizes that deterministic response, including the existing Local World validation error, and stops the workflow instead of retrying it.

Limits

  • every Vercel project may request up to 30 days
  • the limit does not depend on plan or Observability Plus
  • requests beyond 30 days fail before event payload upload or entity materialization

The wire field already flows through world-vercel. The capability is enabled only after the backend implements and deploys the storage semantics.

Stack

Verification

  • @workflow/core build passed
  • focused suspension-handler tests: 19 passed
  • dependency build for the Local, Postgres, and Vercel World stack passed
  • world-vercel TypeScript check passed
  • all 39 world-vercel event tests passed
  • workflow-server production build and TypeScript check passed
  • focused V2 and V4 backend integration tests passed against the rebuilt server

Docs Preview

PagePreview
createHook() minimum retentionv5
Vercel World limitationv5

@vercel

vercelBot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

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

ProjectDeploymentActionsUpdated (UTC)
example-nextjs-workflow-turbopackReadyReadyPreviewAug 5, 2026 10:23pm
example-nextjs-workflow-webpackReadyReadyPreviewAug 5, 2026 10:23pm
example-workflowReadyReadyPreviewAug 5, 2026 10:23pm
workbench-astro-workflowReadyReadyPreviewAug 5, 2026 10:23pm
workbench-express-workflowReadyReadyPreviewAug 5, 2026 10:23pm
workbench-fastify-workflowReadyReadyPreviewAug 5, 2026 10:23pm
workbench-hono-workflowReadyReadyPreviewAug 5, 2026 10:23pm
workbench-nestjs-workflowReadyReadyPreviewAug 5, 2026 10:23pm
workbench-nitro-workflowReadyReadyPreviewAug 5, 2026 10:23pm
workbench-nuxt-workflowReadyReadyPreviewAug 5, 2026 10:23pm
workbench-sveltekit-workflowReadyReadyPreviewAug 5, 2026 10:23pm
workbench-tanstack-start-workflowReadyReadyPreviewAug 5, 2026 10:23pm
workbench-vite-workflowReadyReadyPreviewAug 5, 2026 10:23pm
workflow-docsReadyReadyPreview, v0Aug 5, 2026 10:23pm
workflow-swc-playgroundReadyReadyPreviewAug 5, 2026 10:23pm
workflow-tarballsReadyReadyPreviewAug 5, 2026 10:23pm
workflow-webReadyReadyPreviewAug 5, 2026 10:23pm

@changeset-bot

changeset-botBot commented Aug 2, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 4a912fa

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

This PR includes changesets to release 17 packages
NameType
@workflow/world-vercelMinor
@workflow/cliPatch
@workflow/corePatch
@workflow/webPatch
workflowPatch
@workflow/world-testingPatch
@workflow/buildersPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@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

@github-actions

github-actionsBot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

E2E Test Summary

Summary
PassedFailedSkippedTotal
✅ ▲ Vercel Production295404783432
✅ 💻 Local Development329004543744
✅ 📦 Local Production329004543744
✅ 🐘 Local Postgres329004543744
✅ 🪟 Windows31200312
✅ 📋 Other207204242496
✅ vercel-multi-region270027
Total152350226417499
Details by Category

✅ ▲ Vercel Production

AppPassedFailedSkipped
✅ astro-node128028
✅ astro-quickjs128028
✅ example-node128028
✅ example-quickjs128028
✅ express-node128028
✅ express-quickjs128028
✅ fastify-node128028
✅ fastify-quickjs128028
✅ hono-node128028
✅ hono-quickjs128028
✅ nextjs-turbopack-node15303
✅ nextjs-turbopack-quickjs15303
✅ nextjs-webpack-node15303
✅ nextjs-webpack-quickjs15303
✅ nitro-node128028
✅ nitro-quickjs128028
✅ nuxt-node128028
✅ nuxt-quickjs128028
✅ sveltekit-node14709
✅ sveltekit-quickjs14709
✅ vite-node128028
✅ vite-quickjs128028

✅ 💻 Local Development

AppPassedFailedSkipped
✅ astro-stable-node130026
✅ astro-stable-quickjs130026
✅ express-stable-node130026
✅ express-stable-quickjs130026
✅ fastify-stable-node130026
✅ fastify-stable-quickjs130026
✅ hono-stable-node130026
✅ hono-stable-quickjs130026
✅ nextjs-turbopack-canary-node137019
✅ nextjs-turbopack-canary-quickjs137019
✅ nextjs-turbopack-stable-node15600
✅ nextjs-turbopack-stable-quickjs15600
✅ nextjs-webpack-canary-node137019
✅ nextjs-webpack-canary-quickjs137019
✅ nextjs-webpack-stable-node15600
✅ nextjs-webpack-stable-quickjs15600
✅ nitro-stable-node130026
✅ nitro-stable-quickjs130026
✅ nuxt-stable-node130026
✅ nuxt-stable-quickjs130026
✅ sveltekit-stable-node14907
✅ sveltekit-stable-quickjs14907
✅ vite-stable-node130026
✅ vite-stable-quickjs130026

✅ 📦 Local Production

AppPassedFailedSkipped
✅ astro-stable-node130026
✅ astro-stable-quickjs130026
✅ express-stable-node130026
✅ express-stable-quickjs130026
✅ fastify-stable-node130026
✅ fastify-stable-quickjs130026
✅ hono-stable-node130026
✅ hono-stable-quickjs130026
✅ nextjs-turbopack-canary-node137019
✅ nextjs-turbopack-canary-quickjs137019
✅ nextjs-turbopack-stable-node15600
✅ nextjs-turbopack-stable-quickjs15600
✅ nextjs-webpack-canary-node137019
✅ nextjs-webpack-canary-quickjs137019
✅ nextjs-webpack-stable-node15600
✅ nextjs-webpack-stable-quickjs15600
✅ nitro-stable-node130026
✅ nitro-stable-quickjs130026
✅ nuxt-stable-node130026
✅ nuxt-stable-quickjs130026
✅ sveltekit-stable-node14907
✅ sveltekit-stable-quickjs14907
✅ vite-stable-node130026
✅ vite-stable-quickjs130026

✅ 🐘 Local Postgres

AppPassedFailedSkipped
✅ astro-stable-node130026
✅ astro-stable-quickjs130026
✅ express-stable-node130026
✅ express-stable-quickjs130026
✅ fastify-stable-node130026
✅ fastify-stable-quickjs130026
✅ hono-stable-node130026
✅ hono-stable-quickjs130026
✅ nextjs-turbopack-canary-node137019
✅ nextjs-turbopack-canary-quickjs137019
✅ nextjs-turbopack-stable-node15600
✅ nextjs-turbopack-stable-quickjs15600
✅ nextjs-webpack-canary-node137019
✅ nextjs-webpack-canary-quickjs137019
✅ nextjs-webpack-stable-node15600
✅ nextjs-webpack-stable-quickjs15600
✅ nitro-stable-node130026
✅ nitro-stable-quickjs130026
✅ nuxt-stable-node130026
✅ nuxt-stable-quickjs130026
✅ sveltekit-stable-node14907
✅ sveltekit-stable-quickjs14907
✅ vite-stable-node130026
✅ vite-stable-quickjs130026

✅ 🪟 Windows

AppPassedFailedSkipped
✅ nextjs-turbopack-node15600
✅ nextjs-turbopack-quickjs15600

✅ 📋 Other

AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable-node130026
✅ e2e-local-dev-nest-stable-quickjs130026
✅ e2e-local-dev-tanstack-start-node130026
✅ e2e-local-dev-tanstack-start-quickjs130026
✅ e2e-local-postgres-nest-stable-node130026
✅ e2e-local-postgres-nest-stable-quickjs130026
✅ e2e-local-postgres-tanstack-start-node130026
✅ e2e-local-postgres-tanstack-start-quickjs130026
✅ e2e-local-prod-nest-stable-node130026
✅ e2e-local-prod-nest-stable-quickjs130026
✅ e2e-local-prod-tanstack-start-node130026
✅ e2e-local-prod-tanstack-start-quickjs130026
✅ e2e-vercel-prod-nest-node128028
✅ e2e-vercel-prod-nest-quickjs128028
✅ e2e-vercel-prod-tanstack-start-node128028
✅ e2e-vercel-prod-tanstack-start-quickjs128028

✅ vercel-multi-region

AppPassedFailedSkipped
✅ nextjs-turbopack2700

📋 View full workflow run

@github-actions

github-actionsBot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

📊 Workflow Benchmarks

commit 0b1dc5b · Wed, 05 Aug 2026 22:37:22 GMT · run logs

Backend: vercel · app: nextjs-turbopack

MetricScenarioBest (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstep1239 (+61%) 🔻1321 🔴 (+35%) 🔻1361 🔴 (+29%) 🔻1556 🔴 (-7.2%)30
TTFSstream335 (-63%) 💚1296 🔴 (+33%) 🔻1330 🔴 (+34%) 🔻1399 🔴 (+8.0%)30
TTFShook + stream1489 (+71%) 🔻1597 🔴 (+29%) 🔻1640 🔴 (+30%) 🔻1999 🔴 (+34%) 🔻30
STSO1020 steps (inline)92 (+2.2%)122 (-6.9%)139 (-11%)206 (-17%) 💚1018
STSO1020 steps (queue-hop)32073207320732071
WO1020 steps124846 (-3.8%)124846 (-3.8%)124846 (-3.8%)124846 (-3.8%)1
SLstream latency94 (+16%) 🔻160 🔴 (+29%) 🔻206 🔴 (+37%) 🔻629 🔴 (+53%) 🔻30
SOstream overhead (text)103 (+1.0%)164 (+4.5%)188 (+3.3%)234 (+18%) 🔻30
SOstream overhead (structured)110 (+6.8%)147 (-10%)168 (-5.6%)206 (-35%) 💚30
📈 STSO distribution vs main (inline / queue-hop histograms)

1020 steps (inline)

Cumulative STSO time: main 128734ms → this run 120344ms (Δ -8390ms, -7%)

 50-100 ms ┃ main 4 this 34 +30
100-150 ms ███████████████████████┃ main 896 this 921 +25
150-200 ms ┃█ main 93 this 51 -42
200-250 ms ┃ main 16 this 6 -10
250-300 ms ┃ main 3 this 4 +1
300-350 ms ┃ main 4 this 1 -3
350-400 ms ┃ main 1 this 1 +0
400-450 ms ┃ main 1 this 0 -1
500-550 ms ┃ main 1 this 0 -1

1020 steps (queue-hop)

Cumulative STSO time: 3207ms over 1 samples

No main baseline with raw samples yet — showing this run's distribution on its own; the diff appears once a run on main has recorded them.

3000-3500 ms ████████████████████████ steps 1
📜 Previous results (2)

4a912fa

Wed, 05 Aug 2026 05:35:44 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioBest (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstep1240 (+40%) 🔻1342 🔴 (+30%) 🔻1359 🔴 (+2.4%)1418 🔴 (-4.3%)30
TTFSstream210 (-79%) 💚1364 🔴 (+33%) 🔻1405 🔴 (+35%) 🔻1478 🔴 (+27%) 🔻30
TTFShook + stream1518 (+261%) 🔻1642 🔴 (+26%) 🔻1706 🔴 (+11%)1755 🔴 (+2.7%)30
STSO1020 steps (inline)81 (-4.7%)129 (-7.9%)145 (-13%)227 (-47%) 💚1018
STSO1020 steps (queue-hop)3292 (-30%) 💚3292 (-30%) 💚3292 (-30%) 💚3292 (-30%) 💚1
WO1020 steps132284 (-12%)132284 (-12%)132284 (-12%)132284 (-12%)1
SLstream latency101 (+23%) 🔻142 🔴 (+31%) 🔻181 🔴 (+53%) 🔻4026 🔴 (+2282%) 🔻30
SOstream overhead (text)114 (+9.6%)166 (-1.8%)187 (+4.5%)276 (-23%) 💚30
SOstream overhead (structured)103 (-1.0%)145 (-18%) 💚157 (-33%) 💚264 (-49%) 💚30

6ec2f5d

Tue, 04 Aug 2026 00:01:24 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioBest (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstep1271 (+56%) 🔻1329 🔴 (+20%) 🔻1368 🔴 (+16%) 🔻1662 🔴 (-4.5%)30
TTFSstream285 (+5.6%)1293 🔴 (+21%) 🔻1308 🔴 (+21%) 🔻1359 🔴 (+20%) 🔻30
TTFShook + stream593 (-51%) 💚1602 🔴 (+20%) 🔻1669 🔴 (+23%) 🔻1765 🔴 (+23%) 🔻30
STSO1020 steps (inline)173 (+90%) 🔻464 (+197%) 🔻522 (+175%) 🔻698 (+115%) 🔻1016
STSO1020 steps (queue-hop)1447 (-58%) 💚3377 (-2.7%)3377 (-2.7%)3377 (-2.7%)3
WO1020 steps396410 (+165%) 🔻396410 (+165%) 🔻396410 (+165%) 🔻396410 (+165%) 🔻1
SLstream latency87 (-15%)145 🔴 (-4.6%)180 🔴 (-2.2%)229 🔴 (-24%) 💚30
SOstream overhead (text)107 (-26%) 💚164 (-32%) 💚176 (-54%) 💚423 (-3.2%)30
SOstream overhead (structured)98 (-16%) 💚141 (-34%) 💚162 (-31%) 💚458 (+55%) 🔻30
ℹ️ Metric definitions & methodology

The collapsed STSO distribution section above buckets every step gap of the sequential-steps run (not a sampled window), split by whether the step ending the gap ran inline — in the same warm process as the step before it, so the gap is pure framework overhead — or after a queue-hop — the first step of a fresh process, which pays queue dispatch, client reinit and event-log replay. Bars overlay the two runs: is main, marks where this run lands, bridges the gap when this run has more samples in a bucket.

Best/P75/P90/P99 deltas compare against the most recent benchmark run on main at the time of this run. 🔻 flags a delta worse than +15%, 💚 one better than −15%.

Metrics — TTFS: time to first step body (in-deployment start() → first step body, deployment clocks) · STSO: step-to-step overhead (gap between consecutive step bodies) · WO: workflow overhead (whole-run time outside step bodies, in-deployment anchored) · SL: stream latency (in-deployment write → read propagation, readAt - writtenAt) · SO: stream overhead (end-to-end write+consume time beyond the modelled generation window)

Scenarios — step: one trivial no-op step, no stream; no hooks, so the run stays in turbo mode (in-process fast path) · stream: one streaming step; no hooks, so the run stays in turbo mode (in-process fast path) · hook + stream: registers a hook before one step, which exits turbo mode (dispatch path) · 1020 steps: 1020 trivial sequential steps; STSO is measured between consecutive steps in the given step ranges, and WO is the whole-run overhead outside step bodies · stream latency: parallel reader/writer steps on a dedicated stream; SL is the in-deployment write->read propagation (readAt - writtenAt) · stream overhead (text): writer streams 300 variable-length text token deltas paced at 100/s for 3s (a haiku-size LLM's token throughput) while a parallel reader drains the whole stream; SO is the end-to-end write+consume time beyond the 3s generation window (overhead/backpressure) · stream overhead (structured): same workload as stream overhead (text), but each delta is an AI-SDK-style structured object ({ type: 'text-delta', id, text }) instead of a raw string, so the SO gap vs the text scenario is the added serialization cost

🔴 marks a percentile over its target (within target is left unmarked). Targets (p75/p90/p99, ms) — TTFS 200/300/600 · SL 50/60/125 · SO 250/500/1000

All metrics are measured from deployment-side timestamps only. Runs are triggered by an in-deployment route that stamps the anchor (clientStart) right before start(), so the CI runner’s request and its path through api.vercel.com sit outside every measured window. TTFS = in-deployment start() → first step body (turbo uses the in-process fast path, non-turbo the dispatch path), and includes the VQS dispatch hop plus any /flow cold start. STSO/WO are measured between step bodies on the deployment. SL is measured inside the workflow (parallel reader/writer steps), so it no longer includes the api.vercel.com read path.

Cold starts are kept in the numbers on purpose — they are part of real bursty-workload latency. The workbench deployment cold-starts the /flow invocation for a large fraction of runs, inflating P75+; the Best column shows the fastest (warm-start) sample for comparison.

@github-actions

Copy link
Copy Markdown
Contributor

No backport to stable for 4bb86d3 (AI decision).

This is feature work: it advertises a new hookRetention capability from the Vercel World and enables the corresponding Hook minimum-retention e2e case, adding a capability that does not exist on stable (verified: origin/stable's packages/world-vercel/src/index.ts has no hookRetention capability and packages/core/src/runtime/suspension-handler.ts has no attribute/World validation handling at all). The one arguably fix-like part — turning deterministic World validation rejections into FatalError in createHookEvent so workflows stop retrying invalid Hook requests — is a generalization of main-only validation-failure code and would need to be split out and force-backported if wanted. Per policy, a commit mixing a fix with feature work counts as feature work.

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

4bb86d305423a2da813cbef8b7d6fe4421288bba

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.

2 participants

@NathanColosimo@VaguelySerious