Skip to content

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

Open
NathanColosimo wants to merge 22 commits into
mainfrom
codex/hook-retention-core
Open

feat(core): add experimental Hook token expiration#2865
NathanColosimo wants to merge 22 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: ecd2c61

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 13, 2026 5:57pm
example-nextjs-workflow-webpack Ready Ready Preview, Comment Jul 13, 2026 5:57pm
example-workflow Ready Ready Preview, Comment Jul 13, 2026 5:57pm
workbench-astro-workflow Ready Ready Preview, Comment Jul 13, 2026 5:57pm
workbench-express-workflow Ready Ready Preview, Comment Jul 13, 2026 5:57pm
workbench-fastify-workflow Ready Ready Preview, Comment Jul 13, 2026 5:57pm
workbench-hono-workflow Ready Ready Preview, Comment Jul 13, 2026 5:57pm
workbench-nitro-workflow Ready Ready Preview, Comment Jul 13, 2026 5:57pm
workbench-nuxt-workflow Ready Ready Preview, Comment Jul 13, 2026 5:57pm
workbench-sveltekit-workflow Ready Ready Preview, Comment Jul 13, 2026 5:57pm
workbench-tanstack-start-workflow Ready Ready Preview, Comment Jul 13, 2026 5:57pm
workbench-vite-workflow Ready Ready Preview, Comment Jul 13, 2026 5:57pm
workflow-docs Ready Ready Preview, Comment, Open in v0 Jul 13, 2026 5:57pm
workflow-swc-playground Ready Ready Preview, Comment Jul 13, 2026 5:57pm
workflow-tarballs Ready Ready Preview, Comment Jul 13, 2026 5:57pm
workflow-web Ready Ready Preview, Comment Jul 13, 2026 5:57pm

@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

Passed Failed Skipped Total
❌ ▲ Vercel Production 1451 2 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 892 2 177 1071
Total 7347 4 1064 8415

❌ Failed Tests

▲ Vercel Production (2 failed)

example (1 failed):

  • hookClaimOnlyMutexWorkflow - hook works as a pure run mutex without payload data | wrun_01KXEAA0XWH2ECKCCBREHAR68B | 🔍 observability

sveltekit (1 failed):

  • stepDirectCallWorkflow - calling step functions directly outside workflow context
📋 Other (2 failed)

e2e-vercel-prod-tanstack-start (2 failed):

  • outputStreamWorkflow positive startIndex (skips first chunk)
  • stepDirectCallWorkflow - calling step functions directly outside workflow context

Details by Category

❌ ▲ Vercel Production
App Passed Failed Skipped
✅ astro 126 0 27
❌ example 125 1 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 144 1 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 124 2 27

📋 View full workflow run


Some E2E test jobs failed:

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

Check the workflow run for details.

@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

📊 Workflow Benchmarks

commit ecd2c61 · Mon, 13 Jul 2026 18:14:24 GMT · run logs

Backend: vercel · app: nextjs-turbopack

Metric Scenario Avg (ms) P75 (ms) P90 (ms) P99 (ms) Samples
TTFS stream 1428 (+18%) 1729 🔴 1750 🔴 1898 🔴 30
TTFS hook + stream 1631 (+8.2%) 1973 🔴 2070 🔴 2212 🔴 30
STSO 1020 steps (1-20) 286 (+5.2%) 294 🔴 407 🔴 647 🔴 19
STSO 1020 steps (101-120) 408 (+0.9%) 420 🔴 489 🔴 536 🔴 19
STSO 1020 steps (1001-1020) 851 (-6.1%) 895 🔴 948 🔴 952 🔴 19
WO stream 1428 (+18%) 1729 1750 1898 30
WO hook + stream 1631 (+8.2%) 1973 2070 2212 30
SL stream 4836 (+3.6%) 5125 🔴 5644 🔴 5824 🔴 30
SL hook + stream 4896 (-3.3%) 5456 🔴 5647 🔴 5788 🔴 30
📜 Previous results (6)

8b75665

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

vercel / 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

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