Skip to content

[core][world][world-vercel] Add World.createRunId() and region-aware queue routing - #1981

Merged
TooTallNate merged 35 commits into
mainfrom
world-create-run-id
Jul 13, 2026
Merged

[core][world][world-vercel] Add World.createRunId() and region-aware queue routing#1981
TooTallNate merged 35 commits into
mainfrom
world-create-run-id

Conversation

@TooTallNate

@TooTallNateTooTallNate commented May 13, 2026

Copy link
Copy Markdown
Member

Closes#1931 and helps with #2708 (and possibly more)

Wires the tagged-ULID encoding introduced in #1978 into the workflow run lifecycle:

  1. @workflow/world — adds two extension points to the World interface:

    • createRunId?(input?: Record<string, unknown>): string — opt-in hook for worlds to mint custom run IDs (the returned value is the bare ULID; the core attaches the wrun_ prefix).
    • QueueOptions.region?: string — per-message routing hint that worlds can use to dispatch messages to a specific region.
  2. @workflow/corestart() now:

    • Accepts a new region option (StartOptionsBase.region).
    • Calls world.createRunId(opts) when the world provides one (forwarding the full options bag so worlds can read whichever fields they recognise), falling back to a monotonic ULID otherwise.
    • Threads opts.region onto the queue options so the initial workflow message is dispatched to the matching region.
  3. @workflow/world-vercel — implements both extension points:

    • createRunId(options?) mints region-tagged ULIDs. Region resolution order: options.regionVERCEL_REGION env var → iad1 default. A run ID is always tagged with a concrete, routable region; the unknown (0) sentinel is never minted.
    • The queue resolves its destination region from (in order): an explicit opts.region, the region embedded in the payload's tagged run ID, the VERCEL_REGION env var, and finally a hardcoded iad1 default — replacing the previous unconditional 'iad1' passed to @vercel/queue. In proxy mode the resolved region is forwarded as a routing header.

Monotonicity

encode() writes the 11 bits of region/version metadata into the top of the ULID's randomness section (all of byte 6 + the top 3 bits of byte 7), leaving the low 69 bits untouched. The underlying ulid monotonic factory's same-millisecond bottom-bit increments therefore survive encoding, and consecutive IDs with the same region/version metadata are naturally monotonic — no correction needed in the common case.

The one remaining hazard is the metadata changing between same-millisecond calls (a lower-numbered region sorts below a higher one at the same timestamp). For that, createRunId tracks the last emitted ID and, on collision, bumps the bit immediately above the metadata window (the lowest bit of the 48-bit timestamp) before re-stamping the requested region/version. The test suite covers 4096 same-ms mints plus same-ms region changes.

E2E coverage

Adds packages/core/e2e/e2e-region.test.ts (CI job e2e-vercel-multi-region, deployed to all 19 routable regions):

  • Explicit start({ region }) for iad1 / sfo1 / fra1, verifying run-ID tags, data placement, and execution region.
  • A 9-way and an all-19-regions concurrent batch verifying region isolation under parallel starts.
  • Implicit region tagging via region-pinned routes for each of the 19 regions (no explicit region option — VERCEL_REGION drives the tag).
  • A cross-region durable stream test (writer in iad1, mid-stream reader in sfo1).

Execution-region assertions use a small adjacency tolerance for geo-elastic execution (e.g. kix1 → hnd1); run-ID tags and data placement are always asserted strictly.

Encodes a tag bit, 5-bit version, and 6-bit Vercel region ID into a
ULID-shaped string used for workflow run IDs. Tagged values remain
valid 26-char Crockford-Base32 ULIDs so they still sort and round-trip
through any system that accepts ULIDs.
Add exact-string expectations for encoded outputs at known inputs,
covering the default region/version pair, numeric region IDs, version
overrides, boundary values (all-zero, all-max), the dirty-input
overwrite case, and the lexicographic-order checks. Also adds an
explicit byte-array expectation for the canonical ULID-spec example
string and an additional first-char-range coverage test for isTagged.
- isTaggedString now fully validates the input as a 26-char Crockford
Base32 ULID (delegating to ulidToBytes) instead of only inspecting
the first character. This fixes false positives on inputs like
'4UUUU...' that have a valid tag-bit position but invalid chars
later in the string.
- isTagged() now accepts `unknown` to match its documented behavior
of safely rejecting non-string inputs without requiring callers to
cast.
- Introduce `RegionKey` for the full set of keys including 'unknown',
and narrow `RegionCode` to `Exclude<RegionKey, 'unknown'>` so the
return type of `lookupRegion` and the `DecodedRunId.region` field
accurately reflect that 'unknown' is never produced. Updates
`encode` to reject 'unknown' as a region code string at runtime
(callers wanting the unknown sentinel should pass numeric 0).
…e queue routing
- @workflow/world: add optional createRunId(input?) to the World
interface so worlds can mint run IDs with embedded metadata, and
add an optional 'region' field to QueueOptions for per-message
routing hints.
- @workflow/core: start() now delegates run ID generation to
world.createRunId() when defined (falling back to a monotonic
ULID otherwise), and accepts a new 'runIdInput' option that is
forwarded verbatim to createRunId. When runIdInput.region is a
string, it is also threaded onto the queue options so the initial
workflow message is dispatched to the matching region.
- @workflow/world-vercel: implement createRunId() to mint
region-tagged ULIDs, preferring an explicit runIdInput.region and
falling back to the VERCEL_REGION env var. The queue now resolves
its destination region from (in order): an explicit opts.region,
the region embedded in the payload's tagged run ID, the
VERCEL_REGION env var, and finally a hardcoded 'iad1' default.
This replaces the previous unconditional 'iad1' region passed to
the @vercel/queue client.
Monotonicity within a process is preserved by tracking the last
emitted run ID and bumping the bit immediately above the 11-bit
metadata window when a same-ms collision would otherwise occur,
then re-stamping the requested region/version on top so metadata
remains stable.
@TooTallNate
TooTallNate requested a review from a team as a code ownerMay 13, 2026 17:47
CopilotAI review requested due to automatic review settings May 13, 2026 17:47
@vercel

vercelBot commented May 13, 2026

Copy link
Copy Markdown
Contributor

@changeset-bot

changeset-botBot commented May 13, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 4f77bc1

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

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

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

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production145302301683
✅ 💻 Local Development161702191836
✅ 📦 Local Production161702191836
✅ 🐘 Local Postgres161702191836
✅ 🪟 Windows15300153
✅ 📋 Other89401771071
✅ vercel-multi-region270027
Total7378010648442

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro126027
✅ example126027
✅ express126027
✅ fastify126027
✅ hono126027
✅ nextjs-turbopack15003
✅ nextjs-webpack15003
✅ nitro126027
✅ nuxt126027
✅ sveltekit14508
✅ vite126027
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack15300
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable128025
✅ e2e-local-dev-tanstack-start-128025
✅ e2e-local-postgres-nest-stable128025
✅ e2e-local-postgres-tanstack-start-128025
✅ e2e-local-prod-nest-stable128025
✅ e2e-local-prod-tanstack-start-128025
✅ e2e-vercel-prod-tanstack-start126027
✅ vercel-multi-region
AppPassedFailedSkipped
✅ nextjs-turbopack2700

📋 View full workflow run

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR extends the workflow runtime/world contract to support (1) world-minted run IDs and (2) region-aware queue routing, and wires those capabilities through @workflow/core and the Vercel world implementation.

Changes:

  • Add optional World.createRunId(input?) and QueueOptions.region in @workflow/world.
  • Update @workflow/corestart() to delegate run ID generation to world.createRunId(opts.runIdInput) when available, and to forward runIdInput.region to queue options.
  • Implement region-tagged run IDs + region-resolving queue routing in @workflow/world-vercel, including new tests and a new ulid dependency.

Reviewed changes

Copilot reviewed 13 out of 14 changed files in this pull request and generated 4 comments.

Show a summary per file
FileDescription
pnpm-lock.yamlAdds lockfile entry for ulid used by world-vercel.
packages/world/src/queue.tsExtends QueueOptions with a region routing hint + docs.
packages/world/src/interfaces.tsExtends World interface with optional createRunId.
packages/world-vercel/src/queue.tsAdds region resolution order (opts → tagged runId → env → fallback) and updates CBOR encode/decode imports.
packages/world-vercel/src/queue.test.tsAdds region-routing test coverage.
packages/world-vercel/src/index.tsWires createRunId into createVercelWorld and re-exports it.
packages/world-vercel/src/create-run-id.tsImplements region-tagged ULID minting with per-process monotonicity.
packages/world-vercel/src/create-run-id.test.tsTests for env/input fallback and monotonicity behavior.
packages/world-vercel/package.jsonAdds ulid dependency.
packages/core/src/runtime/start.tsUses world.createRunId when available; forwards runIdInput.region into queue options.
packages/core/src/runtime/start.test.tsAdds tests validating createRunId usage and region threading.
.changeset/world-vercel-create-run-id.mdRelease notes for new run ID minting + routing behavior in world-vercel.
.changeset/world-create-run-id.mdRelease notes for new @workflow/world interface fields.
.changeset/core-run-id-input.mdRelease notes for core start() runIdInput behavior.
Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

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

Comment threadpackages/world/src/queue.ts Outdated
Comment threadpackages/world-vercel/src/queue.ts
Comment threadpackages/world-vercel/src/queue.test.ts Outdated
Comment threadpackages/world-vercel/src/create-run-id.test.ts Outdated
…unId
Drop the dedicated 'runIdInput' field on StartOptions and forward the
entire options bag to world.createRunId() instead. This keeps the
public API surface smaller and lets each World pick the fields it
recognises (e.g. world-vercel reads 'region'). The top-level 'region'
option remains on StartOptionsBase and is also threaded onto the
queue's per-call region opt when set.
…ride
- queue: validate opts.region and VERCEL_REGION against the known region
table before routing, ignoring unrecognised codes so a bad override
can't clobber the payload-derived region (Copilot)
- add isKnownRegionCode() runtime guard to run-id/regions
- reset WORKFLOW_SERVER_URL_OVERRIDE to '' (must be empty on main)
- fold the within-PR iad1-default changeset into the main world-vercel
changeset and delete it (review)
- start.test: declare specVersion on createRunId mock worlds now that
the merged world-compatibility check requires it
- cover the new region-validation fall-through paths in queue.test
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@VaguelySerious

Copy link
Copy Markdown
Member

(AI) Deleted .changeset/run-id-default-iad1.md and folded its net user-facing behavior (the iad1 default when no region/env is available) into the main world-vercel-create-run-id changeset so the changelog stays accurate without the within-PR intermediate note.

…i-region preview
BRANCH-ONLY — revert the override to '' before merge (lint enforces).
Points this PR's e2e/benchmark runs at the wave-1 multi-region
workflow-server preview (vercel/workflow-server#590: iad1+sfo1+fra1
serving, staging data backends) so region-tagged runs are validated
against real multi-region serving end-to-end.
Also makes the unit-test mock origins in events-v4.test.ts and
trace-propagation.test.ts override-aware (same pattern the rest of
the file and utils.test.ts already use), so the suite passes whether
or not the override is set — these two files were the only spots
hardcoding https://vercel-workflow.com.
Conflicts: packages/world-vercel/src/index.ts — both sides added
imports/exports (createRunId from this branch, createAnalytics from
main #2698); kept both. Includes the TypeScript 6 workspace upgrade
(#2700) from main. world-vercel (249) and core (1356) tests pass.
Adds a dedicated e2e suite validating @workflow/world-vercel region
routing end to end, run as its own CI job (e2e-vercel-multi-region)
against the nextjs-turbopack workbench only — deliberately separate
from e2e.test.ts, which runs as a matrix across all worlds/frameworks
where Vercel-specific multi-region behavior doesn't apply.
- workbench/nextjs-turbopack/vercel.json: deploy to iad1+sfo1+fra1 so
region-routed flow messages have a function to land on in each region.
- workflows/99_e2e.ts: regionProbeWorkflow returns the VERCEL_REGION
observed by both the workflow and a step, so tests can assert the run
EXECUTED in the intended region (not just that it was tagged).
- packages/core/e2e/e2e-region.test.ts: per-region cases assert
1) start(..., { region }) mints a region-tagged run ID (decoded via
@workflow/world-vercel/run-id),
2) the workflow + step both observed VERCEL_REGION === region,
3) the server reports the run completed;
plus a concurrent all-regions case guarding against cross-region
misrouting under simultaneous multi-region traffic. Skips on local
deployments.
- .github/workflows/tests.yml: new e2e-vercel-multi-region job
mirroring e2e-vercel-prod's env/deployment-wait, running only the
new suite.
The first multi-region CI run surfaced two issues:
1. sfo1/fra1-tagged runs executed in iad1. The suite started runs from
the external test process, which uses the api.vercel.com token proxy
— and the proxy's queues path forwards every send to the region-less
VQS host (the world's proxy-mode resolveBaseUrl ignores the region
argument, and the proxy's x-vercel-vqs-api-url escape hatch only
allowlists vqs-server-*.vercel.sh preview hosts). Production traffic
publishes IN-FUNCTION (direct regional queue routing), so the suite
now triggers start() through a new workbench route
(/api/e2e-region-start) and rehydrates the run with getRun() —
testing the path production actually takes. Proxy-mode regional
queue routing is a known gap to address separately in api-workflow.
2. TypeError on world.runs.get: getWorld() is async and was called
without await.
Textually clean but semantically conflicting with #2789: main added the
proxy-mode x-vercel-queue-region header in createQueue's shared
clientOptions, referencing the static region const this branch removed
in favor of per-send resolution. Resolution: the header now rides the
per-send QueueClient construction, carrying resolveTargetRegion's
result — proxied sends route to the region resolved per message
(opts.region -> run-ID tag -> VERCEL_REGION -> iad1).
world-vercel: 253/253 (includes #2789's proxy-header tests, which hold
under per-send resolution).
…ion suite
With regional VQS routing now working through the api.vercel.com proxy
(vercel/api#79056 + #2789 + this branch's per-send region resolution),
the suite covers both start configurations, asserting the same three
properties for each (region-tagged run ID, execution in the intended
region via VERCEL_REGION echoed in the return value, server-side
completion):
1. EXPLICIT: start(..., { region }) called directly in the vitest
runner — publishes through the token proxy, per-send region carried
by x-vercel-queue-region. Restores the direct-start shape the suite
had originally, plus the concurrent all-regions case.
2. IMPLICIT: dedicated per-region workbench routes
(/api/e2e-region-implicit/{iad1,sfo1,fra1}), each pinned to a single
region via a per-function 'regions' entry in the workbench
vercel.json, calling start() with NO region option — createRunId
derives the tag from the minting function's VERCEL_REGION. The test
also asserts the route reported executing in its pinned region, so
the implicit-tagging assertion can't pass vacuously.
Replaces the interim /api/e2e-region-start route (explicit region via
request body), which existed to work around the pre-#79056 proxy gap.
…ng is in production
workflow-server#590 (iad1+sfo1+fra1 serving) merged and deployed to
production and the e2e backend, so this branch's e2e/benchmark runs no
longer need to target the wave-1 preview. Restores the empty override
the No Test Overrides lint job enforces for merge.
The override-aware unit-test origins (events-v4/trace-propagation)
stay — they are correct under any override value.
@saasjesus

Copy link
Copy Markdown

damn its happening

Regression coverage for a backend bug that made cross-region stream
reads report zero chunks on IN-PROGRESS streams (completed streams were
unaffected), which forced the multi-region serving rollback.
The new case exercises exactly that geometry:
- crossRegionStreamWorkflow (99_e2e.ts) writes N chunks to the default
output stream, then holds the stream OPEN for 45s before closing —
the in-progress window is the point, since completed streams are the
easy case.
- The e2e starts it with region iad1, waits (same-region, via the
api.vercel.com proxy) until all chunks are written, asserts the run
is still 'running', then reads through a new sfo1-pinned workbench
route (/api/e2e-stream-read/sfo1) that returns getTailIndex() plus
its VERCEL_REGION. The reader's region served none of the stream's
writes, so the reported chunk count must come from the backend's
cross-region stream metadata. The test fails loudly if the route
isn't actually executing in sfo1.
Also bumps the explicit-region test timeout to 120s: the first case in
the file absorbs every cold start at once (fresh workbench instances
in up to three regions plus a cold backend preview) and was observed
just over the 60s default.
BRANCH-ONLY (revert before merge, lint enforces):
WORKFLOW_SERVER_URL_OVERRIDE points at a multi-region backend preview
that includes the fix, so this validates cross-region stream
visibility end-to-end before multi-region serving is re-enabled.
Points the suite at an all-regions backend preview and widens coverage
from the wave-1 trio to every provisioned region:
- Explicit path: a single concurrent all-regions case starts one
tagged run per region (one shared cold-start window instead of 19
sequential ones) and aggregates per-region failures so a single
region's breakage reports alongside the full picture. The trio keeps
its detailed per-region cases and the 9-way concurrent-isolation
case.
- Implicit path: workbench gains a region-pinned
/api/e2e-region-implicit/<region> route per provisioned region (19
total, shared handler), the workbench itself now deploys to all of
them, and the test.each covers the full set with per-case timeouts
for regional cold starts.
- Multi-region CI job timeout 20m -> 35m for the sequential implicit
cases.
BRANCH-ONLY (revert before merge, lint enforces):
WORKFLOW_SERVER_URL_OVERRIDE now targets the all-regions backend
preview instead of the previous (stale, since-merged) fix preview.
The first all-regions run surfaced a subtle execution-locality
behavior: queue delivery is guaranteed to the tagged region's
dataplane and the delivery callback egresses from that region, but the
consumer invocation's execution region is chosen by where that
callback enters Vercel's edge — and adjacent regions can geo-resolve
to each other's functions. Observed live: kix1-tagged runs (callback
egressing from Osaka) deterministically executing in hnd1/Tokyo on
both the explicit and implicit paths, with tagging, data placement,
and completion all still strictly kix1.
expectRunInRegion now asserts execution lands in the tagged region OR
one of its geographic neighbors (EXECUTION_ADJACENCY), while run-ID
tagging and server-side completion remain strictly the requested
region. Gross misrouting (e.g. kix1 -> iad1) still fails.
… production
The all-regions workflow-server rollout is deployed and serving
production traffic from every Vercel region, so this branch's e2e no
longer needs to target a branch preview. Restores the empty override
the No Test Overrides lint enforces for merge.
With this the PR is complete: region-tagged run IDs, region-aware
queue routing, and the multi-region e2e suite (explicit + implicit +
all-regions + cross-region streams) all validate against the
production-default backends.

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 47 out of 48 changed files in this pull request and generated 3 comments.

Files not reviewed (1)
  • pnpm-lock.yaml: Generated file

Comment threadpackages/core/src/runtime/start.ts Outdated
Comment threadpackages/world-vercel/src/queue.ts
Comment threadpackages/world-vercel/src/events.ts Outdated
- start.ts: StartOptionsBase.region fallback is iad1, not the unknown
sentinel (createRunId always mints a concrete routable region)
- queue.ts: example used a nonexistent start({ runIdInput }) API; the
real option is start({ region })
- events.ts: decode() clears only the tag bit (top bit of the 48-bit
timestamp field) — it does not restore the original untagged ULID;
reword to say what actually matters for timestamp validation
Hooks are resolved by opaque token, which carries no region hint, so
lookup and resume must work regardless of which region owns the run's
data. Exercises the full follow-up-message path on sfo1- and
fra1-tagged runs: create inside the workflow, resolve by token from
the test process, resume twice sequentially, and assert payload order
and completion.
Regression coverage for the failure mode where the first message to a
hook-driven app on a non-iad1 run worked but every follow-up failed
with 'Hook not found'.
Conflict in packages/core/src/runtime/start.test.ts: both sides
appended a new top-level describe block at the same location — this
branch's 'createRunId' tests and main's 'queue namespace' tests.
Resolved by keeping both blocks verbatim (each is self-contained with
its own beforeEach/mocks). Lockfile reconciled with pnpm install.
Verified post-merge: core tsc clean, start/helpers tests 73 passed,
world-vercel 263 passed, full build 27/27.
@github-actionsgithub-actionsBot mentioned this pull request Jul 13, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Backport to stable failed for 9da2d76 due to a workflow error (backport job run).

This is usually an infrastructure problem (e.g. the configured AI model could not be found, an AI Gateway error, or an opencode crash) rather than a merge conflict. Check the job logs linked above for details.

Once the underlying issue is fixed, re-run the Backport to stable workflow manually via workflow_dispatch and paste this commit SHA into the ref input:

9da2d762604c2b73eb39f07fc0b069aea643e18d

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.

Support multi-region on Vercel backend

4 participants

@TooTallNate@VaguelySerious@saasjesus
, '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" + '
[core][world][world-vercel] Add `World.createRunId()` and region-aware queue routing by TooTallNate · Pull Request #1981 · vercel/workflow · GitHub
Skip to content

[core][world][world-vercel] Add World.createRunId() and region-aware queue routing - #1981

Merged
TooTallNate merged 35 commits into
mainfrom
world-create-run-id
Jul 13, 2026
Merged

[core][world][world-vercel] Add World.createRunId() and region-aware queue routing#1981
TooTallNate merged 35 commits into
mainfrom
world-create-run-id

Conversation

@TooTallNate

@TooTallNateTooTallNate commented May 13, 2026

Copy link
Copy Markdown
Member

Closes#1931 and helps with #2708 (and possibly more)

Wires the tagged-ULID encoding introduced in #1978 into the workflow run lifecycle:

  1. @workflow/world — adds two extension points to the World interface:

    • createRunId?(input?: Record<string, unknown>): string — opt-in hook for worlds to mint custom run IDs (the returned value is the bare ULID; the core attaches the wrun_ prefix).
    • QueueOptions.region?: string — per-message routing hint that worlds can use to dispatch messages to a specific region.
  2. @workflow/corestart() now:

    • Accepts a new region option (StartOptionsBase.region).
    • Calls world.createRunId(opts) when the world provides one (forwarding the full options bag so worlds can read whichever fields they recognise), falling back to a monotonic ULID otherwise.
    • Threads opts.region onto the queue options so the initial workflow message is dispatched to the matching region.
  3. @workflow/world-vercel — implements both extension points:

    • createRunId(options?) mints region-tagged ULIDs. Region resolution order: options.regionVERCEL_REGION env var → iad1 default. A run ID is always tagged with a concrete, routable region; the unknown (0) sentinel is never minted.
    • The queue resolves its destination region from (in order): an explicit opts.region, the region embedded in the payload's tagged run ID, the VERCEL_REGION env var, and finally a hardcoded iad1 default — replacing the previous unconditional 'iad1' passed to @vercel/queue. In proxy mode the resolved region is forwarded as a routing header.

Monotonicity

encode() writes the 11 bits of region/version metadata into the top of the ULID's randomness section (all of byte 6 + the top 3 bits of byte 7), leaving the low 69 bits untouched. The underlying ulid monotonic factory's same-millisecond bottom-bit increments therefore survive encoding, and consecutive IDs with the same region/version metadata are naturally monotonic — no correction needed in the common case.

The one remaining hazard is the metadata changing between same-millisecond calls (a lower-numbered region sorts below a higher one at the same timestamp). For that, createRunId tracks the last emitted ID and, on collision, bumps the bit immediately above the metadata window (the lowest bit of the 48-bit timestamp) before re-stamping the requested region/version. The test suite covers 4096 same-ms mints plus same-ms region changes.

E2E coverage

Adds packages/core/e2e/e2e-region.test.ts (CI job e2e-vercel-multi-region, deployed to all 19 routable regions):

  • Explicit start({ region }) for iad1 / sfo1 / fra1, verifying run-ID tags, data placement, and execution region.
  • A 9-way and an all-19-regions concurrent batch verifying region isolation under parallel starts.
  • Implicit region tagging via region-pinned routes for each of the 19 regions (no explicit region option — VERCEL_REGION drives the tag).
  • A cross-region durable stream test (writer in iad1, mid-stream reader in sfo1).

Execution-region assertions use a small adjacency tolerance for geo-elastic execution (e.g. kix1 → hnd1); run-ID tags and data placement are always asserted strictly.

Encodes a tag bit, 5-bit version, and 6-bit Vercel region ID into a
ULID-shaped string used for workflow run IDs. Tagged values remain
valid 26-char Crockford-Base32 ULIDs so they still sort and round-trip
through any system that accepts ULIDs.
Add exact-string expectations for encoded outputs at known inputs,
covering the default region/version pair, numeric region IDs, version
overrides, boundary values (all-zero, all-max), the dirty-input
overwrite case, and the lexicographic-order checks. Also adds an
explicit byte-array expectation for the canonical ULID-spec example
string and an additional first-char-range coverage test for isTagged.
- isTaggedString now fully validates the input as a 26-char Crockford
Base32 ULID (delegating to ulidToBytes) instead of only inspecting
the first character. This fixes false positives on inputs like
'4UUUU...' that have a valid tag-bit position but invalid chars
later in the string.
- isTagged() now accepts `unknown` to match its documented behavior
of safely rejecting non-string inputs without requiring callers to
cast.
- Introduce `RegionKey` for the full set of keys including 'unknown',
and narrow `RegionCode` to `Exclude<RegionKey, 'unknown'>` so the
return type of `lookupRegion` and the `DecodedRunId.region` field
accurately reflect that 'unknown' is never produced. Updates
`encode` to reject 'unknown' as a region code string at runtime
(callers wanting the unknown sentinel should pass numeric 0).
…e queue routing
- @workflow/world: add optional createRunId(input?) to the World
interface so worlds can mint run IDs with embedded metadata, and
add an optional 'region' field to QueueOptions for per-message
routing hints.
- @workflow/core: start() now delegates run ID generation to
world.createRunId() when defined (falling back to a monotonic
ULID otherwise), and accepts a new 'runIdInput' option that is
forwarded verbatim to createRunId. When runIdInput.region is a
string, it is also threaded onto the queue options so the initial
workflow message is dispatched to the matching region.
- @workflow/world-vercel: implement createRunId() to mint
region-tagged ULIDs, preferring an explicit runIdInput.region and
falling back to the VERCEL_REGION env var. The queue now resolves
its destination region from (in order): an explicit opts.region,
the region embedded in the payload's tagged run ID, the
VERCEL_REGION env var, and finally a hardcoded 'iad1' default.
This replaces the previous unconditional 'iad1' region passed to
the @vercel/queue client.
Monotonicity within a process is preserved by tracking the last
emitted run ID and bumping the bit immediately above the 11-bit
metadata window when a same-ms collision would otherwise occur,
then re-stamping the requested region/version on top so metadata
remains stable.
@TooTallNate
TooTallNate requested a review from a team as a code ownerMay 13, 2026 17:47
CopilotAI review requested due to automatic review settings May 13, 2026 17:47
@vercel

vercelBot commented May 13, 2026

Copy link
Copy Markdown
Contributor

@changeset-bot

changeset-botBot commented May 13, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 4f77bc1

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

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

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

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production145302301683
✅ 💻 Local Development161702191836
✅ 📦 Local Production161702191836
✅ 🐘 Local Postgres161702191836
✅ 🪟 Windows15300153
✅ 📋 Other89401771071
✅ vercel-multi-region270027
Total7378010648442

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro126027
✅ example126027
✅ express126027
✅ fastify126027
✅ hono126027
✅ nextjs-turbopack15003
✅ nextjs-webpack15003
✅ nitro126027
✅ nuxt126027
✅ sveltekit14508
✅ vite126027
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack15300
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable128025
✅ e2e-local-dev-tanstack-start-128025
✅ e2e-local-postgres-nest-stable128025
✅ e2e-local-postgres-tanstack-start-128025
✅ e2e-local-prod-nest-stable128025
✅ e2e-local-prod-tanstack-start-128025
✅ e2e-vercel-prod-tanstack-start126027
✅ vercel-multi-region
AppPassedFailedSkipped
✅ nextjs-turbopack2700

📋 View full workflow run

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR extends the workflow runtime/world contract to support (1) world-minted run IDs and (2) region-aware queue routing, and wires those capabilities through @workflow/core and the Vercel world implementation.

Changes:

  • Add optional World.createRunId(input?) and QueueOptions.region in @workflow/world.
  • Update @workflow/corestart() to delegate run ID generation to world.createRunId(opts.runIdInput) when available, and to forward runIdInput.region to queue options.
  • Implement region-tagged run IDs + region-resolving queue routing in @workflow/world-vercel, including new tests and a new ulid dependency.

Reviewed changes

Copilot reviewed 13 out of 14 changed files in this pull request and generated 4 comments.

Show a summary per file
FileDescription
pnpm-lock.yamlAdds lockfile entry for ulid used by world-vercel.
packages/world/src/queue.tsExtends QueueOptions with a region routing hint + docs.
packages/world/src/interfaces.tsExtends World interface with optional createRunId.
packages/world-vercel/src/queue.tsAdds region resolution order (opts → tagged runId → env → fallback) and updates CBOR encode/decode imports.
packages/world-vercel/src/queue.test.tsAdds region-routing test coverage.
packages/world-vercel/src/index.tsWires createRunId into createVercelWorld and re-exports it.
packages/world-vercel/src/create-run-id.tsImplements region-tagged ULID minting with per-process monotonicity.
packages/world-vercel/src/create-run-id.test.tsTests for env/input fallback and monotonicity behavior.
packages/world-vercel/package.jsonAdds ulid dependency.
packages/core/src/runtime/start.tsUses world.createRunId when available; forwards runIdInput.region into queue options.
packages/core/src/runtime/start.test.tsAdds tests validating createRunId usage and region threading.
.changeset/world-vercel-create-run-id.mdRelease notes for new run ID minting + routing behavior in world-vercel.
.changeset/world-create-run-id.mdRelease notes for new @workflow/world interface fields.
.changeset/core-run-id-input.mdRelease notes for core start() runIdInput behavior.
Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

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

Comment threadpackages/world/src/queue.ts Outdated
Comment threadpackages/world-vercel/src/queue.ts
Comment threadpackages/world-vercel/src/queue.test.ts Outdated
Comment threadpackages/world-vercel/src/create-run-id.test.ts Outdated
…unId
Drop the dedicated 'runIdInput' field on StartOptions and forward the
entire options bag to world.createRunId() instead. This keeps the
public API surface smaller and lets each World pick the fields it
recognises (e.g. world-vercel reads 'region'). The top-level 'region'
option remains on StartOptionsBase and is also threaded onto the
queue's per-call region opt when set.
…ride
- queue: validate opts.region and VERCEL_REGION against the known region
table before routing, ignoring unrecognised codes so a bad override
can't clobber the payload-derived region (Copilot)
- add isKnownRegionCode() runtime guard to run-id/regions
- reset WORKFLOW_SERVER_URL_OVERRIDE to '' (must be empty on main)
- fold the within-PR iad1-default changeset into the main world-vercel
changeset and delete it (review)
- start.test: declare specVersion on createRunId mock worlds now that
the merged world-compatibility check requires it
- cover the new region-validation fall-through paths in queue.test
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@VaguelySerious

Copy link
Copy Markdown
Member

(AI) Deleted .changeset/run-id-default-iad1.md and folded its net user-facing behavior (the iad1 default when no region/env is available) into the main world-vercel-create-run-id changeset so the changelog stays accurate without the within-PR intermediate note.

…i-region preview
BRANCH-ONLY — revert the override to '' before merge (lint enforces).
Points this PR's e2e/benchmark runs at the wave-1 multi-region
workflow-server preview (vercel/workflow-server#590: iad1+sfo1+fra1
serving, staging data backends) so region-tagged runs are validated
against real multi-region serving end-to-end.
Also makes the unit-test mock origins in events-v4.test.ts and
trace-propagation.test.ts override-aware (same pattern the rest of
the file and utils.test.ts already use), so the suite passes whether
or not the override is set — these two files were the only spots
hardcoding https://vercel-workflow.com.
Conflicts: packages/world-vercel/src/index.ts — both sides added
imports/exports (createRunId from this branch, createAnalytics from
main #2698); kept both. Includes the TypeScript 6 workspace upgrade
(#2700) from main. world-vercel (249) and core (1356) tests pass.
Adds a dedicated e2e suite validating @workflow/world-vercel region
routing end to end, run as its own CI job (e2e-vercel-multi-region)
against the nextjs-turbopack workbench only — deliberately separate
from e2e.test.ts, which runs as a matrix across all worlds/frameworks
where Vercel-specific multi-region behavior doesn't apply.
- workbench/nextjs-turbopack/vercel.json: deploy to iad1+sfo1+fra1 so
region-routed flow messages have a function to land on in each region.
- workflows/99_e2e.ts: regionProbeWorkflow returns the VERCEL_REGION
observed by both the workflow and a step, so tests can assert the run
EXECUTED in the intended region (not just that it was tagged).
- packages/core/e2e/e2e-region.test.ts: per-region cases assert
1) start(..., { region }) mints a region-tagged run ID (decoded via
@workflow/world-vercel/run-id),
2) the workflow + step both observed VERCEL_REGION === region,
3) the server reports the run completed;
plus a concurrent all-regions case guarding against cross-region
misrouting under simultaneous multi-region traffic. Skips on local
deployments.
- .github/workflows/tests.yml: new e2e-vercel-multi-region job
mirroring e2e-vercel-prod's env/deployment-wait, running only the
new suite.
The first multi-region CI run surfaced two issues:
1. sfo1/fra1-tagged runs executed in iad1. The suite started runs from
the external test process, which uses the api.vercel.com token proxy
— and the proxy's queues path forwards every send to the region-less
VQS host (the world's proxy-mode resolveBaseUrl ignores the region
argument, and the proxy's x-vercel-vqs-api-url escape hatch only
allowlists vqs-server-*.vercel.sh preview hosts). Production traffic
publishes IN-FUNCTION (direct regional queue routing), so the suite
now triggers start() through a new workbench route
(/api/e2e-region-start) and rehydrates the run with getRun() —
testing the path production actually takes. Proxy-mode regional
queue routing is a known gap to address separately in api-workflow.
2. TypeError on world.runs.get: getWorld() is async and was called
without await.
Textually clean but semantically conflicting with #2789: main added the
proxy-mode x-vercel-queue-region header in createQueue's shared
clientOptions, referencing the static region const this branch removed
in favor of per-send resolution. Resolution: the header now rides the
per-send QueueClient construction, carrying resolveTargetRegion's
result — proxied sends route to the region resolved per message
(opts.region -> run-ID tag -> VERCEL_REGION -> iad1).
world-vercel: 253/253 (includes #2789's proxy-header tests, which hold
under per-send resolution).
…ion suite
With regional VQS routing now working through the api.vercel.com proxy
(vercel/api#79056 + #2789 + this branch's per-send region resolution),
the suite covers both start configurations, asserting the same three
properties for each (region-tagged run ID, execution in the intended
region via VERCEL_REGION echoed in the return value, server-side
completion):
1. EXPLICIT: start(..., { region }) called directly in the vitest
runner — publishes through the token proxy, per-send region carried
by x-vercel-queue-region. Restores the direct-start shape the suite
had originally, plus the concurrent all-regions case.
2. IMPLICIT: dedicated per-region workbench routes
(/api/e2e-region-implicit/{iad1,sfo1,fra1}), each pinned to a single
region via a per-function 'regions' entry in the workbench
vercel.json, calling start() with NO region option — createRunId
derives the tag from the minting function's VERCEL_REGION. The test
also asserts the route reported executing in its pinned region, so
the implicit-tagging assertion can't pass vacuously.
Replaces the interim /api/e2e-region-start route (explicit region via
request body), which existed to work around the pre-#79056 proxy gap.
…ng is in production
workflow-server#590 (iad1+sfo1+fra1 serving) merged and deployed to
production and the e2e backend, so this branch's e2e/benchmark runs no
longer need to target the wave-1 preview. Restores the empty override
the No Test Overrides lint job enforces for merge.
The override-aware unit-test origins (events-v4/trace-propagation)
stay — they are correct under any override value.
@saasjesus

Copy link
Copy Markdown

damn its happening

Regression coverage for a backend bug that made cross-region stream
reads report zero chunks on IN-PROGRESS streams (completed streams were
unaffected), which forced the multi-region serving rollback.
The new case exercises exactly that geometry:
- crossRegionStreamWorkflow (99_e2e.ts) writes N chunks to the default
output stream, then holds the stream OPEN for 45s before closing —
the in-progress window is the point, since completed streams are the
easy case.
- The e2e starts it with region iad1, waits (same-region, via the
api.vercel.com proxy) until all chunks are written, asserts the run
is still 'running', then reads through a new sfo1-pinned workbench
route (/api/e2e-stream-read/sfo1) that returns getTailIndex() plus
its VERCEL_REGION. The reader's region served none of the stream's
writes, so the reported chunk count must come from the backend's
cross-region stream metadata. The test fails loudly if the route
isn't actually executing in sfo1.
Also bumps the explicit-region test timeout to 120s: the first case in
the file absorbs every cold start at once (fresh workbench instances
in up to three regions plus a cold backend preview) and was observed
just over the 60s default.
BRANCH-ONLY (revert before merge, lint enforces):
WORKFLOW_SERVER_URL_OVERRIDE points at a multi-region backend preview
that includes the fix, so this validates cross-region stream
visibility end-to-end before multi-region serving is re-enabled.
Points the suite at an all-regions backend preview and widens coverage
from the wave-1 trio to every provisioned region:
- Explicit path: a single concurrent all-regions case starts one
tagged run per region (one shared cold-start window instead of 19
sequential ones) and aggregates per-region failures so a single
region's breakage reports alongside the full picture. The trio keeps
its detailed per-region cases and the 9-way concurrent-isolation
case.
- Implicit path: workbench gains a region-pinned
/api/e2e-region-implicit/<region> route per provisioned region (19
total, shared handler), the workbench itself now deploys to all of
them, and the test.each covers the full set with per-case timeouts
for regional cold starts.
- Multi-region CI job timeout 20m -> 35m for the sequential implicit
cases.
BRANCH-ONLY (revert before merge, lint enforces):
WORKFLOW_SERVER_URL_OVERRIDE now targets the all-regions backend
preview instead of the previous (stale, since-merged) fix preview.
The first all-regions run surfaced a subtle execution-locality
behavior: queue delivery is guaranteed to the tagged region's
dataplane and the delivery callback egresses from that region, but the
consumer invocation's execution region is chosen by where that
callback enters Vercel's edge — and adjacent regions can geo-resolve
to each other's functions. Observed live: kix1-tagged runs (callback
egressing from Osaka) deterministically executing in hnd1/Tokyo on
both the explicit and implicit paths, with tagging, data placement,
and completion all still strictly kix1.
expectRunInRegion now asserts execution lands in the tagged region OR
one of its geographic neighbors (EXECUTION_ADJACENCY), while run-ID
tagging and server-side completion remain strictly the requested
region. Gross misrouting (e.g. kix1 -> iad1) still fails.
… production
The all-regions workflow-server rollout is deployed and serving
production traffic from every Vercel region, so this branch's e2e no
longer needs to target a branch preview. Restores the empty override
the No Test Overrides lint enforces for merge.
With this the PR is complete: region-tagged run IDs, region-aware
queue routing, and the multi-region e2e suite (explicit + implicit +
all-regions + cross-region streams) all validate against the
production-default backends.

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 47 out of 48 changed files in this pull request and generated 3 comments.

Files not reviewed (1)
  • pnpm-lock.yaml: Generated file

Comment threadpackages/core/src/runtime/start.ts Outdated
Comment threadpackages/world-vercel/src/queue.ts
Comment threadpackages/world-vercel/src/events.ts Outdated
- start.ts: StartOptionsBase.region fallback is iad1, not the unknown
sentinel (createRunId always mints a concrete routable region)
- queue.ts: example used a nonexistent start({ runIdInput }) API; the
real option is start({ region })
- events.ts: decode() clears only the tag bit (top bit of the 48-bit
timestamp field) — it does not restore the original untagged ULID;
reword to say what actually matters for timestamp validation
Hooks are resolved by opaque token, which carries no region hint, so
lookup and resume must work regardless of which region owns the run's
data. Exercises the full follow-up-message path on sfo1- and
fra1-tagged runs: create inside the workflow, resolve by token from
the test process, resume twice sequentially, and assert payload order
and completion.
Regression coverage for the failure mode where the first message to a
hook-driven app on a non-iad1 run worked but every follow-up failed
with 'Hook not found'.
Conflict in packages/core/src/runtime/start.test.ts: both sides
appended a new top-level describe block at the same location — this
branch's 'createRunId' tests and main's 'queue namespace' tests.
Resolved by keeping both blocks verbatim (each is self-contained with
its own beforeEach/mocks). Lockfile reconciled with pnpm install.
Verified post-merge: core tsc clean, start/helpers tests 73 passed,
world-vercel 263 passed, full build 27/27.
@github-actionsgithub-actionsBot mentioned this pull request Jul 13, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Backport to stable failed for 9da2d76 due to a workflow error (backport job run).

This is usually an infrastructure problem (e.g. the configured AI model could not be found, an AI Gateway error, or an opencode crash) rather than a merge conflict. Check the job logs linked above for details.

Once the underlying issue is fixed, re-run the Backport to stable workflow manually via workflow_dispatch and paste this commit SHA into the ref input:

9da2d762604c2b73eb39f07fc0b069aea643e18d

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.

Support multi-region on Vercel backend

4 participants

@TooTallNate@VaguelySerious@saasjesus
, '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('^' + ".*" + ' [core][world][world-vercel] Add `World.createRunId()` and region-aware queue routing by TooTallNate · Pull Request #1981 · vercel/workflow · GitHub
Skip to content

[core][world][world-vercel] Add World.createRunId() and region-aware queue routing - #1981

Merged
TooTallNate merged 35 commits into
mainfrom
world-create-run-id
Jul 13, 2026
Merged

[core][world][world-vercel] Add World.createRunId() and region-aware queue routing#1981
TooTallNate merged 35 commits into
mainfrom
world-create-run-id

Conversation

@TooTallNate

@TooTallNateTooTallNate commented May 13, 2026

Copy link
Copy Markdown
Member

Closes#1931 and helps with #2708 (and possibly more)

Wires the tagged-ULID encoding introduced in #1978 into the workflow run lifecycle:

  1. @workflow/world — adds two extension points to the World interface:

    • createRunId?(input?: Record<string, unknown>): string — opt-in hook for worlds to mint custom run IDs (the returned value is the bare ULID; the core attaches the wrun_ prefix).
    • QueueOptions.region?: string — per-message routing hint that worlds can use to dispatch messages to a specific region.
  2. @workflow/corestart() now:

    • Accepts a new region option (StartOptionsBase.region).
    • Calls world.createRunId(opts) when the world provides one (forwarding the full options bag so worlds can read whichever fields they recognise), falling back to a monotonic ULID otherwise.
    • Threads opts.region onto the queue options so the initial workflow message is dispatched to the matching region.
  3. @workflow/world-vercel — implements both extension points:

    • createRunId(options?) mints region-tagged ULIDs. Region resolution order: options.regionVERCEL_REGION env var → iad1 default. A run ID is always tagged with a concrete, routable region; the unknown (0) sentinel is never minted.
    • The queue resolves its destination region from (in order): an explicit opts.region, the region embedded in the payload's tagged run ID, the VERCEL_REGION env var, and finally a hardcoded iad1 default — replacing the previous unconditional 'iad1' passed to @vercel/queue. In proxy mode the resolved region is forwarded as a routing header.

Monotonicity

encode() writes the 11 bits of region/version metadata into the top of the ULID's randomness section (all of byte 6 + the top 3 bits of byte 7), leaving the low 69 bits untouched. The underlying ulid monotonic factory's same-millisecond bottom-bit increments therefore survive encoding, and consecutive IDs with the same region/version metadata are naturally monotonic — no correction needed in the common case.

The one remaining hazard is the metadata changing between same-millisecond calls (a lower-numbered region sorts below a higher one at the same timestamp). For that, createRunId tracks the last emitted ID and, on collision, bumps the bit immediately above the metadata window (the lowest bit of the 48-bit timestamp) before re-stamping the requested region/version. The test suite covers 4096 same-ms mints plus same-ms region changes.

E2E coverage

Adds packages/core/e2e/e2e-region.test.ts (CI job e2e-vercel-multi-region, deployed to all 19 routable regions):

  • Explicit start({ region }) for iad1 / sfo1 / fra1, verifying run-ID tags, data placement, and execution region.
  • A 9-way and an all-19-regions concurrent batch verifying region isolation under parallel starts.
  • Implicit region tagging via region-pinned routes for each of the 19 regions (no explicit region option — VERCEL_REGION drives the tag).
  • A cross-region durable stream test (writer in iad1, mid-stream reader in sfo1).

Execution-region assertions use a small adjacency tolerance for geo-elastic execution (e.g. kix1 → hnd1); run-ID tags and data placement are always asserted strictly.

Encodes a tag bit, 5-bit version, and 6-bit Vercel region ID into a
ULID-shaped string used for workflow run IDs. Tagged values remain
valid 26-char Crockford-Base32 ULIDs so they still sort and round-trip
through any system that accepts ULIDs.
Add exact-string expectations for encoded outputs at known inputs,
covering the default region/version pair, numeric region IDs, version
overrides, boundary values (all-zero, all-max), the dirty-input
overwrite case, and the lexicographic-order checks. Also adds an
explicit byte-array expectation for the canonical ULID-spec example
string and an additional first-char-range coverage test for isTagged.
- isTaggedString now fully validates the input as a 26-char Crockford
Base32 ULID (delegating to ulidToBytes) instead of only inspecting
the first character. This fixes false positives on inputs like
'4UUUU...' that have a valid tag-bit position but invalid chars
later in the string.
- isTagged() now accepts `unknown` to match its documented behavior
of safely rejecting non-string inputs without requiring callers to
cast.
- Introduce `RegionKey` for the full set of keys including 'unknown',
and narrow `RegionCode` to `Exclude<RegionKey, 'unknown'>` so the
return type of `lookupRegion` and the `DecodedRunId.region` field
accurately reflect that 'unknown' is never produced. Updates
`encode` to reject 'unknown' as a region code string at runtime
(callers wanting the unknown sentinel should pass numeric 0).
…e queue routing
- @workflow/world: add optional createRunId(input?) to the World
interface so worlds can mint run IDs with embedded metadata, and
add an optional 'region' field to QueueOptions for per-message
routing hints.
- @workflow/core: start() now delegates run ID generation to
world.createRunId() when defined (falling back to a monotonic
ULID otherwise), and accepts a new 'runIdInput' option that is
forwarded verbatim to createRunId. When runIdInput.region is a
string, it is also threaded onto the queue options so the initial
workflow message is dispatched to the matching region.
- @workflow/world-vercel: implement createRunId() to mint
region-tagged ULIDs, preferring an explicit runIdInput.region and
falling back to the VERCEL_REGION env var. The queue now resolves
its destination region from (in order): an explicit opts.region,
the region embedded in the payload's tagged run ID, the
VERCEL_REGION env var, and finally a hardcoded 'iad1' default.
This replaces the previous unconditional 'iad1' region passed to
the @vercel/queue client.
Monotonicity within a process is preserved by tracking the last
emitted run ID and bumping the bit immediately above the 11-bit
metadata window when a same-ms collision would otherwise occur,
then re-stamping the requested region/version on top so metadata
remains stable.
@TooTallNate
TooTallNate requested a review from a team as a code ownerMay 13, 2026 17:47
CopilotAI review requested due to automatic review settings May 13, 2026 17:47
@vercel

vercelBot commented May 13, 2026

Copy link
Copy Markdown
Contributor

@changeset-bot

changeset-botBot commented May 13, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 4f77bc1

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

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

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

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production145302301683
✅ 💻 Local Development161702191836
✅ 📦 Local Production161702191836
✅ 🐘 Local Postgres161702191836
✅ 🪟 Windows15300153
✅ 📋 Other89401771071
✅ vercel-multi-region270027
Total7378010648442

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro126027
✅ example126027
✅ express126027
✅ fastify126027
✅ hono126027
✅ nextjs-turbopack15003
✅ nextjs-webpack15003
✅ nitro126027
✅ nuxt126027
✅ sveltekit14508
✅ vite126027
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack15300
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable128025
✅ e2e-local-dev-tanstack-start-128025
✅ e2e-local-postgres-nest-stable128025
✅ e2e-local-postgres-tanstack-start-128025
✅ e2e-local-prod-nest-stable128025
✅ e2e-local-prod-tanstack-start-128025
✅ e2e-vercel-prod-tanstack-start126027
✅ vercel-multi-region
AppPassedFailedSkipped
✅ nextjs-turbopack2700

📋 View full workflow run

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR extends the workflow runtime/world contract to support (1) world-minted run IDs and (2) region-aware queue routing, and wires those capabilities through @workflow/core and the Vercel world implementation.

Changes:

  • Add optional World.createRunId(input?) and QueueOptions.region in @workflow/world.
  • Update @workflow/corestart() to delegate run ID generation to world.createRunId(opts.runIdInput) when available, and to forward runIdInput.region to queue options.
  • Implement region-tagged run IDs + region-resolving queue routing in @workflow/world-vercel, including new tests and a new ulid dependency.

Reviewed changes

Copilot reviewed 13 out of 14 changed files in this pull request and generated 4 comments.

Show a summary per file
FileDescription
pnpm-lock.yamlAdds lockfile entry for ulid used by world-vercel.
packages/world/src/queue.tsExtends QueueOptions with a region routing hint + docs.
packages/world/src/interfaces.tsExtends World interface with optional createRunId.
packages/world-vercel/src/queue.tsAdds region resolution order (opts → tagged runId → env → fallback) and updates CBOR encode/decode imports.
packages/world-vercel/src/queue.test.tsAdds region-routing test coverage.
packages/world-vercel/src/index.tsWires createRunId into createVercelWorld and re-exports it.
packages/world-vercel/src/create-run-id.tsImplements region-tagged ULID minting with per-process monotonicity.
packages/world-vercel/src/create-run-id.test.tsTests for env/input fallback and monotonicity behavior.
packages/world-vercel/package.jsonAdds ulid dependency.
packages/core/src/runtime/start.tsUses world.createRunId when available; forwards runIdInput.region into queue options.
packages/core/src/runtime/start.test.tsAdds tests validating createRunId usage and region threading.
.changeset/world-vercel-create-run-id.mdRelease notes for new run ID minting + routing behavior in world-vercel.
.changeset/world-create-run-id.mdRelease notes for new @workflow/world interface fields.
.changeset/core-run-id-input.mdRelease notes for core start() runIdInput behavior.
Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

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

Comment threadpackages/world/src/queue.ts Outdated
Comment threadpackages/world-vercel/src/queue.ts
Comment threadpackages/world-vercel/src/queue.test.ts Outdated
Comment threadpackages/world-vercel/src/create-run-id.test.ts Outdated
…unId
Drop the dedicated 'runIdInput' field on StartOptions and forward the
entire options bag to world.createRunId() instead. This keeps the
public API surface smaller and lets each World pick the fields it
recognises (e.g. world-vercel reads 'region'). The top-level 'region'
option remains on StartOptionsBase and is also threaded onto the
queue's per-call region opt when set.
…ride
- queue: validate opts.region and VERCEL_REGION against the known region
table before routing, ignoring unrecognised codes so a bad override
can't clobber the payload-derived region (Copilot)
- add isKnownRegionCode() runtime guard to run-id/regions
- reset WORKFLOW_SERVER_URL_OVERRIDE to '' (must be empty on main)
- fold the within-PR iad1-default changeset into the main world-vercel
changeset and delete it (review)
- start.test: declare specVersion on createRunId mock worlds now that
the merged world-compatibility check requires it
- cover the new region-validation fall-through paths in queue.test
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@VaguelySerious

Copy link
Copy Markdown
Member

(AI) Deleted .changeset/run-id-default-iad1.md and folded its net user-facing behavior (the iad1 default when no region/env is available) into the main world-vercel-create-run-id changeset so the changelog stays accurate without the within-PR intermediate note.

…i-region preview
BRANCH-ONLY — revert the override to '' before merge (lint enforces).
Points this PR's e2e/benchmark runs at the wave-1 multi-region
workflow-server preview (vercel/workflow-server#590: iad1+sfo1+fra1
serving, staging data backends) so region-tagged runs are validated
against real multi-region serving end-to-end.
Also makes the unit-test mock origins in events-v4.test.ts and
trace-propagation.test.ts override-aware (same pattern the rest of
the file and utils.test.ts already use), so the suite passes whether
or not the override is set — these two files were the only spots
hardcoding https://vercel-workflow.com.
Conflicts: packages/world-vercel/src/index.ts — both sides added
imports/exports (createRunId from this branch, createAnalytics from
main #2698); kept both. Includes the TypeScript 6 workspace upgrade
(#2700) from main. world-vercel (249) and core (1356) tests pass.
Adds a dedicated e2e suite validating @workflow/world-vercel region
routing end to end, run as its own CI job (e2e-vercel-multi-region)
against the nextjs-turbopack workbench only — deliberately separate
from e2e.test.ts, which runs as a matrix across all worlds/frameworks
where Vercel-specific multi-region behavior doesn't apply.
- workbench/nextjs-turbopack/vercel.json: deploy to iad1+sfo1+fra1 so
region-routed flow messages have a function to land on in each region.
- workflows/99_e2e.ts: regionProbeWorkflow returns the VERCEL_REGION
observed by both the workflow and a step, so tests can assert the run
EXECUTED in the intended region (not just that it was tagged).
- packages/core/e2e/e2e-region.test.ts: per-region cases assert
1) start(..., { region }) mints a region-tagged run ID (decoded via
@workflow/world-vercel/run-id),
2) the workflow + step both observed VERCEL_REGION === region,
3) the server reports the run completed;
plus a concurrent all-regions case guarding against cross-region
misrouting under simultaneous multi-region traffic. Skips on local
deployments.
- .github/workflows/tests.yml: new e2e-vercel-multi-region job
mirroring e2e-vercel-prod's env/deployment-wait, running only the
new suite.
The first multi-region CI run surfaced two issues:
1. sfo1/fra1-tagged runs executed in iad1. The suite started runs from
the external test process, which uses the api.vercel.com token proxy
— and the proxy's queues path forwards every send to the region-less
VQS host (the world's proxy-mode resolveBaseUrl ignores the region
argument, and the proxy's x-vercel-vqs-api-url escape hatch only
allowlists vqs-server-*.vercel.sh preview hosts). Production traffic
publishes IN-FUNCTION (direct regional queue routing), so the suite
now triggers start() through a new workbench route
(/api/e2e-region-start) and rehydrates the run with getRun() —
testing the path production actually takes. Proxy-mode regional
queue routing is a known gap to address separately in api-workflow.
2. TypeError on world.runs.get: getWorld() is async and was called
without await.
Textually clean but semantically conflicting with #2789: main added the
proxy-mode x-vercel-queue-region header in createQueue's shared
clientOptions, referencing the static region const this branch removed
in favor of per-send resolution. Resolution: the header now rides the
per-send QueueClient construction, carrying resolveTargetRegion's
result — proxied sends route to the region resolved per message
(opts.region -> run-ID tag -> VERCEL_REGION -> iad1).
world-vercel: 253/253 (includes #2789's proxy-header tests, which hold
under per-send resolution).
…ion suite
With regional VQS routing now working through the api.vercel.com proxy
(vercel/api#79056 + #2789 + this branch's per-send region resolution),
the suite covers both start configurations, asserting the same three
properties for each (region-tagged run ID, execution in the intended
region via VERCEL_REGION echoed in the return value, server-side
completion):
1. EXPLICIT: start(..., { region }) called directly in the vitest
runner — publishes through the token proxy, per-send region carried
by x-vercel-queue-region. Restores the direct-start shape the suite
had originally, plus the concurrent all-regions case.
2. IMPLICIT: dedicated per-region workbench routes
(/api/e2e-region-implicit/{iad1,sfo1,fra1}), each pinned to a single
region via a per-function 'regions' entry in the workbench
vercel.json, calling start() with NO region option — createRunId
derives the tag from the minting function's VERCEL_REGION. The test
also asserts the route reported executing in its pinned region, so
the implicit-tagging assertion can't pass vacuously.
Replaces the interim /api/e2e-region-start route (explicit region via
request body), which existed to work around the pre-#79056 proxy gap.
…ng is in production
workflow-server#590 (iad1+sfo1+fra1 serving) merged and deployed to
production and the e2e backend, so this branch's e2e/benchmark runs no
longer need to target the wave-1 preview. Restores the empty override
the No Test Overrides lint job enforces for merge.
The override-aware unit-test origins (events-v4/trace-propagation)
stay — they are correct under any override value.
@saasjesus

Copy link
Copy Markdown

damn its happening

Regression coverage for a backend bug that made cross-region stream
reads report zero chunks on IN-PROGRESS streams (completed streams were
unaffected), which forced the multi-region serving rollback.
The new case exercises exactly that geometry:
- crossRegionStreamWorkflow (99_e2e.ts) writes N chunks to the default
output stream, then holds the stream OPEN for 45s before closing —
the in-progress window is the point, since completed streams are the
easy case.
- The e2e starts it with region iad1, waits (same-region, via the
api.vercel.com proxy) until all chunks are written, asserts the run
is still 'running', then reads through a new sfo1-pinned workbench
route (/api/e2e-stream-read/sfo1) that returns getTailIndex() plus
its VERCEL_REGION. The reader's region served none of the stream's
writes, so the reported chunk count must come from the backend's
cross-region stream metadata. The test fails loudly if the route
isn't actually executing in sfo1.
Also bumps the explicit-region test timeout to 120s: the first case in
the file absorbs every cold start at once (fresh workbench instances
in up to three regions plus a cold backend preview) and was observed
just over the 60s default.
BRANCH-ONLY (revert before merge, lint enforces):
WORKFLOW_SERVER_URL_OVERRIDE points at a multi-region backend preview
that includes the fix, so this validates cross-region stream
visibility end-to-end before multi-region serving is re-enabled.
Points the suite at an all-regions backend preview and widens coverage
from the wave-1 trio to every provisioned region:
- Explicit path: a single concurrent all-regions case starts one
tagged run per region (one shared cold-start window instead of 19
sequential ones) and aggregates per-region failures so a single
region's breakage reports alongside the full picture. The trio keeps
its detailed per-region cases and the 9-way concurrent-isolation
case.
- Implicit path: workbench gains a region-pinned
/api/e2e-region-implicit/<region> route per provisioned region (19
total, shared handler), the workbench itself now deploys to all of
them, and the test.each covers the full set with per-case timeouts
for regional cold starts.
- Multi-region CI job timeout 20m -> 35m for the sequential implicit
cases.
BRANCH-ONLY (revert before merge, lint enforces):
WORKFLOW_SERVER_URL_OVERRIDE now targets the all-regions backend
preview instead of the previous (stale, since-merged) fix preview.
The first all-regions run surfaced a subtle execution-locality
behavior: queue delivery is guaranteed to the tagged region's
dataplane and the delivery callback egresses from that region, but the
consumer invocation's execution region is chosen by where that
callback enters Vercel's edge — and adjacent regions can geo-resolve
to each other's functions. Observed live: kix1-tagged runs (callback
egressing from Osaka) deterministically executing in hnd1/Tokyo on
both the explicit and implicit paths, with tagging, data placement,
and completion all still strictly kix1.
expectRunInRegion now asserts execution lands in the tagged region OR
one of its geographic neighbors (EXECUTION_ADJACENCY), while run-ID
tagging and server-side completion remain strictly the requested
region. Gross misrouting (e.g. kix1 -> iad1) still fails.
… production
The all-regions workflow-server rollout is deployed and serving
production traffic from every Vercel region, so this branch's e2e no
longer needs to target a branch preview. Restores the empty override
the No Test Overrides lint enforces for merge.
With this the PR is complete: region-tagged run IDs, region-aware
queue routing, and the multi-region e2e suite (explicit + implicit +
all-regions + cross-region streams) all validate against the
production-default backends.

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 47 out of 48 changed files in this pull request and generated 3 comments.

Files not reviewed (1)
  • pnpm-lock.yaml: Generated file

Comment threadpackages/core/src/runtime/start.ts Outdated
Comment threadpackages/world-vercel/src/queue.ts
Comment threadpackages/world-vercel/src/events.ts Outdated
- start.ts: StartOptionsBase.region fallback is iad1, not the unknown
sentinel (createRunId always mints a concrete routable region)
- queue.ts: example used a nonexistent start({ runIdInput }) API; the
real option is start({ region })
- events.ts: decode() clears only the tag bit (top bit of the 48-bit
timestamp field) — it does not restore the original untagged ULID;
reword to say what actually matters for timestamp validation
Hooks are resolved by opaque token, which carries no region hint, so
lookup and resume must work regardless of which region owns the run's
data. Exercises the full follow-up-message path on sfo1- and
fra1-tagged runs: create inside the workflow, resolve by token from
the test process, resume twice sequentially, and assert payload order
and completion.
Regression coverage for the failure mode where the first message to a
hook-driven app on a non-iad1 run worked but every follow-up failed
with 'Hook not found'.
Conflict in packages/core/src/runtime/start.test.ts: both sides
appended a new top-level describe block at the same location — this
branch's 'createRunId' tests and main's 'queue namespace' tests.
Resolved by keeping both blocks verbatim (each is self-contained with
its own beforeEach/mocks). Lockfile reconciled with pnpm install.
Verified post-merge: core tsc clean, start/helpers tests 73 passed,
world-vercel 263 passed, full build 27/27.
@github-actionsgithub-actionsBot mentioned this pull request Jul 13, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Backport to stable failed for 9da2d76 due to a workflow error (backport job run).

This is usually an infrastructure problem (e.g. the configured AI model could not be found, an AI Gateway error, or an opencode crash) rather than a merge conflict. Check the job logs linked above for details.

Once the underlying issue is fixed, re-run the Backport to stable workflow manually via workflow_dispatch and paste this commit SHA into the ref input:

9da2d762604c2b73eb39f07fc0b069aea643e18d

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.

Support multi-region on Vercel backend

4 participants

@TooTallNate@VaguelySerious@saasjesus
, '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('^' + ".*" + ' [core][world][world-vercel] Add `World.createRunId()` and region-aware queue routing by TooTallNate · Pull Request #1981 · vercel/workflow · GitHub
Skip to content

[core][world][world-vercel] Add World.createRunId() and region-aware queue routing - #1981

Merged
TooTallNate merged 35 commits into
mainfrom
world-create-run-id
Jul 13, 2026
Merged

[core][world][world-vercel] Add World.createRunId() and region-aware queue routing#1981
TooTallNate merged 35 commits into
mainfrom
world-create-run-id

Conversation

@TooTallNate

@TooTallNateTooTallNate commented May 13, 2026

Copy link
Copy Markdown
Member

Closes#1931 and helps with #2708 (and possibly more)

Wires the tagged-ULID encoding introduced in #1978 into the workflow run lifecycle:

  1. @workflow/world — adds two extension points to the World interface:

    • createRunId?(input?: Record<string, unknown>): string — opt-in hook for worlds to mint custom run IDs (the returned value is the bare ULID; the core attaches the wrun_ prefix).
    • QueueOptions.region?: string — per-message routing hint that worlds can use to dispatch messages to a specific region.
  2. @workflow/corestart() now:

    • Accepts a new region option (StartOptionsBase.region).
    • Calls world.createRunId(opts) when the world provides one (forwarding the full options bag so worlds can read whichever fields they recognise), falling back to a monotonic ULID otherwise.
    • Threads opts.region onto the queue options so the initial workflow message is dispatched to the matching region.
  3. @workflow/world-vercel — implements both extension points:

    • createRunId(options?) mints region-tagged ULIDs. Region resolution order: options.regionVERCEL_REGION env var → iad1 default. A run ID is always tagged with a concrete, routable region; the unknown (0) sentinel is never minted.
    • The queue resolves its destination region from (in order): an explicit opts.region, the region embedded in the payload's tagged run ID, the VERCEL_REGION env var, and finally a hardcoded iad1 default — replacing the previous unconditional 'iad1' passed to @vercel/queue. In proxy mode the resolved region is forwarded as a routing header.

Monotonicity

encode() writes the 11 bits of region/version metadata into the top of the ULID's randomness section (all of byte 6 + the top 3 bits of byte 7), leaving the low 69 bits untouched. The underlying ulid monotonic factory's same-millisecond bottom-bit increments therefore survive encoding, and consecutive IDs with the same region/version metadata are naturally monotonic — no correction needed in the common case.

The one remaining hazard is the metadata changing between same-millisecond calls (a lower-numbered region sorts below a higher one at the same timestamp). For that, createRunId tracks the last emitted ID and, on collision, bumps the bit immediately above the metadata window (the lowest bit of the 48-bit timestamp) before re-stamping the requested region/version. The test suite covers 4096 same-ms mints plus same-ms region changes.

E2E coverage

Adds packages/core/e2e/e2e-region.test.ts (CI job e2e-vercel-multi-region, deployed to all 19 routable regions):

  • Explicit start({ region }) for iad1 / sfo1 / fra1, verifying run-ID tags, data placement, and execution region.
  • A 9-way and an all-19-regions concurrent batch verifying region isolation under parallel starts.
  • Implicit region tagging via region-pinned routes for each of the 19 regions (no explicit region option — VERCEL_REGION drives the tag).
  • A cross-region durable stream test (writer in iad1, mid-stream reader in sfo1).

Execution-region assertions use a small adjacency tolerance for geo-elastic execution (e.g. kix1 → hnd1); run-ID tags and data placement are always asserted strictly.

Encodes a tag bit, 5-bit version, and 6-bit Vercel region ID into a
ULID-shaped string used for workflow run IDs. Tagged values remain
valid 26-char Crockford-Base32 ULIDs so they still sort and round-trip
through any system that accepts ULIDs.
Add exact-string expectations for encoded outputs at known inputs,
covering the default region/version pair, numeric region IDs, version
overrides, boundary values (all-zero, all-max), the dirty-input
overwrite case, and the lexicographic-order checks. Also adds an
explicit byte-array expectation for the canonical ULID-spec example
string and an additional first-char-range coverage test for isTagged.
- isTaggedString now fully validates the input as a 26-char Crockford
Base32 ULID (delegating to ulidToBytes) instead of only inspecting
the first character. This fixes false positives on inputs like
'4UUUU...' that have a valid tag-bit position but invalid chars
later in the string.
- isTagged() now accepts `unknown` to match its documented behavior
of safely rejecting non-string inputs without requiring callers to
cast.
- Introduce `RegionKey` for the full set of keys including 'unknown',
and narrow `RegionCode` to `Exclude<RegionKey, 'unknown'>` so the
return type of `lookupRegion` and the `DecodedRunId.region` field
accurately reflect that 'unknown' is never produced. Updates
`encode` to reject 'unknown' as a region code string at runtime
(callers wanting the unknown sentinel should pass numeric 0).
…e queue routing
- @workflow/world: add optional createRunId(input?) to the World
interface so worlds can mint run IDs with embedded metadata, and
add an optional 'region' field to QueueOptions for per-message
routing hints.
- @workflow/core: start() now delegates run ID generation to
world.createRunId() when defined (falling back to a monotonic
ULID otherwise), and accepts a new 'runIdInput' option that is
forwarded verbatim to createRunId. When runIdInput.region is a
string, it is also threaded onto the queue options so the initial
workflow message is dispatched to the matching region.
- @workflow/world-vercel: implement createRunId() to mint
region-tagged ULIDs, preferring an explicit runIdInput.region and
falling back to the VERCEL_REGION env var. The queue now resolves
its destination region from (in order): an explicit opts.region,
the region embedded in the payload's tagged run ID, the
VERCEL_REGION env var, and finally a hardcoded 'iad1' default.
This replaces the previous unconditional 'iad1' region passed to
the @vercel/queue client.
Monotonicity within a process is preserved by tracking the last
emitted run ID and bumping the bit immediately above the 11-bit
metadata window when a same-ms collision would otherwise occur,
then re-stamping the requested region/version on top so metadata
remains stable.
@TooTallNate
TooTallNate requested a review from a team as a code ownerMay 13, 2026 17:47
CopilotAI review requested due to automatic review settings May 13, 2026 17:47
@vercel

vercelBot commented May 13, 2026

Copy link
Copy Markdown
Contributor

@changeset-bot

changeset-botBot commented May 13, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 4f77bc1

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

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

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

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production145302301683
✅ 💻 Local Development161702191836
✅ 📦 Local Production161702191836
✅ 🐘 Local Postgres161702191836
✅ 🪟 Windows15300153
✅ 📋 Other89401771071
✅ vercel-multi-region270027
Total7378010648442

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro126027
✅ example126027
✅ express126027
✅ fastify126027
✅ hono126027
✅ nextjs-turbopack15003
✅ nextjs-webpack15003
✅ nitro126027
✅ nuxt126027
✅ sveltekit14508
✅ vite126027
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack15300
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable128025
✅ e2e-local-dev-tanstack-start-128025
✅ e2e-local-postgres-nest-stable128025
✅ e2e-local-postgres-tanstack-start-128025
✅ e2e-local-prod-nest-stable128025
✅ e2e-local-prod-tanstack-start-128025
✅ e2e-vercel-prod-tanstack-start126027
✅ vercel-multi-region
AppPassedFailedSkipped
✅ nextjs-turbopack2700

📋 View full workflow run

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR extends the workflow runtime/world contract to support (1) world-minted run IDs and (2) region-aware queue routing, and wires those capabilities through @workflow/core and the Vercel world implementation.

Changes:

  • Add optional World.createRunId(input?) and QueueOptions.region in @workflow/world.
  • Update @workflow/corestart() to delegate run ID generation to world.createRunId(opts.runIdInput) when available, and to forward runIdInput.region to queue options.
  • Implement region-tagged run IDs + region-resolving queue routing in @workflow/world-vercel, including new tests and a new ulid dependency.

Reviewed changes

Copilot reviewed 13 out of 14 changed files in this pull request and generated 4 comments.

Show a summary per file
FileDescription
pnpm-lock.yamlAdds lockfile entry for ulid used by world-vercel.
packages/world/src/queue.tsExtends QueueOptions with a region routing hint + docs.
packages/world/src/interfaces.tsExtends World interface with optional createRunId.
packages/world-vercel/src/queue.tsAdds region resolution order (opts → tagged runId → env → fallback) and updates CBOR encode/decode imports.
packages/world-vercel/src/queue.test.tsAdds region-routing test coverage.
packages/world-vercel/src/index.tsWires createRunId into createVercelWorld and re-exports it.
packages/world-vercel/src/create-run-id.tsImplements region-tagged ULID minting with per-process monotonicity.
packages/world-vercel/src/create-run-id.test.tsTests for env/input fallback and monotonicity behavior.
packages/world-vercel/package.jsonAdds ulid dependency.
packages/core/src/runtime/start.tsUses world.createRunId when available; forwards runIdInput.region into queue options.
packages/core/src/runtime/start.test.tsAdds tests validating createRunId usage and region threading.
.changeset/world-vercel-create-run-id.mdRelease notes for new run ID minting + routing behavior in world-vercel.
.changeset/world-create-run-id.mdRelease notes for new @workflow/world interface fields.
.changeset/core-run-id-input.mdRelease notes for core start() runIdInput behavior.
Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

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

Comment threadpackages/world/src/queue.ts Outdated
Comment threadpackages/world-vercel/src/queue.ts
Comment threadpackages/world-vercel/src/queue.test.ts Outdated
Comment threadpackages/world-vercel/src/create-run-id.test.ts Outdated
…unId
Drop the dedicated 'runIdInput' field on StartOptions and forward the
entire options bag to world.createRunId() instead. This keeps the
public API surface smaller and lets each World pick the fields it
recognises (e.g. world-vercel reads 'region'). The top-level 'region'
option remains on StartOptionsBase and is also threaded onto the
queue's per-call region opt when set.
…ride
- queue: validate opts.region and VERCEL_REGION against the known region
table before routing, ignoring unrecognised codes so a bad override
can't clobber the payload-derived region (Copilot)
- add isKnownRegionCode() runtime guard to run-id/regions
- reset WORKFLOW_SERVER_URL_OVERRIDE to '' (must be empty on main)
- fold the within-PR iad1-default changeset into the main world-vercel
changeset and delete it (review)
- start.test: declare specVersion on createRunId mock worlds now that
the merged world-compatibility check requires it
- cover the new region-validation fall-through paths in queue.test
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@VaguelySerious

Copy link
Copy Markdown
Member

(AI) Deleted .changeset/run-id-default-iad1.md and folded its net user-facing behavior (the iad1 default when no region/env is available) into the main world-vercel-create-run-id changeset so the changelog stays accurate without the within-PR intermediate note.

…i-region preview
BRANCH-ONLY — revert the override to '' before merge (lint enforces).
Points this PR's e2e/benchmark runs at the wave-1 multi-region
workflow-server preview (vercel/workflow-server#590: iad1+sfo1+fra1
serving, staging data backends) so region-tagged runs are validated
against real multi-region serving end-to-end.
Also makes the unit-test mock origins in events-v4.test.ts and
trace-propagation.test.ts override-aware (same pattern the rest of
the file and utils.test.ts already use), so the suite passes whether
or not the override is set — these two files were the only spots
hardcoding https://vercel-workflow.com.
Conflicts: packages/world-vercel/src/index.ts — both sides added
imports/exports (createRunId from this branch, createAnalytics from
main #2698); kept both. Includes the TypeScript 6 workspace upgrade
(#2700) from main. world-vercel (249) and core (1356) tests pass.
Adds a dedicated e2e suite validating @workflow/world-vercel region
routing end to end, run as its own CI job (e2e-vercel-multi-region)
against the nextjs-turbopack workbench only — deliberately separate
from e2e.test.ts, which runs as a matrix across all worlds/frameworks
where Vercel-specific multi-region behavior doesn't apply.
- workbench/nextjs-turbopack/vercel.json: deploy to iad1+sfo1+fra1 so
region-routed flow messages have a function to land on in each region.
- workflows/99_e2e.ts: regionProbeWorkflow returns the VERCEL_REGION
observed by both the workflow and a step, so tests can assert the run
EXECUTED in the intended region (not just that it was tagged).
- packages/core/e2e/e2e-region.test.ts: per-region cases assert
1) start(..., { region }) mints a region-tagged run ID (decoded via
@workflow/world-vercel/run-id),
2) the workflow + step both observed VERCEL_REGION === region,
3) the server reports the run completed;
plus a concurrent all-regions case guarding against cross-region
misrouting under simultaneous multi-region traffic. Skips on local
deployments.
- .github/workflows/tests.yml: new e2e-vercel-multi-region job
mirroring e2e-vercel-prod's env/deployment-wait, running only the
new suite.
The first multi-region CI run surfaced two issues:
1. sfo1/fra1-tagged runs executed in iad1. The suite started runs from
the external test process, which uses the api.vercel.com token proxy
— and the proxy's queues path forwards every send to the region-less
VQS host (the world's proxy-mode resolveBaseUrl ignores the region
argument, and the proxy's x-vercel-vqs-api-url escape hatch only
allowlists vqs-server-*.vercel.sh preview hosts). Production traffic
publishes IN-FUNCTION (direct regional queue routing), so the suite
now triggers start() through a new workbench route
(/api/e2e-region-start) and rehydrates the run with getRun() —
testing the path production actually takes. Proxy-mode regional
queue routing is a known gap to address separately in api-workflow.
2. TypeError on world.runs.get: getWorld() is async and was called
without await.
Textually clean but semantically conflicting with #2789: main added the
proxy-mode x-vercel-queue-region header in createQueue's shared
clientOptions, referencing the static region const this branch removed
in favor of per-send resolution. Resolution: the header now rides the
per-send QueueClient construction, carrying resolveTargetRegion's
result — proxied sends route to the region resolved per message
(opts.region -> run-ID tag -> VERCEL_REGION -> iad1).
world-vercel: 253/253 (includes #2789's proxy-header tests, which hold
under per-send resolution).
…ion suite
With regional VQS routing now working through the api.vercel.com proxy
(vercel/api#79056 + #2789 + this branch's per-send region resolution),
the suite covers both start configurations, asserting the same three
properties for each (region-tagged run ID, execution in the intended
region via VERCEL_REGION echoed in the return value, server-side
completion):
1. EXPLICIT: start(..., { region }) called directly in the vitest
runner — publishes through the token proxy, per-send region carried
by x-vercel-queue-region. Restores the direct-start shape the suite
had originally, plus the concurrent all-regions case.
2. IMPLICIT: dedicated per-region workbench routes
(/api/e2e-region-implicit/{iad1,sfo1,fra1}), each pinned to a single
region via a per-function 'regions' entry in the workbench
vercel.json, calling start() with NO region option — createRunId
derives the tag from the minting function's VERCEL_REGION. The test
also asserts the route reported executing in its pinned region, so
the implicit-tagging assertion can't pass vacuously.
Replaces the interim /api/e2e-region-start route (explicit region via
request body), which existed to work around the pre-#79056 proxy gap.
…ng is in production
workflow-server#590 (iad1+sfo1+fra1 serving) merged and deployed to
production and the e2e backend, so this branch's e2e/benchmark runs no
longer need to target the wave-1 preview. Restores the empty override
the No Test Overrides lint job enforces for merge.
The override-aware unit-test origins (events-v4/trace-propagation)
stay — they are correct under any override value.
@saasjesus

Copy link
Copy Markdown

damn its happening

Regression coverage for a backend bug that made cross-region stream
reads report zero chunks on IN-PROGRESS streams (completed streams were
unaffected), which forced the multi-region serving rollback.
The new case exercises exactly that geometry:
- crossRegionStreamWorkflow (99_e2e.ts) writes N chunks to the default
output stream, then holds the stream OPEN for 45s before closing —
the in-progress window is the point, since completed streams are the
easy case.
- The e2e starts it with region iad1, waits (same-region, via the
api.vercel.com proxy) until all chunks are written, asserts the run
is still 'running', then reads through a new sfo1-pinned workbench
route (/api/e2e-stream-read/sfo1) that returns getTailIndex() plus
its VERCEL_REGION. The reader's region served none of the stream's
writes, so the reported chunk count must come from the backend's
cross-region stream metadata. The test fails loudly if the route
isn't actually executing in sfo1.
Also bumps the explicit-region test timeout to 120s: the first case in
the file absorbs every cold start at once (fresh workbench instances
in up to three regions plus a cold backend preview) and was observed
just over the 60s default.
BRANCH-ONLY (revert before merge, lint enforces):
WORKFLOW_SERVER_URL_OVERRIDE points at a multi-region backend preview
that includes the fix, so this validates cross-region stream
visibility end-to-end before multi-region serving is re-enabled.
Points the suite at an all-regions backend preview and widens coverage
from the wave-1 trio to every provisioned region:
- Explicit path: a single concurrent all-regions case starts one
tagged run per region (one shared cold-start window instead of 19
sequential ones) and aggregates per-region failures so a single
region's breakage reports alongside the full picture. The trio keeps
its detailed per-region cases and the 9-way concurrent-isolation
case.
- Implicit path: workbench gains a region-pinned
/api/e2e-region-implicit/<region> route per provisioned region (19
total, shared handler), the workbench itself now deploys to all of
them, and the test.each covers the full set with per-case timeouts
for regional cold starts.
- Multi-region CI job timeout 20m -> 35m for the sequential implicit
cases.
BRANCH-ONLY (revert before merge, lint enforces):
WORKFLOW_SERVER_URL_OVERRIDE now targets the all-regions backend
preview instead of the previous (stale, since-merged) fix preview.
The first all-regions run surfaced a subtle execution-locality
behavior: queue delivery is guaranteed to the tagged region's
dataplane and the delivery callback egresses from that region, but the
consumer invocation's execution region is chosen by where that
callback enters Vercel's edge — and adjacent regions can geo-resolve
to each other's functions. Observed live: kix1-tagged runs (callback
egressing from Osaka) deterministically executing in hnd1/Tokyo on
both the explicit and implicit paths, with tagging, data placement,
and completion all still strictly kix1.
expectRunInRegion now asserts execution lands in the tagged region OR
one of its geographic neighbors (EXECUTION_ADJACENCY), while run-ID
tagging and server-side completion remain strictly the requested
region. Gross misrouting (e.g. kix1 -> iad1) still fails.
… production
The all-regions workflow-server rollout is deployed and serving
production traffic from every Vercel region, so this branch's e2e no
longer needs to target a branch preview. Restores the empty override
the No Test Overrides lint enforces for merge.
With this the PR is complete: region-tagged run IDs, region-aware
queue routing, and the multi-region e2e suite (explicit + implicit +
all-regions + cross-region streams) all validate against the
production-default backends.

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 47 out of 48 changed files in this pull request and generated 3 comments.

Files not reviewed (1)
  • pnpm-lock.yaml: Generated file

Comment threadpackages/core/src/runtime/start.ts Outdated
Comment threadpackages/world-vercel/src/queue.ts
Comment threadpackages/world-vercel/src/events.ts Outdated
- start.ts: StartOptionsBase.region fallback is iad1, not the unknown
sentinel (createRunId always mints a concrete routable region)
- queue.ts: example used a nonexistent start({ runIdInput }) API; the
real option is start({ region })
- events.ts: decode() clears only the tag bit (top bit of the 48-bit
timestamp field) — it does not restore the original untagged ULID;
reword to say what actually matters for timestamp validation
Hooks are resolved by opaque token, which carries no region hint, so
lookup and resume must work regardless of which region owns the run's
data. Exercises the full follow-up-message path on sfo1- and
fra1-tagged runs: create inside the workflow, resolve by token from
the test process, resume twice sequentially, and assert payload order
and completion.
Regression coverage for the failure mode where the first message to a
hook-driven app on a non-iad1 run worked but every follow-up failed
with 'Hook not found'.
Conflict in packages/core/src/runtime/start.test.ts: both sides
appended a new top-level describe block at the same location — this
branch's 'createRunId' tests and main's 'queue namespace' tests.
Resolved by keeping both blocks verbatim (each is self-contained with
its own beforeEach/mocks). Lockfile reconciled with pnpm install.
Verified post-merge: core tsc clean, start/helpers tests 73 passed,
world-vercel 263 passed, full build 27/27.
@github-actionsgithub-actionsBot mentioned this pull request Jul 13, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Backport to stable failed for 9da2d76 due to a workflow error (backport job run).

This is usually an infrastructure problem (e.g. the configured AI model could not be found, an AI Gateway error, or an opencode crash) rather than a merge conflict. Check the job logs linked above for details.

Once the underlying issue is fixed, re-run the Backport to stable workflow manually via workflow_dispatch and paste this commit SHA into the ref input:

9da2d762604c2b73eb39f07fc0b069aea643e18d

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.

Support multi-region on Vercel backend

4 participants

@TooTallNate@VaguelySerious@saasjesus
, '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" + ' [core][world][world-vercel] Add `World.createRunId()` and region-aware queue routing by TooTallNate · Pull Request #1981 · vercel/workflow · GitHub
Skip to content

[core][world][world-vercel] Add World.createRunId() and region-aware queue routing - #1981

Merged
TooTallNate merged 35 commits into
mainfrom
world-create-run-id
Jul 13, 2026
Merged

[core][world][world-vercel] Add World.createRunId() and region-aware queue routing#1981
TooTallNate merged 35 commits into
mainfrom
world-create-run-id

Conversation

@TooTallNate

@TooTallNateTooTallNate commented May 13, 2026

Copy link
Copy Markdown
Member

Closes#1931 and helps with #2708 (and possibly more)

Wires the tagged-ULID encoding introduced in #1978 into the workflow run lifecycle:

  1. @workflow/world — adds two extension points to the World interface:

    • createRunId?(input?: Record<string, unknown>): string — opt-in hook for worlds to mint custom run IDs (the returned value is the bare ULID; the core attaches the wrun_ prefix).
    • QueueOptions.region?: string — per-message routing hint that worlds can use to dispatch messages to a specific region.
  2. @workflow/corestart() now:

    • Accepts a new region option (StartOptionsBase.region).
    • Calls world.createRunId(opts) when the world provides one (forwarding the full options bag so worlds can read whichever fields they recognise), falling back to a monotonic ULID otherwise.
    • Threads opts.region onto the queue options so the initial workflow message is dispatched to the matching region.
  3. @workflow/world-vercel — implements both extension points:

    • createRunId(options?) mints region-tagged ULIDs. Region resolution order: options.regionVERCEL_REGION env var → iad1 default. A run ID is always tagged with a concrete, routable region; the unknown (0) sentinel is never minted.
    • The queue resolves its destination region from (in order): an explicit opts.region, the region embedded in the payload's tagged run ID, the VERCEL_REGION env var, and finally a hardcoded iad1 default — replacing the previous unconditional 'iad1' passed to @vercel/queue. In proxy mode the resolved region is forwarded as a routing header.

Monotonicity

encode() writes the 11 bits of region/version metadata into the top of the ULID's randomness section (all of byte 6 + the top 3 bits of byte 7), leaving the low 69 bits untouched. The underlying ulid monotonic factory's same-millisecond bottom-bit increments therefore survive encoding, and consecutive IDs with the same region/version metadata are naturally monotonic — no correction needed in the common case.

The one remaining hazard is the metadata changing between same-millisecond calls (a lower-numbered region sorts below a higher one at the same timestamp). For that, createRunId tracks the last emitted ID and, on collision, bumps the bit immediately above the metadata window (the lowest bit of the 48-bit timestamp) before re-stamping the requested region/version. The test suite covers 4096 same-ms mints plus same-ms region changes.

E2E coverage

Adds packages/core/e2e/e2e-region.test.ts (CI job e2e-vercel-multi-region, deployed to all 19 routable regions):

  • Explicit start({ region }) for iad1 / sfo1 / fra1, verifying run-ID tags, data placement, and execution region.
  • A 9-way and an all-19-regions concurrent batch verifying region isolation under parallel starts.
  • Implicit region tagging via region-pinned routes for each of the 19 regions (no explicit region option — VERCEL_REGION drives the tag).
  • A cross-region durable stream test (writer in iad1, mid-stream reader in sfo1).

Execution-region assertions use a small adjacency tolerance for geo-elastic execution (e.g. kix1 → hnd1); run-ID tags and data placement are always asserted strictly.

Encodes a tag bit, 5-bit version, and 6-bit Vercel region ID into a
ULID-shaped string used for workflow run IDs. Tagged values remain
valid 26-char Crockford-Base32 ULIDs so they still sort and round-trip
through any system that accepts ULIDs.
Add exact-string expectations for encoded outputs at known inputs,
covering the default region/version pair, numeric region IDs, version
overrides, boundary values (all-zero, all-max), the dirty-input
overwrite case, and the lexicographic-order checks. Also adds an
explicit byte-array expectation for the canonical ULID-spec example
string and an additional first-char-range coverage test for isTagged.
- isTaggedString now fully validates the input as a 26-char Crockford
Base32 ULID (delegating to ulidToBytes) instead of only inspecting
the first character. This fixes false positives on inputs like
'4UUUU...' that have a valid tag-bit position but invalid chars
later in the string.
- isTagged() now accepts `unknown` to match its documented behavior
of safely rejecting non-string inputs without requiring callers to
cast.
- Introduce `RegionKey` for the full set of keys including 'unknown',
and narrow `RegionCode` to `Exclude<RegionKey, 'unknown'>` so the
return type of `lookupRegion` and the `DecodedRunId.region` field
accurately reflect that 'unknown' is never produced. Updates
`encode` to reject 'unknown' as a region code string at runtime
(callers wanting the unknown sentinel should pass numeric 0).
…e queue routing
- @workflow/world: add optional createRunId(input?) to the World
interface so worlds can mint run IDs with embedded metadata, and
add an optional 'region' field to QueueOptions for per-message
routing hints.
- @workflow/core: start() now delegates run ID generation to
world.createRunId() when defined (falling back to a monotonic
ULID otherwise), and accepts a new 'runIdInput' option that is
forwarded verbatim to createRunId. When runIdInput.region is a
string, it is also threaded onto the queue options so the initial
workflow message is dispatched to the matching region.
- @workflow/world-vercel: implement createRunId() to mint
region-tagged ULIDs, preferring an explicit runIdInput.region and
falling back to the VERCEL_REGION env var. The queue now resolves
its destination region from (in order): an explicit opts.region,
the region embedded in the payload's tagged run ID, the
VERCEL_REGION env var, and finally a hardcoded 'iad1' default.
This replaces the previous unconditional 'iad1' region passed to
the @vercel/queue client.
Monotonicity within a process is preserved by tracking the last
emitted run ID and bumping the bit immediately above the 11-bit
metadata window when a same-ms collision would otherwise occur,
then re-stamping the requested region/version on top so metadata
remains stable.
@TooTallNate
TooTallNate requested a review from a team as a code ownerMay 13, 2026 17:47
CopilotAI review requested due to automatic review settings May 13, 2026 17:47
@vercel

vercelBot commented May 13, 2026

Copy link
Copy Markdown
Contributor

@changeset-bot

changeset-botBot commented May 13, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 4f77bc1

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

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

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

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production145302301683
✅ 💻 Local Development161702191836
✅ 📦 Local Production161702191836
✅ 🐘 Local Postgres161702191836
✅ 🪟 Windows15300153
✅ 📋 Other89401771071
✅ vercel-multi-region270027
Total7378010648442

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro126027
✅ example126027
✅ express126027
✅ fastify126027
✅ hono126027
✅ nextjs-turbopack15003
✅ nextjs-webpack15003
✅ nitro126027
✅ nuxt126027
✅ sveltekit14508
✅ vite126027
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack15300
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable128025
✅ e2e-local-dev-tanstack-start-128025
✅ e2e-local-postgres-nest-stable128025
✅ e2e-local-postgres-tanstack-start-128025
✅ e2e-local-prod-nest-stable128025
✅ e2e-local-prod-tanstack-start-128025
✅ e2e-vercel-prod-tanstack-start126027
✅ vercel-multi-region
AppPassedFailedSkipped
✅ nextjs-turbopack2700

📋 View full workflow run

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR extends the workflow runtime/world contract to support (1) world-minted run IDs and (2) region-aware queue routing, and wires those capabilities through @workflow/core and the Vercel world implementation.

Changes:

  • Add optional World.createRunId(input?) and QueueOptions.region in @workflow/world.
  • Update @workflow/corestart() to delegate run ID generation to world.createRunId(opts.runIdInput) when available, and to forward runIdInput.region to queue options.
  • Implement region-tagged run IDs + region-resolving queue routing in @workflow/world-vercel, including new tests and a new ulid dependency.

Reviewed changes

Copilot reviewed 13 out of 14 changed files in this pull request and generated 4 comments.

Show a summary per file
FileDescription
pnpm-lock.yamlAdds lockfile entry for ulid used by world-vercel.
packages/world/src/queue.tsExtends QueueOptions with a region routing hint + docs.
packages/world/src/interfaces.tsExtends World interface with optional createRunId.
packages/world-vercel/src/queue.tsAdds region resolution order (opts → tagged runId → env → fallback) and updates CBOR encode/decode imports.
packages/world-vercel/src/queue.test.tsAdds region-routing test coverage.
packages/world-vercel/src/index.tsWires createRunId into createVercelWorld and re-exports it.
packages/world-vercel/src/create-run-id.tsImplements region-tagged ULID minting with per-process monotonicity.
packages/world-vercel/src/create-run-id.test.tsTests for env/input fallback and monotonicity behavior.
packages/world-vercel/package.jsonAdds ulid dependency.
packages/core/src/runtime/start.tsUses world.createRunId when available; forwards runIdInput.region into queue options.
packages/core/src/runtime/start.test.tsAdds tests validating createRunId usage and region threading.
.changeset/world-vercel-create-run-id.mdRelease notes for new run ID minting + routing behavior in world-vercel.
.changeset/world-create-run-id.mdRelease notes for new @workflow/world interface fields.
.changeset/core-run-id-input.mdRelease notes for core start() runIdInput behavior.
Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

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

Comment threadpackages/world/src/queue.ts Outdated
Comment threadpackages/world-vercel/src/queue.ts
Comment threadpackages/world-vercel/src/queue.test.ts Outdated
Comment threadpackages/world-vercel/src/create-run-id.test.ts Outdated
…unId
Drop the dedicated 'runIdInput' field on StartOptions and forward the
entire options bag to world.createRunId() instead. This keeps the
public API surface smaller and lets each World pick the fields it
recognises (e.g. world-vercel reads 'region'). The top-level 'region'
option remains on StartOptionsBase and is also threaded onto the
queue's per-call region opt when set.
…ride
- queue: validate opts.region and VERCEL_REGION against the known region
table before routing, ignoring unrecognised codes so a bad override
can't clobber the payload-derived region (Copilot)
- add isKnownRegionCode() runtime guard to run-id/regions
- reset WORKFLOW_SERVER_URL_OVERRIDE to '' (must be empty on main)
- fold the within-PR iad1-default changeset into the main world-vercel
changeset and delete it (review)
- start.test: declare specVersion on createRunId mock worlds now that
the merged world-compatibility check requires it
- cover the new region-validation fall-through paths in queue.test
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@VaguelySerious

Copy link
Copy Markdown
Member

(AI) Deleted .changeset/run-id-default-iad1.md and folded its net user-facing behavior (the iad1 default when no region/env is available) into the main world-vercel-create-run-id changeset so the changelog stays accurate without the within-PR intermediate note.

…i-region preview
BRANCH-ONLY — revert the override to '' before merge (lint enforces).
Points this PR's e2e/benchmark runs at the wave-1 multi-region
workflow-server preview (vercel/workflow-server#590: iad1+sfo1+fra1
serving, staging data backends) so region-tagged runs are validated
against real multi-region serving end-to-end.
Also makes the unit-test mock origins in events-v4.test.ts and
trace-propagation.test.ts override-aware (same pattern the rest of
the file and utils.test.ts already use), so the suite passes whether
or not the override is set — these two files were the only spots
hardcoding https://vercel-workflow.com.
Conflicts: packages/world-vercel/src/index.ts — both sides added
imports/exports (createRunId from this branch, createAnalytics from
main #2698); kept both. Includes the TypeScript 6 workspace upgrade
(#2700) from main. world-vercel (249) and core (1356) tests pass.
Adds a dedicated e2e suite validating @workflow/world-vercel region
routing end to end, run as its own CI job (e2e-vercel-multi-region)
against the nextjs-turbopack workbench only — deliberately separate
from e2e.test.ts, which runs as a matrix across all worlds/frameworks
where Vercel-specific multi-region behavior doesn't apply.
- workbench/nextjs-turbopack/vercel.json: deploy to iad1+sfo1+fra1 so
region-routed flow messages have a function to land on in each region.
- workflows/99_e2e.ts: regionProbeWorkflow returns the VERCEL_REGION
observed by both the workflow and a step, so tests can assert the run
EXECUTED in the intended region (not just that it was tagged).
- packages/core/e2e/e2e-region.test.ts: per-region cases assert
1) start(..., { region }) mints a region-tagged run ID (decoded via
@workflow/world-vercel/run-id),
2) the workflow + step both observed VERCEL_REGION === region,
3) the server reports the run completed;
plus a concurrent all-regions case guarding against cross-region
misrouting under simultaneous multi-region traffic. Skips on local
deployments.
- .github/workflows/tests.yml: new e2e-vercel-multi-region job
mirroring e2e-vercel-prod's env/deployment-wait, running only the
new suite.
The first multi-region CI run surfaced two issues:
1. sfo1/fra1-tagged runs executed in iad1. The suite started runs from
the external test process, which uses the api.vercel.com token proxy
— and the proxy's queues path forwards every send to the region-less
VQS host (the world's proxy-mode resolveBaseUrl ignores the region
argument, and the proxy's x-vercel-vqs-api-url escape hatch only
allowlists vqs-server-*.vercel.sh preview hosts). Production traffic
publishes IN-FUNCTION (direct regional queue routing), so the suite
now triggers start() through a new workbench route
(/api/e2e-region-start) and rehydrates the run with getRun() —
testing the path production actually takes. Proxy-mode regional
queue routing is a known gap to address separately in api-workflow.
2. TypeError on world.runs.get: getWorld() is async and was called
without await.
Textually clean but semantically conflicting with #2789: main added the
proxy-mode x-vercel-queue-region header in createQueue's shared
clientOptions, referencing the static region const this branch removed
in favor of per-send resolution. Resolution: the header now rides the
per-send QueueClient construction, carrying resolveTargetRegion's
result — proxied sends route to the region resolved per message
(opts.region -> run-ID tag -> VERCEL_REGION -> iad1).
world-vercel: 253/253 (includes #2789's proxy-header tests, which hold
under per-send resolution).
…ion suite
With regional VQS routing now working through the api.vercel.com proxy
(vercel/api#79056 + #2789 + this branch's per-send region resolution),
the suite covers both start configurations, asserting the same three
properties for each (region-tagged run ID, execution in the intended
region via VERCEL_REGION echoed in the return value, server-side
completion):
1. EXPLICIT: start(..., { region }) called directly in the vitest
runner — publishes through the token proxy, per-send region carried
by x-vercel-queue-region. Restores the direct-start shape the suite
had originally, plus the concurrent all-regions case.
2. IMPLICIT: dedicated per-region workbench routes
(/api/e2e-region-implicit/{iad1,sfo1,fra1}), each pinned to a single
region via a per-function 'regions' entry in the workbench
vercel.json, calling start() with NO region option — createRunId
derives the tag from the minting function's VERCEL_REGION. The test
also asserts the route reported executing in its pinned region, so
the implicit-tagging assertion can't pass vacuously.
Replaces the interim /api/e2e-region-start route (explicit region via
request body), which existed to work around the pre-#79056 proxy gap.
…ng is in production
workflow-server#590 (iad1+sfo1+fra1 serving) merged and deployed to
production and the e2e backend, so this branch's e2e/benchmark runs no
longer need to target the wave-1 preview. Restores the empty override
the No Test Overrides lint job enforces for merge.
The override-aware unit-test origins (events-v4/trace-propagation)
stay — they are correct under any override value.
@saasjesus

Copy link
Copy Markdown

damn its happening

Regression coverage for a backend bug that made cross-region stream
reads report zero chunks on IN-PROGRESS streams (completed streams were
unaffected), which forced the multi-region serving rollback.
The new case exercises exactly that geometry:
- crossRegionStreamWorkflow (99_e2e.ts) writes N chunks to the default
output stream, then holds the stream OPEN for 45s before closing —
the in-progress window is the point, since completed streams are the
easy case.
- The e2e starts it with region iad1, waits (same-region, via the
api.vercel.com proxy) until all chunks are written, asserts the run
is still 'running', then reads through a new sfo1-pinned workbench
route (/api/e2e-stream-read/sfo1) that returns getTailIndex() plus
its VERCEL_REGION. The reader's region served none of the stream's
writes, so the reported chunk count must come from the backend's
cross-region stream metadata. The test fails loudly if the route
isn't actually executing in sfo1.
Also bumps the explicit-region test timeout to 120s: the first case in
the file absorbs every cold start at once (fresh workbench instances
in up to three regions plus a cold backend preview) and was observed
just over the 60s default.
BRANCH-ONLY (revert before merge, lint enforces):
WORKFLOW_SERVER_URL_OVERRIDE points at a multi-region backend preview
that includes the fix, so this validates cross-region stream
visibility end-to-end before multi-region serving is re-enabled.
Points the suite at an all-regions backend preview and widens coverage
from the wave-1 trio to every provisioned region:
- Explicit path: a single concurrent all-regions case starts one
tagged run per region (one shared cold-start window instead of 19
sequential ones) and aggregates per-region failures so a single
region's breakage reports alongside the full picture. The trio keeps
its detailed per-region cases and the 9-way concurrent-isolation
case.
- Implicit path: workbench gains a region-pinned
/api/e2e-region-implicit/<region> route per provisioned region (19
total, shared handler), the workbench itself now deploys to all of
them, and the test.each covers the full set with per-case timeouts
for regional cold starts.
- Multi-region CI job timeout 20m -> 35m for the sequential implicit
cases.
BRANCH-ONLY (revert before merge, lint enforces):
WORKFLOW_SERVER_URL_OVERRIDE now targets the all-regions backend
preview instead of the previous (stale, since-merged) fix preview.
The first all-regions run surfaced a subtle execution-locality
behavior: queue delivery is guaranteed to the tagged region's
dataplane and the delivery callback egresses from that region, but the
consumer invocation's execution region is chosen by where that
callback enters Vercel's edge — and adjacent regions can geo-resolve
to each other's functions. Observed live: kix1-tagged runs (callback
egressing from Osaka) deterministically executing in hnd1/Tokyo on
both the explicit and implicit paths, with tagging, data placement,
and completion all still strictly kix1.
expectRunInRegion now asserts execution lands in the tagged region OR
one of its geographic neighbors (EXECUTION_ADJACENCY), while run-ID
tagging and server-side completion remain strictly the requested
region. Gross misrouting (e.g. kix1 -> iad1) still fails.
… production
The all-regions workflow-server rollout is deployed and serving
production traffic from every Vercel region, so this branch's e2e no
longer needs to target a branch preview. Restores the empty override
the No Test Overrides lint enforces for merge.
With this the PR is complete: region-tagged run IDs, region-aware
queue routing, and the multi-region e2e suite (explicit + implicit +
all-regions + cross-region streams) all validate against the
production-default backends.

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 47 out of 48 changed files in this pull request and generated 3 comments.

Files not reviewed (1)
  • pnpm-lock.yaml: Generated file

Comment threadpackages/core/src/runtime/start.ts Outdated
Comment threadpackages/world-vercel/src/queue.ts
Comment threadpackages/world-vercel/src/events.ts Outdated
- start.ts: StartOptionsBase.region fallback is iad1, not the unknown
sentinel (createRunId always mints a concrete routable region)
- queue.ts: example used a nonexistent start({ runIdInput }) API; the
real option is start({ region })
- events.ts: decode() clears only the tag bit (top bit of the 48-bit
timestamp field) — it does not restore the original untagged ULID;
reword to say what actually matters for timestamp validation
Hooks are resolved by opaque token, which carries no region hint, so
lookup and resume must work regardless of which region owns the run's
data. Exercises the full follow-up-message path on sfo1- and
fra1-tagged runs: create inside the workflow, resolve by token from
the test process, resume twice sequentially, and assert payload order
and completion.
Regression coverage for the failure mode where the first message to a
hook-driven app on a non-iad1 run worked but every follow-up failed
with 'Hook not found'.
Conflict in packages/core/src/runtime/start.test.ts: both sides
appended a new top-level describe block at the same location — this
branch's 'createRunId' tests and main's 'queue namespace' tests.
Resolved by keeping both blocks verbatim (each is self-contained with
its own beforeEach/mocks). Lockfile reconciled with pnpm install.
Verified post-merge: core tsc clean, start/helpers tests 73 passed,
world-vercel 263 passed, full build 27/27.
@github-actionsgithub-actionsBot mentioned this pull request Jul 13, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Backport to stable failed for 9da2d76 due to a workflow error (backport job run).

This is usually an infrastructure problem (e.g. the configured AI model could not be found, an AI Gateway error, or an opencode crash) rather than a merge conflict. Check the job logs linked above for details.

Once the underlying issue is fixed, re-run the Backport to stable workflow manually via workflow_dispatch and paste this commit SHA into the ref input:

9da2d762604c2b73eb39f07fc0b069aea643e18d

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.

Support multi-region on Vercel backend

4 participants

@TooTallNate@VaguelySerious@saasjesus
, '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('^' + ".*" + ' [core][world][world-vercel] Add `World.createRunId()` and region-aware queue routing by TooTallNate · Pull Request #1981 · vercel/workflow · GitHub
Skip to content

[core][world][world-vercel] Add World.createRunId() and region-aware queue routing - #1981

Merged
TooTallNate merged 35 commits into
mainfrom
world-create-run-id
Jul 13, 2026
Merged

[core][world][world-vercel] Add World.createRunId() and region-aware queue routing#1981
TooTallNate merged 35 commits into
mainfrom
world-create-run-id

Conversation

@TooTallNate

@TooTallNateTooTallNate commented May 13, 2026

Copy link
Copy Markdown
Member

Closes#1931 and helps with #2708 (and possibly more)

Wires the tagged-ULID encoding introduced in #1978 into the workflow run lifecycle:

  1. @workflow/world — adds two extension points to the World interface:

    • createRunId?(input?: Record<string, unknown>): string — opt-in hook for worlds to mint custom run IDs (the returned value is the bare ULID; the core attaches the wrun_ prefix).
    • QueueOptions.region?: string — per-message routing hint that worlds can use to dispatch messages to a specific region.
  2. @workflow/corestart() now:

    • Accepts a new region option (StartOptionsBase.region).
    • Calls world.createRunId(opts) when the world provides one (forwarding the full options bag so worlds can read whichever fields they recognise), falling back to a monotonic ULID otherwise.
    • Threads opts.region onto the queue options so the initial workflow message is dispatched to the matching region.
  3. @workflow/world-vercel — implements both extension points:

    • createRunId(options?) mints region-tagged ULIDs. Region resolution order: options.regionVERCEL_REGION env var → iad1 default. A run ID is always tagged with a concrete, routable region; the unknown (0) sentinel is never minted.
    • The queue resolves its destination region from (in order): an explicit opts.region, the region embedded in the payload's tagged run ID, the VERCEL_REGION env var, and finally a hardcoded iad1 default — replacing the previous unconditional 'iad1' passed to @vercel/queue. In proxy mode the resolved region is forwarded as a routing header.

Monotonicity

encode() writes the 11 bits of region/version metadata into the top of the ULID's randomness section (all of byte 6 + the top 3 bits of byte 7), leaving the low 69 bits untouched. The underlying ulid monotonic factory's same-millisecond bottom-bit increments therefore survive encoding, and consecutive IDs with the same region/version metadata are naturally monotonic — no correction needed in the common case.

The one remaining hazard is the metadata changing between same-millisecond calls (a lower-numbered region sorts below a higher one at the same timestamp). For that, createRunId tracks the last emitted ID and, on collision, bumps the bit immediately above the metadata window (the lowest bit of the 48-bit timestamp) before re-stamping the requested region/version. The test suite covers 4096 same-ms mints plus same-ms region changes.

E2E coverage

Adds packages/core/e2e/e2e-region.test.ts (CI job e2e-vercel-multi-region, deployed to all 19 routable regions):

  • Explicit start({ region }) for iad1 / sfo1 / fra1, verifying run-ID tags, data placement, and execution region.
  • A 9-way and an all-19-regions concurrent batch verifying region isolation under parallel starts.
  • Implicit region tagging via region-pinned routes for each of the 19 regions (no explicit region option — VERCEL_REGION drives the tag).
  • A cross-region durable stream test (writer in iad1, mid-stream reader in sfo1).

Execution-region assertions use a small adjacency tolerance for geo-elastic execution (e.g. kix1 → hnd1); run-ID tags and data placement are always asserted strictly.

Encodes a tag bit, 5-bit version, and 6-bit Vercel region ID into a
ULID-shaped string used for workflow run IDs. Tagged values remain
valid 26-char Crockford-Base32 ULIDs so they still sort and round-trip
through any system that accepts ULIDs.
Add exact-string expectations for encoded outputs at known inputs,
covering the default region/version pair, numeric region IDs, version
overrides, boundary values (all-zero, all-max), the dirty-input
overwrite case, and the lexicographic-order checks. Also adds an
explicit byte-array expectation for the canonical ULID-spec example
string and an additional first-char-range coverage test for isTagged.
- isTaggedString now fully validates the input as a 26-char Crockford
Base32 ULID (delegating to ulidToBytes) instead of only inspecting
the first character. This fixes false positives on inputs like
'4UUUU...' that have a valid tag-bit position but invalid chars
later in the string.
- isTagged() now accepts `unknown` to match its documented behavior
of safely rejecting non-string inputs without requiring callers to
cast.
- Introduce `RegionKey` for the full set of keys including 'unknown',
and narrow `RegionCode` to `Exclude<RegionKey, 'unknown'>` so the
return type of `lookupRegion` and the `DecodedRunId.region` field
accurately reflect that 'unknown' is never produced. Updates
`encode` to reject 'unknown' as a region code string at runtime
(callers wanting the unknown sentinel should pass numeric 0).
…e queue routing
- @workflow/world: add optional createRunId(input?) to the World
interface so worlds can mint run IDs with embedded metadata, and
add an optional 'region' field to QueueOptions for per-message
routing hints.
- @workflow/core: start() now delegates run ID generation to
world.createRunId() when defined (falling back to a monotonic
ULID otherwise), and accepts a new 'runIdInput' option that is
forwarded verbatim to createRunId. When runIdInput.region is a
string, it is also threaded onto the queue options so the initial
workflow message is dispatched to the matching region.
- @workflow/world-vercel: implement createRunId() to mint
region-tagged ULIDs, preferring an explicit runIdInput.region and
falling back to the VERCEL_REGION env var. The queue now resolves
its destination region from (in order): an explicit opts.region,
the region embedded in the payload's tagged run ID, the
VERCEL_REGION env var, and finally a hardcoded 'iad1' default.
This replaces the previous unconditional 'iad1' region passed to
the @vercel/queue client.
Monotonicity within a process is preserved by tracking the last
emitted run ID and bumping the bit immediately above the 11-bit
metadata window when a same-ms collision would otherwise occur,
then re-stamping the requested region/version on top so metadata
remains stable.
@TooTallNate
TooTallNate requested a review from a team as a code ownerMay 13, 2026 17:47
CopilotAI review requested due to automatic review settings May 13, 2026 17:47
@vercel

vercelBot commented May 13, 2026

Copy link
Copy Markdown
Contributor

@changeset-bot

changeset-botBot commented May 13, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 4f77bc1

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

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

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

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production145302301683
✅ 💻 Local Development161702191836
✅ 📦 Local Production161702191836
✅ 🐘 Local Postgres161702191836
✅ 🪟 Windows15300153
✅ 📋 Other89401771071
✅ vercel-multi-region270027
Total7378010648442

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro126027
✅ example126027
✅ express126027
✅ fastify126027
✅ hono126027
✅ nextjs-turbopack15003
✅ nextjs-webpack15003
✅ nitro126027
✅ nuxt126027
✅ sveltekit14508
✅ vite126027
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack15300
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable128025
✅ e2e-local-dev-tanstack-start-128025
✅ e2e-local-postgres-nest-stable128025
✅ e2e-local-postgres-tanstack-start-128025
✅ e2e-local-prod-nest-stable128025
✅ e2e-local-prod-tanstack-start-128025
✅ e2e-vercel-prod-tanstack-start126027
✅ vercel-multi-region
AppPassedFailedSkipped
✅ nextjs-turbopack2700

📋 View full workflow run

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR extends the workflow runtime/world contract to support (1) world-minted run IDs and (2) region-aware queue routing, and wires those capabilities through @workflow/core and the Vercel world implementation.

Changes:

  • Add optional World.createRunId(input?) and QueueOptions.region in @workflow/world.
  • Update @workflow/corestart() to delegate run ID generation to world.createRunId(opts.runIdInput) when available, and to forward runIdInput.region to queue options.
  • Implement region-tagged run IDs + region-resolving queue routing in @workflow/world-vercel, including new tests and a new ulid dependency.

Reviewed changes

Copilot reviewed 13 out of 14 changed files in this pull request and generated 4 comments.

Show a summary per file
FileDescription
pnpm-lock.yamlAdds lockfile entry for ulid used by world-vercel.
packages/world/src/queue.tsExtends QueueOptions with a region routing hint + docs.
packages/world/src/interfaces.tsExtends World interface with optional createRunId.
packages/world-vercel/src/queue.tsAdds region resolution order (opts → tagged runId → env → fallback) and updates CBOR encode/decode imports.
packages/world-vercel/src/queue.test.tsAdds region-routing test coverage.
packages/world-vercel/src/index.tsWires createRunId into createVercelWorld and re-exports it.
packages/world-vercel/src/create-run-id.tsImplements region-tagged ULID minting with per-process monotonicity.
packages/world-vercel/src/create-run-id.test.tsTests for env/input fallback and monotonicity behavior.
packages/world-vercel/package.jsonAdds ulid dependency.
packages/core/src/runtime/start.tsUses world.createRunId when available; forwards runIdInput.region into queue options.
packages/core/src/runtime/start.test.tsAdds tests validating createRunId usage and region threading.
.changeset/world-vercel-create-run-id.mdRelease notes for new run ID minting + routing behavior in world-vercel.
.changeset/world-create-run-id.mdRelease notes for new @workflow/world interface fields.
.changeset/core-run-id-input.mdRelease notes for core start() runIdInput behavior.
Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

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

Comment threadpackages/world/src/queue.ts Outdated
Comment threadpackages/world-vercel/src/queue.ts
Comment threadpackages/world-vercel/src/queue.test.ts Outdated
Comment threadpackages/world-vercel/src/create-run-id.test.ts Outdated
…unId
Drop the dedicated 'runIdInput' field on StartOptions and forward the
entire options bag to world.createRunId() instead. This keeps the
public API surface smaller and lets each World pick the fields it
recognises (e.g. world-vercel reads 'region'). The top-level 'region'
option remains on StartOptionsBase and is also threaded onto the
queue's per-call region opt when set.
…ride
- queue: validate opts.region and VERCEL_REGION against the known region
table before routing, ignoring unrecognised codes so a bad override
can't clobber the payload-derived region (Copilot)
- add isKnownRegionCode() runtime guard to run-id/regions
- reset WORKFLOW_SERVER_URL_OVERRIDE to '' (must be empty on main)
- fold the within-PR iad1-default changeset into the main world-vercel
changeset and delete it (review)
- start.test: declare specVersion on createRunId mock worlds now that
the merged world-compatibility check requires it
- cover the new region-validation fall-through paths in queue.test
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@VaguelySerious

Copy link
Copy Markdown
Member

(AI) Deleted .changeset/run-id-default-iad1.md and folded its net user-facing behavior (the iad1 default when no region/env is available) into the main world-vercel-create-run-id changeset so the changelog stays accurate without the within-PR intermediate note.

…i-region preview
BRANCH-ONLY — revert the override to '' before merge (lint enforces).
Points this PR's e2e/benchmark runs at the wave-1 multi-region
workflow-server preview (vercel/workflow-server#590: iad1+sfo1+fra1
serving, staging data backends) so region-tagged runs are validated
against real multi-region serving end-to-end.
Also makes the unit-test mock origins in events-v4.test.ts and
trace-propagation.test.ts override-aware (same pattern the rest of
the file and utils.test.ts already use), so the suite passes whether
or not the override is set — these two files were the only spots
hardcoding https://vercel-workflow.com.
Conflicts: packages/world-vercel/src/index.ts — both sides added
imports/exports (createRunId from this branch, createAnalytics from
main #2698); kept both. Includes the TypeScript 6 workspace upgrade
(#2700) from main. world-vercel (249) and core (1356) tests pass.
Adds a dedicated e2e suite validating @workflow/world-vercel region
routing end to end, run as its own CI job (e2e-vercel-multi-region)
against the nextjs-turbopack workbench only — deliberately separate
from e2e.test.ts, which runs as a matrix across all worlds/frameworks
where Vercel-specific multi-region behavior doesn't apply.
- workbench/nextjs-turbopack/vercel.json: deploy to iad1+sfo1+fra1 so
region-routed flow messages have a function to land on in each region.
- workflows/99_e2e.ts: regionProbeWorkflow returns the VERCEL_REGION
observed by both the workflow and a step, so tests can assert the run
EXECUTED in the intended region (not just that it was tagged).
- packages/core/e2e/e2e-region.test.ts: per-region cases assert
1) start(..., { region }) mints a region-tagged run ID (decoded via
@workflow/world-vercel/run-id),
2) the workflow + step both observed VERCEL_REGION === region,
3) the server reports the run completed;
plus a concurrent all-regions case guarding against cross-region
misrouting under simultaneous multi-region traffic. Skips on local
deployments.
- .github/workflows/tests.yml: new e2e-vercel-multi-region job
mirroring e2e-vercel-prod's env/deployment-wait, running only the
new suite.
The first multi-region CI run surfaced two issues:
1. sfo1/fra1-tagged runs executed in iad1. The suite started runs from
the external test process, which uses the api.vercel.com token proxy
— and the proxy's queues path forwards every send to the region-less
VQS host (the world's proxy-mode resolveBaseUrl ignores the region
argument, and the proxy's x-vercel-vqs-api-url escape hatch only
allowlists vqs-server-*.vercel.sh preview hosts). Production traffic
publishes IN-FUNCTION (direct regional queue routing), so the suite
now triggers start() through a new workbench route
(/api/e2e-region-start) and rehydrates the run with getRun() —
testing the path production actually takes. Proxy-mode regional
queue routing is a known gap to address separately in api-workflow.
2. TypeError on world.runs.get: getWorld() is async and was called
without await.
Textually clean but semantically conflicting with #2789: main added the
proxy-mode x-vercel-queue-region header in createQueue's shared
clientOptions, referencing the static region const this branch removed
in favor of per-send resolution. Resolution: the header now rides the
per-send QueueClient construction, carrying resolveTargetRegion's
result — proxied sends route to the region resolved per message
(opts.region -> run-ID tag -> VERCEL_REGION -> iad1).
world-vercel: 253/253 (includes #2789's proxy-header tests, which hold
under per-send resolution).
…ion suite
With regional VQS routing now working through the api.vercel.com proxy
(vercel/api#79056 + #2789 + this branch's per-send region resolution),
the suite covers both start configurations, asserting the same three
properties for each (region-tagged run ID, execution in the intended
region via VERCEL_REGION echoed in the return value, server-side
completion):
1. EXPLICIT: start(..., { region }) called directly in the vitest
runner — publishes through the token proxy, per-send region carried
by x-vercel-queue-region. Restores the direct-start shape the suite
had originally, plus the concurrent all-regions case.
2. IMPLICIT: dedicated per-region workbench routes
(/api/e2e-region-implicit/{iad1,sfo1,fra1}), each pinned to a single
region via a per-function 'regions' entry in the workbench
vercel.json, calling start() with NO region option — createRunId
derives the tag from the minting function's VERCEL_REGION. The test
also asserts the route reported executing in its pinned region, so
the implicit-tagging assertion can't pass vacuously.
Replaces the interim /api/e2e-region-start route (explicit region via
request body), which existed to work around the pre-#79056 proxy gap.
…ng is in production
workflow-server#590 (iad1+sfo1+fra1 serving) merged and deployed to
production and the e2e backend, so this branch's e2e/benchmark runs no
longer need to target the wave-1 preview. Restores the empty override
the No Test Overrides lint job enforces for merge.
The override-aware unit-test origins (events-v4/trace-propagation)
stay — they are correct under any override value.
@saasjesus

Copy link
Copy Markdown

damn its happening

Regression coverage for a backend bug that made cross-region stream
reads report zero chunks on IN-PROGRESS streams (completed streams were
unaffected), which forced the multi-region serving rollback.
The new case exercises exactly that geometry:
- crossRegionStreamWorkflow (99_e2e.ts) writes N chunks to the default
output stream, then holds the stream OPEN for 45s before closing —
the in-progress window is the point, since completed streams are the
easy case.
- The e2e starts it with region iad1, waits (same-region, via the
api.vercel.com proxy) until all chunks are written, asserts the run
is still 'running', then reads through a new sfo1-pinned workbench
route (/api/e2e-stream-read/sfo1) that returns getTailIndex() plus
its VERCEL_REGION. The reader's region served none of the stream's
writes, so the reported chunk count must come from the backend's
cross-region stream metadata. The test fails loudly if the route
isn't actually executing in sfo1.
Also bumps the explicit-region test timeout to 120s: the first case in
the file absorbs every cold start at once (fresh workbench instances
in up to three regions plus a cold backend preview) and was observed
just over the 60s default.
BRANCH-ONLY (revert before merge, lint enforces):
WORKFLOW_SERVER_URL_OVERRIDE points at a multi-region backend preview
that includes the fix, so this validates cross-region stream
visibility end-to-end before multi-region serving is re-enabled.
Points the suite at an all-regions backend preview and widens coverage
from the wave-1 trio to every provisioned region:
- Explicit path: a single concurrent all-regions case starts one
tagged run per region (one shared cold-start window instead of 19
sequential ones) and aggregates per-region failures so a single
region's breakage reports alongside the full picture. The trio keeps
its detailed per-region cases and the 9-way concurrent-isolation
case.
- Implicit path: workbench gains a region-pinned
/api/e2e-region-implicit/<region> route per provisioned region (19
total, shared handler), the workbench itself now deploys to all of
them, and the test.each covers the full set with per-case timeouts
for regional cold starts.
- Multi-region CI job timeout 20m -> 35m for the sequential implicit
cases.
BRANCH-ONLY (revert before merge, lint enforces):
WORKFLOW_SERVER_URL_OVERRIDE now targets the all-regions backend
preview instead of the previous (stale, since-merged) fix preview.
The first all-regions run surfaced a subtle execution-locality
behavior: queue delivery is guaranteed to the tagged region's
dataplane and the delivery callback egresses from that region, but the
consumer invocation's execution region is chosen by where that
callback enters Vercel's edge — and adjacent regions can geo-resolve
to each other's functions. Observed live: kix1-tagged runs (callback
egressing from Osaka) deterministically executing in hnd1/Tokyo on
both the explicit and implicit paths, with tagging, data placement,
and completion all still strictly kix1.
expectRunInRegion now asserts execution lands in the tagged region OR
one of its geographic neighbors (EXECUTION_ADJACENCY), while run-ID
tagging and server-side completion remain strictly the requested
region. Gross misrouting (e.g. kix1 -> iad1) still fails.
… production
The all-regions workflow-server rollout is deployed and serving
production traffic from every Vercel region, so this branch's e2e no
longer needs to target a branch preview. Restores the empty override
the No Test Overrides lint enforces for merge.
With this the PR is complete: region-tagged run IDs, region-aware
queue routing, and the multi-region e2e suite (explicit + implicit +
all-regions + cross-region streams) all validate against the
production-default backends.

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 47 out of 48 changed files in this pull request and generated 3 comments.

Files not reviewed (1)
  • pnpm-lock.yaml: Generated file

Comment threadpackages/core/src/runtime/start.ts Outdated
Comment threadpackages/world-vercel/src/queue.ts
Comment threadpackages/world-vercel/src/events.ts Outdated
- start.ts: StartOptionsBase.region fallback is iad1, not the unknown
sentinel (createRunId always mints a concrete routable region)
- queue.ts: example used a nonexistent start({ runIdInput }) API; the
real option is start({ region })
- events.ts: decode() clears only the tag bit (top bit of the 48-bit
timestamp field) — it does not restore the original untagged ULID;
reword to say what actually matters for timestamp validation
Hooks are resolved by opaque token, which carries no region hint, so
lookup and resume must work regardless of which region owns the run's
data. Exercises the full follow-up-message path on sfo1- and
fra1-tagged runs: create inside the workflow, resolve by token from
the test process, resume twice sequentially, and assert payload order
and completion.
Regression coverage for the failure mode where the first message to a
hook-driven app on a non-iad1 run worked but every follow-up failed
with 'Hook not found'.
Conflict in packages/core/src/runtime/start.test.ts: both sides
appended a new top-level describe block at the same location — this
branch's 'createRunId' tests and main's 'queue namespace' tests.
Resolved by keeping both blocks verbatim (each is self-contained with
its own beforeEach/mocks). Lockfile reconciled with pnpm install.
Verified post-merge: core tsc clean, start/helpers tests 73 passed,
world-vercel 263 passed, full build 27/27.
@github-actionsgithub-actionsBot mentioned this pull request Jul 13, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Backport to stable failed for 9da2d76 due to a workflow error (backport job run).

This is usually an infrastructure problem (e.g. the configured AI model could not be found, an AI Gateway error, or an opencode crash) rather than a merge conflict. Check the job logs linked above for details.

Once the underlying issue is fixed, re-run the Backport to stable workflow manually via workflow_dispatch and paste this commit SHA into the ref input:

9da2d762604c2b73eb39f07fc0b069aea643e18d

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.

Support multi-region on Vercel backend

4 participants

@TooTallNate@VaguelySerious@saasjesus
, '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('^' + ".*" + ' [core][world][world-vercel] Add `World.createRunId()` and region-aware queue routing by TooTallNate · Pull Request #1981 · vercel/workflow · GitHub
Skip to content

[core][world][world-vercel] Add World.createRunId() and region-aware queue routing - #1981

Merged
TooTallNate merged 35 commits into
mainfrom
world-create-run-id
Jul 13, 2026
Merged

[core][world][world-vercel] Add World.createRunId() and region-aware queue routing#1981
TooTallNate merged 35 commits into
mainfrom
world-create-run-id

Conversation

@TooTallNate

@TooTallNateTooTallNate commented May 13, 2026

Copy link
Copy Markdown
Member

Closes#1931 and helps with #2708 (and possibly more)

Wires the tagged-ULID encoding introduced in #1978 into the workflow run lifecycle:

  1. @workflow/world — adds two extension points to the World interface:

    • createRunId?(input?: Record<string, unknown>): string — opt-in hook for worlds to mint custom run IDs (the returned value is the bare ULID; the core attaches the wrun_ prefix).
    • QueueOptions.region?: string — per-message routing hint that worlds can use to dispatch messages to a specific region.
  2. @workflow/corestart() now:

    • Accepts a new region option (StartOptionsBase.region).
    • Calls world.createRunId(opts) when the world provides one (forwarding the full options bag so worlds can read whichever fields they recognise), falling back to a monotonic ULID otherwise.
    • Threads opts.region onto the queue options so the initial workflow message is dispatched to the matching region.
  3. @workflow/world-vercel — implements both extension points:

    • createRunId(options?) mints region-tagged ULIDs. Region resolution order: options.regionVERCEL_REGION env var → iad1 default. A run ID is always tagged with a concrete, routable region; the unknown (0) sentinel is never minted.
    • The queue resolves its destination region from (in order): an explicit opts.region, the region embedded in the payload's tagged run ID, the VERCEL_REGION env var, and finally a hardcoded iad1 default — replacing the previous unconditional 'iad1' passed to @vercel/queue. In proxy mode the resolved region is forwarded as a routing header.

Monotonicity

encode() writes the 11 bits of region/version metadata into the top of the ULID's randomness section (all of byte 6 + the top 3 bits of byte 7), leaving the low 69 bits untouched. The underlying ulid monotonic factory's same-millisecond bottom-bit increments therefore survive encoding, and consecutive IDs with the same region/version metadata are naturally monotonic — no correction needed in the common case.

The one remaining hazard is the metadata changing between same-millisecond calls (a lower-numbered region sorts below a higher one at the same timestamp). For that, createRunId tracks the last emitted ID and, on collision, bumps the bit immediately above the metadata window (the lowest bit of the 48-bit timestamp) before re-stamping the requested region/version. The test suite covers 4096 same-ms mints plus same-ms region changes.

E2E coverage

Adds packages/core/e2e/e2e-region.test.ts (CI job e2e-vercel-multi-region, deployed to all 19 routable regions):

  • Explicit start({ region }) for iad1 / sfo1 / fra1, verifying run-ID tags, data placement, and execution region.
  • A 9-way and an all-19-regions concurrent batch verifying region isolation under parallel starts.
  • Implicit region tagging via region-pinned routes for each of the 19 regions (no explicit region option — VERCEL_REGION drives the tag).
  • A cross-region durable stream test (writer in iad1, mid-stream reader in sfo1).

Execution-region assertions use a small adjacency tolerance for geo-elastic execution (e.g. kix1 → hnd1); run-ID tags and data placement are always asserted strictly.

Encodes a tag bit, 5-bit version, and 6-bit Vercel region ID into a
ULID-shaped string used for workflow run IDs. Tagged values remain
valid 26-char Crockford-Base32 ULIDs so they still sort and round-trip
through any system that accepts ULIDs.
Add exact-string expectations for encoded outputs at known inputs,
covering the default region/version pair, numeric region IDs, version
overrides, boundary values (all-zero, all-max), the dirty-input
overwrite case, and the lexicographic-order checks. Also adds an
explicit byte-array expectation for the canonical ULID-spec example
string and an additional first-char-range coverage test for isTagged.
- isTaggedString now fully validates the input as a 26-char Crockford
Base32 ULID (delegating to ulidToBytes) instead of only inspecting
the first character. This fixes false positives on inputs like
'4UUUU...' that have a valid tag-bit position but invalid chars
later in the string.
- isTagged() now accepts `unknown` to match its documented behavior
of safely rejecting non-string inputs without requiring callers to
cast.
- Introduce `RegionKey` for the full set of keys including 'unknown',
and narrow `RegionCode` to `Exclude<RegionKey, 'unknown'>` so the
return type of `lookupRegion` and the `DecodedRunId.region` field
accurately reflect that 'unknown' is never produced. Updates
`encode` to reject 'unknown' as a region code string at runtime
(callers wanting the unknown sentinel should pass numeric 0).
…e queue routing
- @workflow/world: add optional createRunId(input?) to the World
interface so worlds can mint run IDs with embedded metadata, and
add an optional 'region' field to QueueOptions for per-message
routing hints.
- @workflow/core: start() now delegates run ID generation to
world.createRunId() when defined (falling back to a monotonic
ULID otherwise), and accepts a new 'runIdInput' option that is
forwarded verbatim to createRunId. When runIdInput.region is a
string, it is also threaded onto the queue options so the initial
workflow message is dispatched to the matching region.
- @workflow/world-vercel: implement createRunId() to mint
region-tagged ULIDs, preferring an explicit runIdInput.region and
falling back to the VERCEL_REGION env var. The queue now resolves
its destination region from (in order): an explicit opts.region,
the region embedded in the payload's tagged run ID, the
VERCEL_REGION env var, and finally a hardcoded 'iad1' default.
This replaces the previous unconditional 'iad1' region passed to
the @vercel/queue client.
Monotonicity within a process is preserved by tracking the last
emitted run ID and bumping the bit immediately above the 11-bit
metadata window when a same-ms collision would otherwise occur,
then re-stamping the requested region/version on top so metadata
remains stable.
@TooTallNate
TooTallNate requested a review from a team as a code ownerMay 13, 2026 17:47
CopilotAI review requested due to automatic review settings May 13, 2026 17:47
@vercel

vercelBot commented May 13, 2026

Copy link
Copy Markdown
Contributor

@changeset-bot

changeset-botBot commented May 13, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 4f77bc1

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

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

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

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production145302301683
✅ 💻 Local Development161702191836
✅ 📦 Local Production161702191836
✅ 🐘 Local Postgres161702191836
✅ 🪟 Windows15300153
✅ 📋 Other89401771071
✅ vercel-multi-region270027
Total7378010648442

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro126027
✅ example126027
✅ express126027
✅ fastify126027
✅ hono126027
✅ nextjs-turbopack15003
✅ nextjs-webpack15003
✅ nitro126027
✅ nuxt126027
✅ sveltekit14508
✅ vite126027
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack15300
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable128025
✅ e2e-local-dev-tanstack-start-128025
✅ e2e-local-postgres-nest-stable128025
✅ e2e-local-postgres-tanstack-start-128025
✅ e2e-local-prod-nest-stable128025
✅ e2e-local-prod-tanstack-start-128025
✅ e2e-vercel-prod-tanstack-start126027
✅ vercel-multi-region
AppPassedFailedSkipped
✅ nextjs-turbopack2700

📋 View full workflow run

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR extends the workflow runtime/world contract to support (1) world-minted run IDs and (2) region-aware queue routing, and wires those capabilities through @workflow/core and the Vercel world implementation.

Changes:

  • Add optional World.createRunId(input?) and QueueOptions.region in @workflow/world.
  • Update @workflow/corestart() to delegate run ID generation to world.createRunId(opts.runIdInput) when available, and to forward runIdInput.region to queue options.
  • Implement region-tagged run IDs + region-resolving queue routing in @workflow/world-vercel, including new tests and a new ulid dependency.

Reviewed changes

Copilot reviewed 13 out of 14 changed files in this pull request and generated 4 comments.

Show a summary per file
FileDescription
pnpm-lock.yamlAdds lockfile entry for ulid used by world-vercel.
packages/world/src/queue.tsExtends QueueOptions with a region routing hint + docs.
packages/world/src/interfaces.tsExtends World interface with optional createRunId.
packages/world-vercel/src/queue.tsAdds region resolution order (opts → tagged runId → env → fallback) and updates CBOR encode/decode imports.
packages/world-vercel/src/queue.test.tsAdds region-routing test coverage.
packages/world-vercel/src/index.tsWires createRunId into createVercelWorld and re-exports it.
packages/world-vercel/src/create-run-id.tsImplements region-tagged ULID minting with per-process monotonicity.
packages/world-vercel/src/create-run-id.test.tsTests for env/input fallback and monotonicity behavior.
packages/world-vercel/package.jsonAdds ulid dependency.
packages/core/src/runtime/start.tsUses world.createRunId when available; forwards runIdInput.region into queue options.
packages/core/src/runtime/start.test.tsAdds tests validating createRunId usage and region threading.
.changeset/world-vercel-create-run-id.mdRelease notes for new run ID minting + routing behavior in world-vercel.
.changeset/world-create-run-id.mdRelease notes for new @workflow/world interface fields.
.changeset/core-run-id-input.mdRelease notes for core start() runIdInput behavior.
Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

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

Comment threadpackages/world/src/queue.ts Outdated
Comment threadpackages/world-vercel/src/queue.ts
Comment threadpackages/world-vercel/src/queue.test.ts Outdated
Comment threadpackages/world-vercel/src/create-run-id.test.ts Outdated
…unId
Drop the dedicated 'runIdInput' field on StartOptions and forward the
entire options bag to world.createRunId() instead. This keeps the
public API surface smaller and lets each World pick the fields it
recognises (e.g. world-vercel reads 'region'). The top-level 'region'
option remains on StartOptionsBase and is also threaded onto the
queue's per-call region opt when set.
…ride
- queue: validate opts.region and VERCEL_REGION against the known region
table before routing, ignoring unrecognised codes so a bad override
can't clobber the payload-derived region (Copilot)
- add isKnownRegionCode() runtime guard to run-id/regions
- reset WORKFLOW_SERVER_URL_OVERRIDE to '' (must be empty on main)
- fold the within-PR iad1-default changeset into the main world-vercel
changeset and delete it (review)
- start.test: declare specVersion on createRunId mock worlds now that
the merged world-compatibility check requires it
- cover the new region-validation fall-through paths in queue.test
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@VaguelySerious

Copy link
Copy Markdown
Member

(AI) Deleted .changeset/run-id-default-iad1.md and folded its net user-facing behavior (the iad1 default when no region/env is available) into the main world-vercel-create-run-id changeset so the changelog stays accurate without the within-PR intermediate note.

…i-region preview
BRANCH-ONLY — revert the override to '' before merge (lint enforces).
Points this PR's e2e/benchmark runs at the wave-1 multi-region
workflow-server preview (vercel/workflow-server#590: iad1+sfo1+fra1
serving, staging data backends) so region-tagged runs are validated
against real multi-region serving end-to-end.
Also makes the unit-test mock origins in events-v4.test.ts and
trace-propagation.test.ts override-aware (same pattern the rest of
the file and utils.test.ts already use), so the suite passes whether
or not the override is set — these two files were the only spots
hardcoding https://vercel-workflow.com.
Conflicts: packages/world-vercel/src/index.ts — both sides added
imports/exports (createRunId from this branch, createAnalytics from
main #2698); kept both. Includes the TypeScript 6 workspace upgrade
(#2700) from main. world-vercel (249) and core (1356) tests pass.
Adds a dedicated e2e suite validating @workflow/world-vercel region
routing end to end, run as its own CI job (e2e-vercel-multi-region)
against the nextjs-turbopack workbench only — deliberately separate
from e2e.test.ts, which runs as a matrix across all worlds/frameworks
where Vercel-specific multi-region behavior doesn't apply.
- workbench/nextjs-turbopack/vercel.json: deploy to iad1+sfo1+fra1 so
region-routed flow messages have a function to land on in each region.
- workflows/99_e2e.ts: regionProbeWorkflow returns the VERCEL_REGION
observed by both the workflow and a step, so tests can assert the run
EXECUTED in the intended region (not just that it was tagged).
- packages/core/e2e/e2e-region.test.ts: per-region cases assert
1) start(..., { region }) mints a region-tagged run ID (decoded via
@workflow/world-vercel/run-id),
2) the workflow + step both observed VERCEL_REGION === region,
3) the server reports the run completed;
plus a concurrent all-regions case guarding against cross-region
misrouting under simultaneous multi-region traffic. Skips on local
deployments.
- .github/workflows/tests.yml: new e2e-vercel-multi-region job
mirroring e2e-vercel-prod's env/deployment-wait, running only the
new suite.
The first multi-region CI run surfaced two issues:
1. sfo1/fra1-tagged runs executed in iad1. The suite started runs from
the external test process, which uses the api.vercel.com token proxy
— and the proxy's queues path forwards every send to the region-less
VQS host (the world's proxy-mode resolveBaseUrl ignores the region
argument, and the proxy's x-vercel-vqs-api-url escape hatch only
allowlists vqs-server-*.vercel.sh preview hosts). Production traffic
publishes IN-FUNCTION (direct regional queue routing), so the suite
now triggers start() through a new workbench route
(/api/e2e-region-start) and rehydrates the run with getRun() —
testing the path production actually takes. Proxy-mode regional
queue routing is a known gap to address separately in api-workflow.
2. TypeError on world.runs.get: getWorld() is async and was called
without await.
Textually clean but semantically conflicting with #2789: main added the
proxy-mode x-vercel-queue-region header in createQueue's shared
clientOptions, referencing the static region const this branch removed
in favor of per-send resolution. Resolution: the header now rides the
per-send QueueClient construction, carrying resolveTargetRegion's
result — proxied sends route to the region resolved per message
(opts.region -> run-ID tag -> VERCEL_REGION -> iad1).
world-vercel: 253/253 (includes #2789's proxy-header tests, which hold
under per-send resolution).
…ion suite
With regional VQS routing now working through the api.vercel.com proxy
(vercel/api#79056 + #2789 + this branch's per-send region resolution),
the suite covers both start configurations, asserting the same three
properties for each (region-tagged run ID, execution in the intended
region via VERCEL_REGION echoed in the return value, server-side
completion):
1. EXPLICIT: start(..., { region }) called directly in the vitest
runner — publishes through the token proxy, per-send region carried
by x-vercel-queue-region. Restores the direct-start shape the suite
had originally, plus the concurrent all-regions case.
2. IMPLICIT: dedicated per-region workbench routes
(/api/e2e-region-implicit/{iad1,sfo1,fra1}), each pinned to a single
region via a per-function 'regions' entry in the workbench
vercel.json, calling start() with NO region option — createRunId
derives the tag from the minting function's VERCEL_REGION. The test
also asserts the route reported executing in its pinned region, so
the implicit-tagging assertion can't pass vacuously.
Replaces the interim /api/e2e-region-start route (explicit region via
request body), which existed to work around the pre-#79056 proxy gap.
…ng is in production
workflow-server#590 (iad1+sfo1+fra1 serving) merged and deployed to
production and the e2e backend, so this branch's e2e/benchmark runs no
longer need to target the wave-1 preview. Restores the empty override
the No Test Overrides lint job enforces for merge.
The override-aware unit-test origins (events-v4/trace-propagation)
stay — they are correct under any override value.
@saasjesus

Copy link
Copy Markdown

damn its happening

Regression coverage for a backend bug that made cross-region stream
reads report zero chunks on IN-PROGRESS streams (completed streams were
unaffected), which forced the multi-region serving rollback.
The new case exercises exactly that geometry:
- crossRegionStreamWorkflow (99_e2e.ts) writes N chunks to the default
output stream, then holds the stream OPEN for 45s before closing —
the in-progress window is the point, since completed streams are the
easy case.
- The e2e starts it with region iad1, waits (same-region, via the
api.vercel.com proxy) until all chunks are written, asserts the run
is still 'running', then reads through a new sfo1-pinned workbench
route (/api/e2e-stream-read/sfo1) that returns getTailIndex() plus
its VERCEL_REGION. The reader's region served none of the stream's
writes, so the reported chunk count must come from the backend's
cross-region stream metadata. The test fails loudly if the route
isn't actually executing in sfo1.
Also bumps the explicit-region test timeout to 120s: the first case in
the file absorbs every cold start at once (fresh workbench instances
in up to three regions plus a cold backend preview) and was observed
just over the 60s default.
BRANCH-ONLY (revert before merge, lint enforces):
WORKFLOW_SERVER_URL_OVERRIDE points at a multi-region backend preview
that includes the fix, so this validates cross-region stream
visibility end-to-end before multi-region serving is re-enabled.
Points the suite at an all-regions backend preview and widens coverage
from the wave-1 trio to every provisioned region:
- Explicit path: a single concurrent all-regions case starts one
tagged run per region (one shared cold-start window instead of 19
sequential ones) and aggregates per-region failures so a single
region's breakage reports alongside the full picture. The trio keeps
its detailed per-region cases and the 9-way concurrent-isolation
case.
- Implicit path: workbench gains a region-pinned
/api/e2e-region-implicit/<region> route per provisioned region (19
total, shared handler), the workbench itself now deploys to all of
them, and the test.each covers the full set with per-case timeouts
for regional cold starts.
- Multi-region CI job timeout 20m -> 35m for the sequential implicit
cases.
BRANCH-ONLY (revert before merge, lint enforces):
WORKFLOW_SERVER_URL_OVERRIDE now targets the all-regions backend
preview instead of the previous (stale, since-merged) fix preview.
The first all-regions run surfaced a subtle execution-locality
behavior: queue delivery is guaranteed to the tagged region's
dataplane and the delivery callback egresses from that region, but the
consumer invocation's execution region is chosen by where that
callback enters Vercel's edge — and adjacent regions can geo-resolve
to each other's functions. Observed live: kix1-tagged runs (callback
egressing from Osaka) deterministically executing in hnd1/Tokyo on
both the explicit and implicit paths, with tagging, data placement,
and completion all still strictly kix1.
expectRunInRegion now asserts execution lands in the tagged region OR
one of its geographic neighbors (EXECUTION_ADJACENCY), while run-ID
tagging and server-side completion remain strictly the requested
region. Gross misrouting (e.g. kix1 -> iad1) still fails.
… production
The all-regions workflow-server rollout is deployed and serving
production traffic from every Vercel region, so this branch's e2e no
longer needs to target a branch preview. Restores the empty override
the No Test Overrides lint enforces for merge.
With this the PR is complete: region-tagged run IDs, region-aware
queue routing, and the multi-region e2e suite (explicit + implicit +
all-regions + cross-region streams) all validate against the
production-default backends.

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 47 out of 48 changed files in this pull request and generated 3 comments.

Files not reviewed (1)
  • pnpm-lock.yaml: Generated file

Comment threadpackages/core/src/runtime/start.ts Outdated
Comment threadpackages/world-vercel/src/queue.ts
Comment threadpackages/world-vercel/src/events.ts Outdated
- start.ts: StartOptionsBase.region fallback is iad1, not the unknown
sentinel (createRunId always mints a concrete routable region)
- queue.ts: example used a nonexistent start({ runIdInput }) API; the
real option is start({ region })
- events.ts: decode() clears only the tag bit (top bit of the 48-bit
timestamp field) — it does not restore the original untagged ULID;
reword to say what actually matters for timestamp validation
Hooks are resolved by opaque token, which carries no region hint, so
lookup and resume must work regardless of which region owns the run's
data. Exercises the full follow-up-message path on sfo1- and
fra1-tagged runs: create inside the workflow, resolve by token from
the test process, resume twice sequentially, and assert payload order
and completion.
Regression coverage for the failure mode where the first message to a
hook-driven app on a non-iad1 run worked but every follow-up failed
with 'Hook not found'.
Conflict in packages/core/src/runtime/start.test.ts: both sides
appended a new top-level describe block at the same location — this
branch's 'createRunId' tests and main's 'queue namespace' tests.
Resolved by keeping both blocks verbatim (each is self-contained with
its own beforeEach/mocks). Lockfile reconciled with pnpm install.
Verified post-merge: core tsc clean, start/helpers tests 73 passed,
world-vercel 263 passed, full build 27/27.
@github-actionsgithub-actionsBot mentioned this pull request Jul 13, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Backport to stable failed for 9da2d76 due to a workflow error (backport job run).

This is usually an infrastructure problem (e.g. the configured AI model could not be found, an AI Gateway error, or an opencode crash) rather than a merge conflict. Check the job logs linked above for details.

Once the underlying issue is fixed, re-run the Backport to stable workflow manually via workflow_dispatch and paste this commit SHA into the ref input:

9da2d762604c2b73eb39f07fc0b069aea643e18d

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.

Support multi-region on Vercel backend

4 participants

@TooTallNate@VaguelySerious@saasjesus
, '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); } })(); })(); [core][world][world-vercel] Add `World.createRunId()` and region-aware queue routing by TooTallNate · Pull Request #1981 · vercel/workflow · GitHub
Skip to content

[core][world][world-vercel] Add World.createRunId() and region-aware queue routing - #1981

Merged
TooTallNate merged 35 commits into
mainfrom
world-create-run-id
Jul 13, 2026
Merged

[core][world][world-vercel] Add World.createRunId() and region-aware queue routing#1981
TooTallNate merged 35 commits into
mainfrom
world-create-run-id

Conversation

@TooTallNate

@TooTallNateTooTallNate commented May 13, 2026

Copy link
Copy Markdown
Member

Closes#1931 and helps with #2708 (and possibly more)

Wires the tagged-ULID encoding introduced in #1978 into the workflow run lifecycle:

  1. @workflow/world — adds two extension points to the World interface:

    • createRunId?(input?: Record<string, unknown>): string — opt-in hook for worlds to mint custom run IDs (the returned value is the bare ULID; the core attaches the wrun_ prefix).
    • QueueOptions.region?: string — per-message routing hint that worlds can use to dispatch messages to a specific region.
  2. @workflow/corestart() now:

    • Accepts a new region option (StartOptionsBase.region).
    • Calls world.createRunId(opts) when the world provides one (forwarding the full options bag so worlds can read whichever fields they recognise), falling back to a monotonic ULID otherwise.
    • Threads opts.region onto the queue options so the initial workflow message is dispatched to the matching region.
  3. @workflow/world-vercel — implements both extension points:

    • createRunId(options?) mints region-tagged ULIDs. Region resolution order: options.regionVERCEL_REGION env var → iad1 default. A run ID is always tagged with a concrete, routable region; the unknown (0) sentinel is never minted.
    • The queue resolves its destination region from (in order): an explicit opts.region, the region embedded in the payload's tagged run ID, the VERCEL_REGION env var, and finally a hardcoded iad1 default — replacing the previous unconditional 'iad1' passed to @vercel/queue. In proxy mode the resolved region is forwarded as a routing header.

Monotonicity

encode() writes the 11 bits of region/version metadata into the top of the ULID's randomness section (all of byte 6 + the top 3 bits of byte 7), leaving the low 69 bits untouched. The underlying ulid monotonic factory's same-millisecond bottom-bit increments therefore survive encoding, and consecutive IDs with the same region/version metadata are naturally monotonic — no correction needed in the common case.

The one remaining hazard is the metadata changing between same-millisecond calls (a lower-numbered region sorts below a higher one at the same timestamp). For that, createRunId tracks the last emitted ID and, on collision, bumps the bit immediately above the metadata window (the lowest bit of the 48-bit timestamp) before re-stamping the requested region/version. The test suite covers 4096 same-ms mints plus same-ms region changes.

E2E coverage

Adds packages/core/e2e/e2e-region.test.ts (CI job e2e-vercel-multi-region, deployed to all 19 routable regions):

  • Explicit start({ region }) for iad1 / sfo1 / fra1, verifying run-ID tags, data placement, and execution region.
  • A 9-way and an all-19-regions concurrent batch verifying region isolation under parallel starts.
  • Implicit region tagging via region-pinned routes for each of the 19 regions (no explicit region option — VERCEL_REGION drives the tag).
  • A cross-region durable stream test (writer in iad1, mid-stream reader in sfo1).

Execution-region assertions use a small adjacency tolerance for geo-elastic execution (e.g. kix1 → hnd1); run-ID tags and data placement are always asserted strictly.

Encodes a tag bit, 5-bit version, and 6-bit Vercel region ID into a
ULID-shaped string used for workflow run IDs. Tagged values remain
valid 26-char Crockford-Base32 ULIDs so they still sort and round-trip
through any system that accepts ULIDs.
Add exact-string expectations for encoded outputs at known inputs,
covering the default region/version pair, numeric region IDs, version
overrides, boundary values (all-zero, all-max), the dirty-input
overwrite case, and the lexicographic-order checks. Also adds an
explicit byte-array expectation for the canonical ULID-spec example
string and an additional first-char-range coverage test for isTagged.
- isTaggedString now fully validates the input as a 26-char Crockford
Base32 ULID (delegating to ulidToBytes) instead of only inspecting
the first character. This fixes false positives on inputs like
'4UUUU...' that have a valid tag-bit position but invalid chars
later in the string.
- isTagged() now accepts `unknown` to match its documented behavior
of safely rejecting non-string inputs without requiring callers to
cast.
- Introduce `RegionKey` for the full set of keys including 'unknown',
and narrow `RegionCode` to `Exclude<RegionKey, 'unknown'>` so the
return type of `lookupRegion` and the `DecodedRunId.region` field
accurately reflect that 'unknown' is never produced. Updates
`encode` to reject 'unknown' as a region code string at runtime
(callers wanting the unknown sentinel should pass numeric 0).
…e queue routing
- @workflow/world: add optional createRunId(input?) to the World
interface so worlds can mint run IDs with embedded metadata, and
add an optional 'region' field to QueueOptions for per-message
routing hints.
- @workflow/core: start() now delegates run ID generation to
world.createRunId() when defined (falling back to a monotonic
ULID otherwise), and accepts a new 'runIdInput' option that is
forwarded verbatim to createRunId. When runIdInput.region is a
string, it is also threaded onto the queue options so the initial
workflow message is dispatched to the matching region.
- @workflow/world-vercel: implement createRunId() to mint
region-tagged ULIDs, preferring an explicit runIdInput.region and
falling back to the VERCEL_REGION env var. The queue now resolves
its destination region from (in order): an explicit opts.region,
the region embedded in the payload's tagged run ID, the
VERCEL_REGION env var, and finally a hardcoded 'iad1' default.
This replaces the previous unconditional 'iad1' region passed to
the @vercel/queue client.
Monotonicity within a process is preserved by tracking the last
emitted run ID and bumping the bit immediately above the 11-bit
metadata window when a same-ms collision would otherwise occur,
then re-stamping the requested region/version on top so metadata
remains stable.
@TooTallNate
TooTallNate requested a review from a team as a code ownerMay 13, 2026 17:47
CopilotAI review requested due to automatic review settings May 13, 2026 17:47
@vercel

vercelBot commented May 13, 2026

Copy link
Copy Markdown
Contributor

@changeset-bot

changeset-botBot commented May 13, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 4f77bc1

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

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

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

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production145302301683
✅ 💻 Local Development161702191836
✅ 📦 Local Production161702191836
✅ 🐘 Local Postgres161702191836
✅ 🪟 Windows15300153
✅ 📋 Other89401771071
✅ vercel-multi-region270027
Total7378010648442

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro126027
✅ example126027
✅ express126027
✅ fastify126027
✅ hono126027
✅ nextjs-turbopack15003
✅ nextjs-webpack15003
✅ nitro126027
✅ nuxt126027
✅ sveltekit14508
✅ vite126027
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack15300
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable128025
✅ e2e-local-dev-tanstack-start-128025
✅ e2e-local-postgres-nest-stable128025
✅ e2e-local-postgres-tanstack-start-128025
✅ e2e-local-prod-nest-stable128025
✅ e2e-local-prod-tanstack-start-128025
✅ e2e-vercel-prod-tanstack-start126027
✅ vercel-multi-region
AppPassedFailedSkipped
✅ nextjs-turbopack2700

📋 View full workflow run

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR extends the workflow runtime/world contract to support (1) world-minted run IDs and (2) region-aware queue routing, and wires those capabilities through @workflow/core and the Vercel world implementation.

Changes:

  • Add optional World.createRunId(input?) and QueueOptions.region in @workflow/world.
  • Update @workflow/corestart() to delegate run ID generation to world.createRunId(opts.runIdInput) when available, and to forward runIdInput.region to queue options.
  • Implement region-tagged run IDs + region-resolving queue routing in @workflow/world-vercel, including new tests and a new ulid dependency.

Reviewed changes

Copilot reviewed 13 out of 14 changed files in this pull request and generated 4 comments.

Show a summary per file
FileDescription
pnpm-lock.yamlAdds lockfile entry for ulid used by world-vercel.
packages/world/src/queue.tsExtends QueueOptions with a region routing hint + docs.
packages/world/src/interfaces.tsExtends World interface with optional createRunId.
packages/world-vercel/src/queue.tsAdds region resolution order (opts → tagged runId → env → fallback) and updates CBOR encode/decode imports.
packages/world-vercel/src/queue.test.tsAdds region-routing test coverage.
packages/world-vercel/src/index.tsWires createRunId into createVercelWorld and re-exports it.
packages/world-vercel/src/create-run-id.tsImplements region-tagged ULID minting with per-process monotonicity.
packages/world-vercel/src/create-run-id.test.tsTests for env/input fallback and monotonicity behavior.
packages/world-vercel/package.jsonAdds ulid dependency.
packages/core/src/runtime/start.tsUses world.createRunId when available; forwards runIdInput.region into queue options.
packages/core/src/runtime/start.test.tsAdds tests validating createRunId usage and region threading.
.changeset/world-vercel-create-run-id.mdRelease notes for new run ID minting + routing behavior in world-vercel.
.changeset/world-create-run-id.mdRelease notes for new @workflow/world interface fields.
.changeset/core-run-id-input.mdRelease notes for core start() runIdInput behavior.
Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

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

Comment threadpackages/world/src/queue.ts Outdated
Comment threadpackages/world-vercel/src/queue.ts
Comment threadpackages/world-vercel/src/queue.test.ts Outdated
Comment threadpackages/world-vercel/src/create-run-id.test.ts Outdated
…unId
Drop the dedicated 'runIdInput' field on StartOptions and forward the
entire options bag to world.createRunId() instead. This keeps the
public API surface smaller and lets each World pick the fields it
recognises (e.g. world-vercel reads 'region'). The top-level 'region'
option remains on StartOptionsBase and is also threaded onto the
queue's per-call region opt when set.
…ride
- queue: validate opts.region and VERCEL_REGION against the known region
table before routing, ignoring unrecognised codes so a bad override
can't clobber the payload-derived region (Copilot)
- add isKnownRegionCode() runtime guard to run-id/regions
- reset WORKFLOW_SERVER_URL_OVERRIDE to '' (must be empty on main)
- fold the within-PR iad1-default changeset into the main world-vercel
changeset and delete it (review)
- start.test: declare specVersion on createRunId mock worlds now that
the merged world-compatibility check requires it
- cover the new region-validation fall-through paths in queue.test
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@VaguelySerious

Copy link
Copy Markdown
Member

(AI) Deleted .changeset/run-id-default-iad1.md and folded its net user-facing behavior (the iad1 default when no region/env is available) into the main world-vercel-create-run-id changeset so the changelog stays accurate without the within-PR intermediate note.

…i-region preview
BRANCH-ONLY — revert the override to '' before merge (lint enforces).
Points this PR's e2e/benchmark runs at the wave-1 multi-region
workflow-server preview (vercel/workflow-server#590: iad1+sfo1+fra1
serving, staging data backends) so region-tagged runs are validated
against real multi-region serving end-to-end.
Also makes the unit-test mock origins in events-v4.test.ts and
trace-propagation.test.ts override-aware (same pattern the rest of
the file and utils.test.ts already use), so the suite passes whether
or not the override is set — these two files were the only spots
hardcoding https://vercel-workflow.com.
Conflicts: packages/world-vercel/src/index.ts — both sides added
imports/exports (createRunId from this branch, createAnalytics from
main #2698); kept both. Includes the TypeScript 6 workspace upgrade
(#2700) from main. world-vercel (249) and core (1356) tests pass.
Adds a dedicated e2e suite validating @workflow/world-vercel region
routing end to end, run as its own CI job (e2e-vercel-multi-region)
against the nextjs-turbopack workbench only — deliberately separate
from e2e.test.ts, which runs as a matrix across all worlds/frameworks
where Vercel-specific multi-region behavior doesn't apply.
- workbench/nextjs-turbopack/vercel.json: deploy to iad1+sfo1+fra1 so
region-routed flow messages have a function to land on in each region.
- workflows/99_e2e.ts: regionProbeWorkflow returns the VERCEL_REGION
observed by both the workflow and a step, so tests can assert the run
EXECUTED in the intended region (not just that it was tagged).
- packages/core/e2e/e2e-region.test.ts: per-region cases assert
1) start(..., { region }) mints a region-tagged run ID (decoded via
@workflow/world-vercel/run-id),
2) the workflow + step both observed VERCEL_REGION === region,
3) the server reports the run completed;
plus a concurrent all-regions case guarding against cross-region
misrouting under simultaneous multi-region traffic. Skips on local
deployments.
- .github/workflows/tests.yml: new e2e-vercel-multi-region job
mirroring e2e-vercel-prod's env/deployment-wait, running only the
new suite.
The first multi-region CI run surfaced two issues:
1. sfo1/fra1-tagged runs executed in iad1. The suite started runs from
the external test process, which uses the api.vercel.com token proxy
— and the proxy's queues path forwards every send to the region-less
VQS host (the world's proxy-mode resolveBaseUrl ignores the region
argument, and the proxy's x-vercel-vqs-api-url escape hatch only
allowlists vqs-server-*.vercel.sh preview hosts). Production traffic
publishes IN-FUNCTION (direct regional queue routing), so the suite
now triggers start() through a new workbench route
(/api/e2e-region-start) and rehydrates the run with getRun() —
testing the path production actually takes. Proxy-mode regional
queue routing is a known gap to address separately in api-workflow.
2. TypeError on world.runs.get: getWorld() is async and was called
without await.
Textually clean but semantically conflicting with #2789: main added the
proxy-mode x-vercel-queue-region header in createQueue's shared
clientOptions, referencing the static region const this branch removed
in favor of per-send resolution. Resolution: the header now rides the
per-send QueueClient construction, carrying resolveTargetRegion's
result — proxied sends route to the region resolved per message
(opts.region -> run-ID tag -> VERCEL_REGION -> iad1).
world-vercel: 253/253 (includes #2789's proxy-header tests, which hold
under per-send resolution).
…ion suite
With regional VQS routing now working through the api.vercel.com proxy
(vercel/api#79056 + #2789 + this branch's per-send region resolution),
the suite covers both start configurations, asserting the same three
properties for each (region-tagged run ID, execution in the intended
region via VERCEL_REGION echoed in the return value, server-side
completion):
1. EXPLICIT: start(..., { region }) called directly in the vitest
runner — publishes through the token proxy, per-send region carried
by x-vercel-queue-region. Restores the direct-start shape the suite
had originally, plus the concurrent all-regions case.
2. IMPLICIT: dedicated per-region workbench routes
(/api/e2e-region-implicit/{iad1,sfo1,fra1}), each pinned to a single
region via a per-function 'regions' entry in the workbench
vercel.json, calling start() with NO region option — createRunId
derives the tag from the minting function's VERCEL_REGION. The test
also asserts the route reported executing in its pinned region, so
the implicit-tagging assertion can't pass vacuously.
Replaces the interim /api/e2e-region-start route (explicit region via
request body), which existed to work around the pre-#79056 proxy gap.
…ng is in production
workflow-server#590 (iad1+sfo1+fra1 serving) merged and deployed to
production and the e2e backend, so this branch's e2e/benchmark runs no
longer need to target the wave-1 preview. Restores the empty override
the No Test Overrides lint job enforces for merge.
The override-aware unit-test origins (events-v4/trace-propagation)
stay — they are correct under any override value.
@saasjesus

Copy link
Copy Markdown

damn its happening

Regression coverage for a backend bug that made cross-region stream
reads report zero chunks on IN-PROGRESS streams (completed streams were
unaffected), which forced the multi-region serving rollback.
The new case exercises exactly that geometry:
- crossRegionStreamWorkflow (99_e2e.ts) writes N chunks to the default
output stream, then holds the stream OPEN for 45s before closing —
the in-progress window is the point, since completed streams are the
easy case.
- The e2e starts it with region iad1, waits (same-region, via the
api.vercel.com proxy) until all chunks are written, asserts the run
is still 'running', then reads through a new sfo1-pinned workbench
route (/api/e2e-stream-read/sfo1) that returns getTailIndex() plus
its VERCEL_REGION. The reader's region served none of the stream's
writes, so the reported chunk count must come from the backend's
cross-region stream metadata. The test fails loudly if the route
isn't actually executing in sfo1.
Also bumps the explicit-region test timeout to 120s: the first case in
the file absorbs every cold start at once (fresh workbench instances
in up to three regions plus a cold backend preview) and was observed
just over the 60s default.
BRANCH-ONLY (revert before merge, lint enforces):
WORKFLOW_SERVER_URL_OVERRIDE points at a multi-region backend preview
that includes the fix, so this validates cross-region stream
visibility end-to-end before multi-region serving is re-enabled.
Points the suite at an all-regions backend preview and widens coverage
from the wave-1 trio to every provisioned region:
- Explicit path: a single concurrent all-regions case starts one
tagged run per region (one shared cold-start window instead of 19
sequential ones) and aggregates per-region failures so a single
region's breakage reports alongside the full picture. The trio keeps
its detailed per-region cases and the 9-way concurrent-isolation
case.
- Implicit path: workbench gains a region-pinned
/api/e2e-region-implicit/<region> route per provisioned region (19
total, shared handler), the workbench itself now deploys to all of
them, and the test.each covers the full set with per-case timeouts
for regional cold starts.
- Multi-region CI job timeout 20m -> 35m for the sequential implicit
cases.
BRANCH-ONLY (revert before merge, lint enforces):
WORKFLOW_SERVER_URL_OVERRIDE now targets the all-regions backend
preview instead of the previous (stale, since-merged) fix preview.
The first all-regions run surfaced a subtle execution-locality
behavior: queue delivery is guaranteed to the tagged region's
dataplane and the delivery callback egresses from that region, but the
consumer invocation's execution region is chosen by where that
callback enters Vercel's edge — and adjacent regions can geo-resolve
to each other's functions. Observed live: kix1-tagged runs (callback
egressing from Osaka) deterministically executing in hnd1/Tokyo on
both the explicit and implicit paths, with tagging, data placement,
and completion all still strictly kix1.
expectRunInRegion now asserts execution lands in the tagged region OR
one of its geographic neighbors (EXECUTION_ADJACENCY), while run-ID
tagging and server-side completion remain strictly the requested
region. Gross misrouting (e.g. kix1 -> iad1) still fails.
… production
The all-regions workflow-server rollout is deployed and serving
production traffic from every Vercel region, so this branch's e2e no
longer needs to target a branch preview. Restores the empty override
the No Test Overrides lint enforces for merge.
With this the PR is complete: region-tagged run IDs, region-aware
queue routing, and the multi-region e2e suite (explicit + implicit +
all-regions + cross-region streams) all validate against the
production-default backends.

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 47 out of 48 changed files in this pull request and generated 3 comments.

Files not reviewed (1)
  • pnpm-lock.yaml: Generated file

Comment threadpackages/core/src/runtime/start.ts Outdated
Comment threadpackages/world-vercel/src/queue.ts
Comment threadpackages/world-vercel/src/events.ts Outdated
- start.ts: StartOptionsBase.region fallback is iad1, not the unknown
sentinel (createRunId always mints a concrete routable region)
- queue.ts: example used a nonexistent start({ runIdInput }) API; the
real option is start({ region })
- events.ts: decode() clears only the tag bit (top bit of the 48-bit
timestamp field) — it does not restore the original untagged ULID;
reword to say what actually matters for timestamp validation
Hooks are resolved by opaque token, which carries no region hint, so
lookup and resume must work regardless of which region owns the run's
data. Exercises the full follow-up-message path on sfo1- and
fra1-tagged runs: create inside the workflow, resolve by token from
the test process, resume twice sequentially, and assert payload order
and completion.
Regression coverage for the failure mode where the first message to a
hook-driven app on a non-iad1 run worked but every follow-up failed
with 'Hook not found'.
Conflict in packages/core/src/runtime/start.test.ts: both sides
appended a new top-level describe block at the same location — this
branch's 'createRunId' tests and main's 'queue namespace' tests.
Resolved by keeping both blocks verbatim (each is self-contained with
its own beforeEach/mocks). Lockfile reconciled with pnpm install.
Verified post-merge: core tsc clean, start/helpers tests 73 passed,
world-vercel 263 passed, full build 27/27.
@github-actionsgithub-actionsBot mentioned this pull request Jul 13, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Backport to stable failed for 9da2d76 due to a workflow error (backport job run).

This is usually an infrastructure problem (e.g. the configured AI model could not be found, an AI Gateway error, or an opencode crash) rather than a merge conflict. Check the job logs linked above for details.

Once the underlying issue is fixed, re-run the Backport to stable workflow manually via workflow_dispatch and paste this commit SHA into the ref input:

9da2d762604c2b73eb39f07fc0b069aea643e18d

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.

Support multi-region on Vercel backend

4 participants

@TooTallNate@VaguelySerious@saasjesus