Skip to content

feat(core): add experimental Hook token expiration#2865

Draft
NathanColosimo wants to merge 20 commits into
mainfrom
codex/hook-retention-core
Draft

feat(core): add experimental Hook token expiration#2865
NathanColosimo wants to merge 20 commits into
mainfrom
codex/hook-retention-core

Conversation

@NathanColosimo

@NathanColosimo NathanColosimo commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Summary

  • add experimental_expires to createHook(), accepting the same duration strings, millisecond numbers, and absolute Dates as sleep()
  • store the absolute tokenExpiresAt time on hook_created and forward it as hookTokenExpiresAt through the Vercel event wire
  • keep active Hooks working even after the configured time; after the run ends, keep the token unavailable only until that time
  • make dispose() and using release the token immediately, and exclude webhooks
  • document unsupported Worlds

This PR defines and transports the portable contract only. It does not implement expiration in a World. A stacked follow-up adds the Local World implementation and integration coverage.

Part of #2376.

Feedback requested

This is a running list of design questions I would like team feedback on:

  1. Disposal and manual release

    • hook.dispose() currently disposes the Hook and releases its token immediately. With experimental_expires, this still seems like the expected behavior.
    • using calls dispose() automatically. Users who want the token to remain unavailable after the run must therefore stop using using for that Hook. Is that tradeoff acceptable, or should automatic disposal behave differently when expiration is configured?
    • Do we need an API for releasing an existing token from outside the owning workflow? This is especially relevant after the run ends, when the resumable Hook is gone but its token may still be unavailable.
  2. Unsupported Worlds

    • The current contract has older or unsupported Worlds ignore experimental_expires without erroring.
    • Alternatives include incrementing the spec version, adding World capabilities, failing during World startup, or failing when a workflow first uses the option.
    • Should unsupported expiration be best-effort or fail-fast?
  3. Option name

    • Does experimental_expires communicate the behavior clearly?
    • In particular, is it still intuitive once readers learn that reaching the configured time never stops an active Hook?
  4. When expiration starts and how it relates to run retention

    • The current design measures relative values from createHook(), not from workflow completion. This matches the usual expectation that an idempotency window starts at the intake or operation start time.
    • A Vercel World implementation would likely cap expiration at 30 days from Hook creation to match current data-retention limits, and should validate that limit when the Hook is created rather than when the workflow ends.
    • Active workflows keep their Hooks even after the configured time. For example, if a run lasts 40 days and expiration was set to 30 days, the token becomes available only when the run ends.
    • Completed run data can outlive the expiration window. If a run finishes on day 7, the token can become reusable on day 30 while 30-day post-completion run retention could keep the run until day 37.
    • The alternative is to start the expiration period when the workflow ends. That gives the full configured period after completion, but differs from common idempotency semantics and makes the final time unknown at admission.
    • A user could try to approximate indefinite protection by keeping a workflow open with a very long sleep(). Should we explicitly discourage that, or eventually support an intentional indefinite mode?

Verification

  • merged and conflict-resolved against origin/main at 5de1b7a10
  • builds: @workflow/world, @workflow/core, and workflow
  • tests: 1,450 @workflow/core tests, 236 @workflow/world-vercel tests, and 8 combined @workflow/world event-schema tests
  • docs: 54 filtered checks pass; 20 intentionally skipped samples remain skipped

Docs Preview

Page Preview
createHook() expiration Open
Building a World Open
Hook conflicts Open
Run idempotency Open
Hook lifecycle Open

@changeset-bot

changeset-bot Bot commented Jul 9, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 8b75665

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

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

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel

vercel Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

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

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

@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

Passed Failed Skipped Total
✅ ▲ Vercel Production 1453 0 230 1683
✅ 💻 Local Development 1617 0 219 1836
✅ 📦 Local Production 1617 0 219 1836
✅ 🐘 Local Postgres 1617 0 219 1836
✅ 🪟 Windows 153 0 0 153
✅ 📋 Other 894 0 177 1071
Total 7351 0 1064 8415

Details by Category

✅ ▲ Vercel Production
App Passed Failed Skipped
✅ astro 126 0 27
✅ example 126 0 27
✅ express 126 0 27
✅ fastify 126 0 27
✅ hono 126 0 27
✅ nextjs-turbopack 150 0 3
✅ nextjs-webpack 150 0 3
✅ nitro 126 0 27
✅ nuxt 126 0 27
✅ sveltekit 145 0 8
✅ vite 126 0 27
✅ 💻 Local Development
App Passed Failed Skipped
✅ astro-stable 128 0 25
✅ express-stable 128 0 25
✅ fastify-stable 128 0 25
✅ hono-stable 128 0 25
✅ nextjs-turbopack-canary 134 0 19
✅ nextjs-turbopack-stable 153 0 0
✅ nextjs-webpack-canary 134 0 19
✅ nextjs-webpack-stable 153 0 0
✅ nitro-stable 128 0 25
✅ nuxt-stable 128 0 25
✅ sveltekit-stable 147 0 6
✅ vite-stable 128 0 25
✅ 📦 Local Production
App Passed Failed Skipped
✅ astro-stable 128 0 25
✅ express-stable 128 0 25
✅ fastify-stable 128 0 25
✅ hono-stable 128 0 25
✅ nextjs-turbopack-canary 134 0 19
✅ nextjs-turbopack-stable 153 0 0
✅ nextjs-webpack-canary 134 0 19
✅ nextjs-webpack-stable 153 0 0
✅ nitro-stable 128 0 25
✅ nuxt-stable 128 0 25
✅ sveltekit-stable 147 0 6
✅ vite-stable 128 0 25
✅ 🐘 Local Postgres
App Passed Failed Skipped
✅ astro-stable 128 0 25
✅ express-stable 128 0 25
✅ fastify-stable 128 0 25
✅ hono-stable 128 0 25
✅ nextjs-turbopack-canary 134 0 19
✅ nextjs-turbopack-stable 153 0 0
✅ nextjs-webpack-canary 134 0 19
✅ nextjs-webpack-stable 153 0 0
✅ nitro-stable 128 0 25
✅ nuxt-stable 128 0 25
✅ sveltekit-stable 147 0 6
✅ vite-stable 128 0 25
✅ 🪟 Windows
App Passed Failed Skipped
✅ nextjs-turbopack 153 0 0
✅ 📋 Other
App Passed Failed Skipped
✅ e2e-local-dev-nest-stable 128 0 25
✅ e2e-local-dev-tanstack-start- 128 0 25
✅ e2e-local-postgres-nest-stable 128 0 25
✅ e2e-local-postgres-tanstack-start- 128 0 25
✅ e2e-local-prod-nest-stable 128 0 25
✅ e2e-local-prod-tanstack-start- 128 0 25
✅ e2e-vercel-prod-tanstack-start 126 0 27

📋 View full workflow run

@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

📊 Workflow Benchmarks

commit 8b75665 · Fri, 10 Jul 2026 22:36:16 GMT · run logs

Backend: vercel · app: nextjs-turbopack

Metric Scenario Avg (ms) P75 (ms) P90 (ms) P99 (ms) Samples
TTFS stream 1294 (-8.2%) 1668 🔴 1737 🔴 2142 🔴 30
TTFS hook + stream 1529 (-6.1%) 1946 🔴 2035 🔴 2282 🔴 30
STSO 1020 steps (1-20) 289 (+2.6%) 320 🔴 430 🔴 562 🔴 19
STSO 1020 steps (101-120) 438 (+8.3%) 441 🔴 630 🔴 745 🔴 19
STSO 1020 steps (1001-1020) 939 (+15%) 1021 🔴 1050 🔴 1170 🔴 19
WO stream 1294 (-8.2%) 1668 1737 2142 30
WO hook + stream 1529 (-6.1%) 1946 2035 2282 30
SL stream 4442 (-12%) 5604 🔴 5698 🔴 5892 🔴 30
SL hook + stream 5062 (+5.7%) 5733 🔴 5833 🔴 6029 🔴 30
📜 Previous results (5)

0deba85

Fri, 10 Jul 2026 21:46:05 GMT · run logs

vercel / nextjs-turbopack

Metric Scenario Avg (ms) P75 (ms) P90 (ms) P99 (ms) Samples
TTFS stream 1091 (-6.1%) 1578 🔴 1625 🔴 1904 🔴 30
TTFS hook + stream 1591 (+26%) 1903 🔴 1999 🔴 2147 🔴 30
STSO 1020 steps (1-20) 461 (+83%) 312 🔴 536 🔴 3858 🔴 19
STSO 1020 steps (101-120) 385 (-21%) 429 🔴 491 🔴 497 🔴 19
STSO 1020 steps (1001-1020) 858 (-0.8%) 888 🔴 991 🔴 1147 🔴 19
WO stream 1091 (-6.1%) 1578 1625 1904 30
WO hook + stream 1591 (+26%) 1903 1999 2147 30
SL stream 4415 (-11%) 5516 🔴 5632 🔴 5839 🔴 30
SL hook + stream 4685 (-4.8%) 5419 🔴 5536 🔴 5708 🔴 30

9d60f99

Fri, 10 Jul 2026 21:13:57 GMT · run logs

vercel / nextjs-turbopack

Metric Scenario Avg (ms) P75 (ms) P90 (ms) P99 (ms) Samples
TTFS stream 1289 (+11%) 1673 🔴 1811 🔴 2253 🔴 30
TTFS hook + stream 1740 (+37%) 2070 🔴 2163 🔴 2290 🔴 30
STSO 1020 steps (1-20) 288 (+14%) 309 🔴 357 🔴 605 🔴 19
STSO 1020 steps (101-120) 439 (-10%) 530 🔴 566 🔴 586 🔴 19
STSO 1020 steps (1001-1020) 881 (+1.9%) 877 🔴 913 🔴 2021 🔴 19
WO stream 1289 (+11%) 1673 1811 2253 30
WO hook + stream 1740 (+37%) 2070 2163 2290 30
SL stream 4636 (-6.2%) 5678 🔴 5831 🔴 6631 🔴 30
SL hook + stream 4773 (-3.0%) 5128 🔴 5784 🔴 6229 🔴 30

7549626

Fri, 10 Jul 2026 19:21:05 GMT · run logs

vercel / nextjs-turbopack

Metric Scenario Avg (ms) P75 (ms) P90 (ms) P99 (ms) Samples
TTFS stream 1156 (-20%) 1668 🔴 1724 🔴 2167 🔴 30
TTFS hook + stream 1605 (±0%) 1951 🔴 2027 🔴 2167 🔴 30
STSO 1020 steps (1-20) 238 (-17%) 256 🔴 347 🔴 350 🔴 19
STSO 1020 steps (101-120) 388 (-7.4%) 392 🔴 508 🔴 620 🔴 19
STSO 1020 steps (1001-1020) 884 (-6.3%) 947 🔴 1194 🔴 1224 🔴 19
WO stream 1156 (-20%) 1668 1724 2167 30
WO hook + stream 1605 (±0%) 1951 2027 2167 30
SL stream 4321 (-13%) 5716 🔴 5802 🔴 6123 🔴 30
SL hook + stream 4948 (+2.1%) 5569 🔴 5689 🔴 5804 🔴 30

bd33c10

Thu, 09 Jul 2026 23:37:25 GMT · run logs

vercel / nextjs-turbopack

Metric Scenario Avg (ms) P75 (ms) P90 (ms) P99 (ms) Samples
TTFS stream 1345 (+21%) 1588 🔴 1608 🔴 2139 🔴 30
TTFS hook + stream 1439 (+12%) 1784 🔴 1842 🔴 2069 🔴 30
STSO 1020 steps (1-20) 245 (-9.7%) 277 🔴 314 🔴 321 🔴 19
STSO 1020 steps (101-120) 399 (-3.0%) 454 🔴 501 🔴 515 🔴 19
STSO 1020 steps (1001-1020) 836 (-4.3%) 899 🔴 979 🔴 1002 🔴 19
WO stream 1345 (+21%) 1588 1608 2139 30
WO hook + stream 1439 (+12%) 1784 1842 2069 30
SL stream 4712 (+0.8%) 5007 🔴 5520 🔴 5781 🔴 30
SL hook + stream 4871 (-3.2%) 5424 🔴 5618 🔴 5740 🔴 30

87b6bd9

Thu, 09 Jul 2026 22:13:25 GMT · run logs

vercel / nextjs-turbopack

Metric Scenario Avg (ms) P75 (ms) P90 (ms) P99 (ms) Samples
TTFS stream 1262 (+26%) 1639 🔴 1661 🔴 2212 🔴 30
TTFS hook + stream 1557 (+24%) 1865 🔴 1929 🔴 3542 🔴 30
STSO 1020 steps (1-20) 292 (+22%) 344 🔴 387 🔴 443 🔴 19
STSO 1020 steps (101-120) 392 (-3.2%) 437 🔴 466 🔴 489 🔴 19
STSO 1020 steps (1001-1020) 850 (-18%) 868 🔴 1021 🔴 1104 🔴 19
WO stream 1262 (+26%) 1639 1661 2212 30
WO hook + stream 1557 (+24%) 1865 1929 3542 30
SL stream 4986 (+4.3%) 5576 🔴 5631 🔴 5838 🔴 30
SL hook + stream 4796 (-4.0%) 5397 🔴 5506 🔴 5985 🔴 30

Avg deltas compare against the most recent benchmark run on main at the time of this run.

Metrics — TTFS: time to first step body execution · STSO: step-to-step overhead (gap between consecutive step bodies) · WO: workflow overhead (time outside step bodies, client start → last step body exit) · SL: stream latency (first chunk write → visible to the reader)

Scenarios — stream: one step that streams chunks back to the client; no hooks, so the run stays in turbo mode · hook + stream: registers a hook before the same streaming step, which exits turbo mode · 1020 steps: 1020 trivial sequential steps; STSO is measured between consecutive steps in the given step ranges

🟢/🔴 mark percentiles within/above target. Targets (p75/p90/p99, ms) — TTFS 200/300/600 · SL 50/60/125 · STSO (1-20) 20/30/60 · STSO (101-120) 30/45/90 · STSO (1001-1020) 40/60/120

TTFS/WO compare client vs deployment clocks and SL compares the step runner’s clock vs the client’s (NTP-synced in CI). WO ends at the last step body exit, the closest observable proxy for the final step-completion request.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant