feat(world): add optional getMany() for batch run reads - #2915

Merged
VaguelySerious merged 2 commits into
vercel:mainfrom
joeyhotz:joeyhotz/get-many-workflow-runs
Jul 16, 2026
Merged

feat(world): add optional getMany() for batch run reads#2915
VaguelySerious merged 2 commits into
vercel:mainfrom
joeyhotz:joeyhotz/get-many-workflow-runs

Conversation

@joeyhotz

@joeyhotzjoeyhotz commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Description

Add optional world.runs.getMany() for one-operation snapshots of multiple workflow runs. It lets GraphQL list resolvers and bulk orchestration fetch many run snapshots in a single storage call, avoiding N+1 reads.

Results preserve input order, fetch duplicate IDs once, and use null for missing runs. getMany remains optional so existing community Worlds are source-compatible.

local/vercel: delegate to the per-ID runs.get handler via Promise.all, sharing the same read, schema validation, resolveData, and error handling.

postgres: uses a single IN (...) query for an indexed batch read.

How did you test your changes?

  • pnpm --filter @workflow/world-local test — 465 tests pass, including order, duplicate-ID, missing-ID, and metadata-only getMany coverage.
  • pnpm --filter @workflow/world-postgres exec vitest run test/storage.test.ts --testNamePattern getMany — Postgres integration coverage passes for result mapping and a single-query assertion with duplicate IDs.
  • pnpm --filter @workflow/world-vercel test — 295 tests pass, including MockAgent coverage of individual GET delegation with duplicate-ID deduplication and missing-run null handling.

PR Checklist - Required to merge

  • 📦 pnpm changeset was run with minor changesets for @workflow/world, @workflow/world-local, @workflow/world-postgres, and @workflow/world-vercel.
  • 🔒 DCO sign-off passes.
  • 📝 Ping @vercel/workflow in a comment once the PR is ready, and the above checklist is complete.

@joeyhotz
joeyhotz requested review from a team and ijjk as code ownersJuly 14, 2026 15:34
@changeset-bot

changeset-botBot commented Jul 14, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 5cec417

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

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

@vercel

vercelBot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

@joeyhotz is attempting to deploy a commit to the Vercel Labs Team on Vercel.

A member of the Team first needs to authorize it.

@joeyhotz

Copy link
Copy Markdown
ContributorAuthor

@vercel/workflow Ready for review. The PR follows the template; its remaining dependency is the paired workflow-server batch endpoint described above.

@VaguelySerious

VaguelySerious commented Jul 14, 2026

Copy link
Copy Markdown
Member

@joeyhotz Can you speak more to the motivation here? Is this useful for more than simply reducing request count over e.g. Promise.all([world.runs.get, ...])? If not, in what cases would the request count be an issue?

@joeyhotz

Copy link
Copy Markdown
ContributorAuthor

@joeyhotz Can you speak more to the motivation here? Is this useful for more than simply reducing request count over e.g. Promise.all([world.runs.get, ...])? If not, in what cases would the request count be an issue?

Hi @VaguelySerious. We're migrating from inngest to workflow sdk with the Postgres world. During the migration we decided to lean hard on the workflow sdk for storing and managing state. We have a graphql API. The reason why we need a batch fetch is we want to reduce the number of trips to the DB.

Think if you have a table of 25 entities that have a running job/previous job run behind it. For each entity you want to read the job timestamp or the job status (if it's running, or failed). We want to put this job status fetch in a data loader so the db read for the whole table is done in one trip, not 25.

@VaguelySerious

Copy link
Copy Markdown
Member

@joeyhotz I can see how this would reduce trips to the DB, and I'm mainly trying to assess what the actual performance impact is before adding a new method to the world interface for something that might have negligible impact for most users. Have you tried a Promise.all(...world.runs.get) and measured that against a tarball of this branch?

@joeyhotz

joeyhotz commented Jul 15, 2026

Copy link
Copy Markdown
ContributorAuthor

@joeyhotz I can see how this would reduce trips to the DB, and I'm mainly trying to assess what the actual performance impact is before adding a new method to the world interface for something that might have negligible impact for most users. Have you tried a Promise.all(...world.runs.get) and measured that against a tarball of this branch?

@VaguelySerious Ran a comparison locally with a local psql db here:

// Concurrent, but still N database queriesawaitPromise.all(runIds.map((id)=>world.runs.get(id,{resolveData: 'none'})));
// One database queryawaitworld.runs.getMany!(runIds,{resolveData: 'none'});
Run IDsPromise.all(runs.get)getMany
2525 queries, 1.69 ms1 query, 0.98 ms
100100 queries, 5.15 ms1 query, 1.85 ms

Promise.all still checks out one connection/query per run ID. With a 10-connection pool, 100 run reads compete with normal API traffic and execute in roughly ten pool waves. getMany uses one connection for one indexed IN (...) query.

I think the performance benefit will be higher in a remote environment too.

@joeyhotz

Copy link
Copy Markdown
ContributorAuthor

@VaguelySerious I'm new to the sdk so forgive me if I come across as naive, maybe another option is adding an ID filter to the list interface?

@joeyhotz
joeyhotzforce-pushed the joeyhotz/get-many-workflow-runs branch from 68837c0 to 96e8fddCompareJuly 15, 2026 02:21

@VaguelySeriousVaguelySerious left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Alright, we're good with shipping this, could you make one more update? world-local and world-vercel implementations duplicate some code from the runs.get handler internally. Since they essentially just Promise.all the same code, would you be able to re-use runs.get internally for those Worlds? Let me know if that doesn't make sense

@joeyhotz
joeyhotzforce-pushed the joeyhotz/get-many-workflow-runs branch from 96e8fdd to 42e446bCompareJuly 15, 2026 23:51
@joeyhotzjoeyhotz changed the title feat(world): add batch run readsfeat(world): add optional getMany() for batch run readsJul 16, 2026
@joeyhotz

Copy link
Copy Markdown
ContributorAuthor

Alright, we're good with shipping this, could you make one more update? world-local and world-vercel implementations duplicate some code from the runs.get handler internally. Since they essentially just Promise.all the same code, would you be able to re-use runs.get internally for those Worlds? Let me know if that doesn't make sense

@VaguelySerious makes sense! Have pushed the changes 👍

@joeyhotz
joeyhotzforce-pushed the joeyhotz/get-many-workflow-runs branch from 0494f11 to 81f3eebCompareJuly 16, 2026 00:22
@joeyhotz

Copy link
Copy Markdown
ContributorAuthor

pinging @vercel/workflow

@VaguelySerious

Copy link
Copy Markdown
Member

Sorry, merging is blocked on "Commits must have verified signatures". Would you be able to sign your commits and force-push?

Signed-off-by: Joey Hotz <joeyhotz1@gmail.com>
Delegate to the per-ID handler in world-local and world-vercel
getMany implementations instead of duplicating read, schema
validation, resolveData, and error-deserialization logic.
world-postgres keeps its single-query IN (...) path.
Signed-off-by: Joey Hotz <joeyhotz1@gmail.com>
@joeyhotz
joeyhotzforce-pushed the joeyhotz/get-many-workflow-runs branch from 81f3eeb to 5cec417CompareJuly 16, 2026 04:34
@joeyhotz

Copy link
Copy Markdown
ContributorAuthor

@VaguelySerious sorry, should be good now! thank you. not sure if i need to keep pinging @vercel/workflow but i will anyway

@VaguelySerious
VaguelySerious merged commit 7d29bab into vercel:mainJul 16, 2026
66 of 103 checks passed
@github-actionsgithub-actionsBot mentioned this pull request Jul 16, 2026
github-actionsBot added a commit that referenced this pull request Jul 16, 2026
Signed-off-by: Joey Hotz <joeyhotz1@gmail.com>
@github-actions

Copy link
Copy Markdown
Contributor

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

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@joeyhotz@VaguelySerious
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n 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;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

feat(world): add optional getMany() for batch run reads - #2915

Merged
VaguelySerious merged 2 commits into
vercel:mainfrom
joeyhotz:joeyhotz/get-many-workflow-runs
Jul 16, 2026
Merged

feat(world): add optional getMany() for batch run reads#2915
VaguelySerious merged 2 commits into
vercel:mainfrom
joeyhotz:joeyhotz/get-many-workflow-runs

Conversation

@joeyhotz

@joeyhotzjoeyhotz commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Description

Add optional world.runs.getMany() for one-operation snapshots of multiple workflow runs. It lets GraphQL list resolvers and bulk orchestration fetch many run snapshots in a single storage call, avoiding N+1 reads.

Results preserve input order, fetch duplicate IDs once, and use null for missing runs. getMany remains optional so existing community Worlds are source-compatible.

local/vercel: delegate to the per-ID runs.get handler via Promise.all, sharing the same read, schema validation, resolveData, and error handling.

postgres: uses a single IN (...) query for an indexed batch read.

How did you test your changes?

  • pnpm --filter @workflow/world-local test — 465 tests pass, including order, duplicate-ID, missing-ID, and metadata-only getMany coverage.
  • pnpm --filter @workflow/world-postgres exec vitest run test/storage.test.ts --testNamePattern getMany — Postgres integration coverage passes for result mapping and a single-query assertion with duplicate IDs.
  • pnpm --filter @workflow/world-vercel test — 295 tests pass, including MockAgent coverage of individual GET delegation with duplicate-ID deduplication and missing-run null handling.

PR Checklist - Required to merge

  • 📦 pnpm changeset was run with minor changesets for @workflow/world, @workflow/world-local, @workflow/world-postgres, and @workflow/world-vercel.
  • 🔒 DCO sign-off passes.
  • 📝 Ping @vercel/workflow in a comment once the PR is ready, and the above checklist is complete.

@joeyhotz
joeyhotz requested review from a team and ijjk as code ownersJuly 14, 2026 15:34
@changeset-bot

changeset-botBot commented Jul 14, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 5cec417

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

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

@vercel

vercelBot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

@joeyhotz is attempting to deploy a commit to the Vercel Labs Team on Vercel.

A member of the Team first needs to authorize it.

@joeyhotz

Copy link
Copy Markdown
ContributorAuthor

@vercel/workflow Ready for review. The PR follows the template; its remaining dependency is the paired workflow-server batch endpoint described above.

@VaguelySerious

VaguelySerious commented Jul 14, 2026

Copy link
Copy Markdown
Member

@joeyhotz Can you speak more to the motivation here? Is this useful for more than simply reducing request count over e.g. Promise.all([world.runs.get, ...])? If not, in what cases would the request count be an issue?

@joeyhotz

Copy link
Copy Markdown
ContributorAuthor

@joeyhotz Can you speak more to the motivation here? Is this useful for more than simply reducing request count over e.g. Promise.all([world.runs.get, ...])? If not, in what cases would the request count be an issue?

Hi @VaguelySerious. We're migrating from inngest to workflow sdk with the Postgres world. During the migration we decided to lean hard on the workflow sdk for storing and managing state. We have a graphql API. The reason why we need a batch fetch is we want to reduce the number of trips to the DB.

Think if you have a table of 25 entities that have a running job/previous job run behind it. For each entity you want to read the job timestamp or the job status (if it's running, or failed). We want to put this job status fetch in a data loader so the db read for the whole table is done in one trip, not 25.

@VaguelySerious

Copy link
Copy Markdown
Member

@joeyhotz I can see how this would reduce trips to the DB, and I'm mainly trying to assess what the actual performance impact is before adding a new method to the world interface for something that might have negligible impact for most users. Have you tried a Promise.all(...world.runs.get) and measured that against a tarball of this branch?

@joeyhotz

joeyhotz commented Jul 15, 2026

Copy link
Copy Markdown
ContributorAuthor

@joeyhotz I can see how this would reduce trips to the DB, and I'm mainly trying to assess what the actual performance impact is before adding a new method to the world interface for something that might have negligible impact for most users. Have you tried a Promise.all(...world.runs.get) and measured that against a tarball of this branch?

@VaguelySerious Ran a comparison locally with a local psql db here:

// Concurrent, but still N database queriesawaitPromise.all(runIds.map((id)=>world.runs.get(id,{resolveData: 'none'})));
// One database queryawaitworld.runs.getMany!(runIds,{resolveData: 'none'});
Run IDsPromise.all(runs.get)getMany
2525 queries, 1.69 ms1 query, 0.98 ms
100100 queries, 5.15 ms1 query, 1.85 ms

Promise.all still checks out one connection/query per run ID. With a 10-connection pool, 100 run reads compete with normal API traffic and execute in roughly ten pool waves. getMany uses one connection for one indexed IN (...) query.

I think the performance benefit will be higher in a remote environment too.

@joeyhotz

Copy link
Copy Markdown
ContributorAuthor

@VaguelySerious I'm new to the sdk so forgive me if I come across as naive, maybe another option is adding an ID filter to the list interface?

@joeyhotz
joeyhotzforce-pushed the joeyhotz/get-many-workflow-runs branch from 68837c0 to 96e8fddCompareJuly 15, 2026 02:21

@VaguelySeriousVaguelySerious left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Alright, we're good with shipping this, could you make one more update? world-local and world-vercel implementations duplicate some code from the runs.get handler internally. Since they essentially just Promise.all the same code, would you be able to re-use runs.get internally for those Worlds? Let me know if that doesn't make sense

@joeyhotz
joeyhotzforce-pushed the joeyhotz/get-many-workflow-runs branch from 96e8fdd to 42e446bCompareJuly 15, 2026 23:51
@joeyhotzjoeyhotz changed the title feat(world): add batch run readsfeat(world): add optional getMany() for batch run readsJul 16, 2026
@joeyhotz

Copy link
Copy Markdown
ContributorAuthor

Alright, we're good with shipping this, could you make one more update? world-local and world-vercel implementations duplicate some code from the runs.get handler internally. Since they essentially just Promise.all the same code, would you be able to re-use runs.get internally for those Worlds? Let me know if that doesn't make sense

@VaguelySerious makes sense! Have pushed the changes 👍

@joeyhotz
joeyhotzforce-pushed the joeyhotz/get-many-workflow-runs branch from 0494f11 to 81f3eebCompareJuly 16, 2026 00:22
@joeyhotz

Copy link
Copy Markdown
ContributorAuthor

pinging @vercel/workflow

@VaguelySerious

Copy link
Copy Markdown
Member

Sorry, merging is blocked on "Commits must have verified signatures". Would you be able to sign your commits and force-push?

Signed-off-by: Joey Hotz <joeyhotz1@gmail.com>
Delegate to the per-ID handler in world-local and world-vercel
getMany implementations instead of duplicating read, schema
validation, resolveData, and error-deserialization logic.
world-postgres keeps its single-query IN (...) path.
Signed-off-by: Joey Hotz <joeyhotz1@gmail.com>
@joeyhotz
joeyhotzforce-pushed the joeyhotz/get-many-workflow-runs branch from 81f3eeb to 5cec417CompareJuly 16, 2026 04:34
@joeyhotz

Copy link
Copy Markdown
ContributorAuthor

@VaguelySerious sorry, should be good now! thank you. not sure if i need to keep pinging @vercel/workflow but i will anyway

@VaguelySerious
VaguelySerious merged commit 7d29bab into vercel:mainJul 16, 2026
66 of 103 checks passed
@github-actionsgithub-actionsBot mentioned this pull request Jul 16, 2026
github-actionsBot added a commit that referenced this pull request Jul 16, 2026
Signed-off-by: Joey Hotz <joeyhotz1@gmail.com>
@github-actions

Copy link
Copy Markdown
Contributor

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

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@joeyhotz@VaguelySerious
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(world): add optional getMany() for batch run reads - #2915

Merged
VaguelySerious merged 2 commits into
vercel:mainfrom
joeyhotz:joeyhotz/get-many-workflow-runs
Jul 16, 2026
Merged

feat(world): add optional getMany() for batch run reads#2915
VaguelySerious merged 2 commits into
vercel:mainfrom
joeyhotz:joeyhotz/get-many-workflow-runs

Conversation

@joeyhotz

@joeyhotzjoeyhotz commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Description

Add optional world.runs.getMany() for one-operation snapshots of multiple workflow runs. It lets GraphQL list resolvers and bulk orchestration fetch many run snapshots in a single storage call, avoiding N+1 reads.

Results preserve input order, fetch duplicate IDs once, and use null for missing runs. getMany remains optional so existing community Worlds are source-compatible.

local/vercel: delegate to the per-ID runs.get handler via Promise.all, sharing the same read, schema validation, resolveData, and error handling.

postgres: uses a single IN (...) query for an indexed batch read.

How did you test your changes?

  • pnpm --filter @workflow/world-local test — 465 tests pass, including order, duplicate-ID, missing-ID, and metadata-only getMany coverage.
  • pnpm --filter @workflow/world-postgres exec vitest run test/storage.test.ts --testNamePattern getMany — Postgres integration coverage passes for result mapping and a single-query assertion with duplicate IDs.
  • pnpm --filter @workflow/world-vercel test — 295 tests pass, including MockAgent coverage of individual GET delegation with duplicate-ID deduplication and missing-run null handling.

PR Checklist - Required to merge

  • 📦 pnpm changeset was run with minor changesets for @workflow/world, @workflow/world-local, @workflow/world-postgres, and @workflow/world-vercel.
  • 🔒 DCO sign-off passes.
  • 📝 Ping @vercel/workflow in a comment once the PR is ready, and the above checklist is complete.

@joeyhotz
joeyhotz requested review from a team and ijjk as code ownersJuly 14, 2026 15:34
@changeset-bot

changeset-botBot commented Jul 14, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 5cec417

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

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

@vercel

vercelBot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

@joeyhotz is attempting to deploy a commit to the Vercel Labs Team on Vercel.

A member of the Team first needs to authorize it.

@joeyhotz

Copy link
Copy Markdown
ContributorAuthor

@vercel/workflow Ready for review. The PR follows the template; its remaining dependency is the paired workflow-server batch endpoint described above.

@VaguelySerious

VaguelySerious commented Jul 14, 2026

Copy link
Copy Markdown
Member

@joeyhotz Can you speak more to the motivation here? Is this useful for more than simply reducing request count over e.g. Promise.all([world.runs.get, ...])? If not, in what cases would the request count be an issue?

@joeyhotz

Copy link
Copy Markdown
ContributorAuthor

@joeyhotz Can you speak more to the motivation here? Is this useful for more than simply reducing request count over e.g. Promise.all([world.runs.get, ...])? If not, in what cases would the request count be an issue?

Hi @VaguelySerious. We're migrating from inngest to workflow sdk with the Postgres world. During the migration we decided to lean hard on the workflow sdk for storing and managing state. We have a graphql API. The reason why we need a batch fetch is we want to reduce the number of trips to the DB.

Think if you have a table of 25 entities that have a running job/previous job run behind it. For each entity you want to read the job timestamp or the job status (if it's running, or failed). We want to put this job status fetch in a data loader so the db read for the whole table is done in one trip, not 25.

@VaguelySerious

Copy link
Copy Markdown
Member

@joeyhotz I can see how this would reduce trips to the DB, and I'm mainly trying to assess what the actual performance impact is before adding a new method to the world interface for something that might have negligible impact for most users. Have you tried a Promise.all(...world.runs.get) and measured that against a tarball of this branch?

@joeyhotz

joeyhotz commented Jul 15, 2026

Copy link
Copy Markdown
ContributorAuthor

@joeyhotz I can see how this would reduce trips to the DB, and I'm mainly trying to assess what the actual performance impact is before adding a new method to the world interface for something that might have negligible impact for most users. Have you tried a Promise.all(...world.runs.get) and measured that against a tarball of this branch?

@VaguelySerious Ran a comparison locally with a local psql db here:

// Concurrent, but still N database queriesawaitPromise.all(runIds.map((id)=>world.runs.get(id,{resolveData: 'none'})));
// One database queryawaitworld.runs.getMany!(runIds,{resolveData: 'none'});
Run IDsPromise.all(runs.get)getMany
2525 queries, 1.69 ms1 query, 0.98 ms
100100 queries, 5.15 ms1 query, 1.85 ms

Promise.all still checks out one connection/query per run ID. With a 10-connection pool, 100 run reads compete with normal API traffic and execute in roughly ten pool waves. getMany uses one connection for one indexed IN (...) query.

I think the performance benefit will be higher in a remote environment too.

@joeyhotz

Copy link
Copy Markdown
ContributorAuthor

@VaguelySerious I'm new to the sdk so forgive me if I come across as naive, maybe another option is adding an ID filter to the list interface?

@joeyhotz
joeyhotzforce-pushed the joeyhotz/get-many-workflow-runs branch from 68837c0 to 96e8fddCompareJuly 15, 2026 02:21

@VaguelySeriousVaguelySerious left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Alright, we're good with shipping this, could you make one more update? world-local and world-vercel implementations duplicate some code from the runs.get handler internally. Since they essentially just Promise.all the same code, would you be able to re-use runs.get internally for those Worlds? Let me know if that doesn't make sense

@joeyhotz
joeyhotzforce-pushed the joeyhotz/get-many-workflow-runs branch from 96e8fdd to 42e446bCompareJuly 15, 2026 23:51
@joeyhotzjoeyhotz changed the title feat(world): add batch run readsfeat(world): add optional getMany() for batch run readsJul 16, 2026
@joeyhotz

Copy link
Copy Markdown
ContributorAuthor

Alright, we're good with shipping this, could you make one more update? world-local and world-vercel implementations duplicate some code from the runs.get handler internally. Since they essentially just Promise.all the same code, would you be able to re-use runs.get internally for those Worlds? Let me know if that doesn't make sense

@VaguelySerious makes sense! Have pushed the changes 👍

@joeyhotz
joeyhotzforce-pushed the joeyhotz/get-many-workflow-runs branch from 0494f11 to 81f3eebCompareJuly 16, 2026 00:22
@joeyhotz

Copy link
Copy Markdown
ContributorAuthor

pinging @vercel/workflow

@VaguelySerious

Copy link
Copy Markdown
Member

Sorry, merging is blocked on "Commits must have verified signatures". Would you be able to sign your commits and force-push?

Signed-off-by: Joey Hotz <joeyhotz1@gmail.com>
Delegate to the per-ID handler in world-local and world-vercel
getMany implementations instead of duplicating read, schema
validation, resolveData, and error-deserialization logic.
world-postgres keeps its single-query IN (...) path.
Signed-off-by: Joey Hotz <joeyhotz1@gmail.com>
@joeyhotz
joeyhotzforce-pushed the joeyhotz/get-many-workflow-runs branch from 81f3eeb to 5cec417CompareJuly 16, 2026 04:34
@joeyhotz

Copy link
Copy Markdown
ContributorAuthor

@VaguelySerious sorry, should be good now! thank you. not sure if i need to keep pinging @vercel/workflow but i will anyway

@VaguelySerious
VaguelySerious merged commit 7d29bab into vercel:mainJul 16, 2026
66 of 103 checks passed
@github-actionsgithub-actionsBot mentioned this pull request Jul 16, 2026
github-actionsBot added a commit that referenced this pull request Jul 16, 2026
Signed-off-by: Joey Hotz <joeyhotz1@gmail.com>
@github-actions

Copy link
Copy Markdown
Contributor

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

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@joeyhotz@VaguelySerious
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(world): add optional getMany() for batch run reads - #2915

Merged
VaguelySerious merged 2 commits into
vercel:mainfrom
joeyhotz:joeyhotz/get-many-workflow-runs
Jul 16, 2026
Merged

feat(world): add optional getMany() for batch run reads#2915
VaguelySerious merged 2 commits into
vercel:mainfrom
joeyhotz:joeyhotz/get-many-workflow-runs

Conversation

@joeyhotz

@joeyhotzjoeyhotz commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Description

Add optional world.runs.getMany() for one-operation snapshots of multiple workflow runs. It lets GraphQL list resolvers and bulk orchestration fetch many run snapshots in a single storage call, avoiding N+1 reads.

Results preserve input order, fetch duplicate IDs once, and use null for missing runs. getMany remains optional so existing community Worlds are source-compatible.

local/vercel: delegate to the per-ID runs.get handler via Promise.all, sharing the same read, schema validation, resolveData, and error handling.

postgres: uses a single IN (...) query for an indexed batch read.

How did you test your changes?

  • pnpm --filter @workflow/world-local test — 465 tests pass, including order, duplicate-ID, missing-ID, and metadata-only getMany coverage.
  • pnpm --filter @workflow/world-postgres exec vitest run test/storage.test.ts --testNamePattern getMany — Postgres integration coverage passes for result mapping and a single-query assertion with duplicate IDs.
  • pnpm --filter @workflow/world-vercel test — 295 tests pass, including MockAgent coverage of individual GET delegation with duplicate-ID deduplication and missing-run null handling.

PR Checklist - Required to merge

  • 📦 pnpm changeset was run with minor changesets for @workflow/world, @workflow/world-local, @workflow/world-postgres, and @workflow/world-vercel.
  • 🔒 DCO sign-off passes.
  • 📝 Ping @vercel/workflow in a comment once the PR is ready, and the above checklist is complete.

@joeyhotz
joeyhotz requested review from a team and ijjk as code ownersJuly 14, 2026 15:34
@changeset-bot

changeset-botBot commented Jul 14, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 5cec417

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

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

@vercel

vercelBot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

@joeyhotz is attempting to deploy a commit to the Vercel Labs Team on Vercel.

A member of the Team first needs to authorize it.

@joeyhotz

Copy link
Copy Markdown
ContributorAuthor

@vercel/workflow Ready for review. The PR follows the template; its remaining dependency is the paired workflow-server batch endpoint described above.

@VaguelySerious

VaguelySerious commented Jul 14, 2026

Copy link
Copy Markdown
Member

@joeyhotz Can you speak more to the motivation here? Is this useful for more than simply reducing request count over e.g. Promise.all([world.runs.get, ...])? If not, in what cases would the request count be an issue?

@joeyhotz

Copy link
Copy Markdown
ContributorAuthor

@joeyhotz Can you speak more to the motivation here? Is this useful for more than simply reducing request count over e.g. Promise.all([world.runs.get, ...])? If not, in what cases would the request count be an issue?

Hi @VaguelySerious. We're migrating from inngest to workflow sdk with the Postgres world. During the migration we decided to lean hard on the workflow sdk for storing and managing state. We have a graphql API. The reason why we need a batch fetch is we want to reduce the number of trips to the DB.

Think if you have a table of 25 entities that have a running job/previous job run behind it. For each entity you want to read the job timestamp or the job status (if it's running, or failed). We want to put this job status fetch in a data loader so the db read for the whole table is done in one trip, not 25.

@VaguelySerious

Copy link
Copy Markdown
Member

@joeyhotz I can see how this would reduce trips to the DB, and I'm mainly trying to assess what the actual performance impact is before adding a new method to the world interface for something that might have negligible impact for most users. Have you tried a Promise.all(...world.runs.get) and measured that against a tarball of this branch?

@joeyhotz

joeyhotz commented Jul 15, 2026

Copy link
Copy Markdown
ContributorAuthor

@joeyhotz I can see how this would reduce trips to the DB, and I'm mainly trying to assess what the actual performance impact is before adding a new method to the world interface for something that might have negligible impact for most users. Have you tried a Promise.all(...world.runs.get) and measured that against a tarball of this branch?

@VaguelySerious Ran a comparison locally with a local psql db here:

// Concurrent, but still N database queriesawaitPromise.all(runIds.map((id)=>world.runs.get(id,{resolveData: 'none'})));
// One database queryawaitworld.runs.getMany!(runIds,{resolveData: 'none'});
Run IDsPromise.all(runs.get)getMany
2525 queries, 1.69 ms1 query, 0.98 ms
100100 queries, 5.15 ms1 query, 1.85 ms

Promise.all still checks out one connection/query per run ID. With a 10-connection pool, 100 run reads compete with normal API traffic and execute in roughly ten pool waves. getMany uses one connection for one indexed IN (...) query.

I think the performance benefit will be higher in a remote environment too.

@joeyhotz

Copy link
Copy Markdown
ContributorAuthor

@VaguelySerious I'm new to the sdk so forgive me if I come across as naive, maybe another option is adding an ID filter to the list interface?

@joeyhotz
joeyhotzforce-pushed the joeyhotz/get-many-workflow-runs branch from 68837c0 to 96e8fddCompareJuly 15, 2026 02:21

@VaguelySeriousVaguelySerious left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Alright, we're good with shipping this, could you make one more update? world-local and world-vercel implementations duplicate some code from the runs.get handler internally. Since they essentially just Promise.all the same code, would you be able to re-use runs.get internally for those Worlds? Let me know if that doesn't make sense

@joeyhotz
joeyhotzforce-pushed the joeyhotz/get-many-workflow-runs branch from 96e8fdd to 42e446bCompareJuly 15, 2026 23:51
@joeyhotzjoeyhotz changed the title feat(world): add batch run readsfeat(world): add optional getMany() for batch run readsJul 16, 2026
@joeyhotz

Copy link
Copy Markdown
ContributorAuthor

Alright, we're good with shipping this, could you make one more update? world-local and world-vercel implementations duplicate some code from the runs.get handler internally. Since they essentially just Promise.all the same code, would you be able to re-use runs.get internally for those Worlds? Let me know if that doesn't make sense

@VaguelySerious makes sense! Have pushed the changes 👍

@joeyhotz
joeyhotzforce-pushed the joeyhotz/get-many-workflow-runs branch from 0494f11 to 81f3eebCompareJuly 16, 2026 00:22
@joeyhotz

Copy link
Copy Markdown
ContributorAuthor

pinging @vercel/workflow

@VaguelySerious

Copy link
Copy Markdown
Member

Sorry, merging is blocked on "Commits must have verified signatures". Would you be able to sign your commits and force-push?

Signed-off-by: Joey Hotz <joeyhotz1@gmail.com>
Delegate to the per-ID handler in world-local and world-vercel
getMany implementations instead of duplicating read, schema
validation, resolveData, and error-deserialization logic.
world-postgres keeps its single-query IN (...) path.
Signed-off-by: Joey Hotz <joeyhotz1@gmail.com>
@joeyhotz
joeyhotzforce-pushed the joeyhotz/get-many-workflow-runs branch from 81f3eeb to 5cec417CompareJuly 16, 2026 04:34
@joeyhotz

Copy link
Copy Markdown
ContributorAuthor

@VaguelySerious sorry, should be good now! thank you. not sure if i need to keep pinging @vercel/workflow but i will anyway

@VaguelySerious
VaguelySerious merged commit 7d29bab into vercel:mainJul 16, 2026
66 of 103 checks passed
@github-actionsgithub-actionsBot mentioned this pull request Jul 16, 2026
github-actionsBot added a commit that referenced this pull request Jul 16, 2026
Signed-off-by: Joey Hotz <joeyhotz1@gmail.com>
@github-actions

Copy link
Copy Markdown
Contributor

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

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@joeyhotz@VaguelySerious
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

feat(world): add optional getMany() for batch run reads - #2915

Merged
VaguelySerious merged 2 commits into
vercel:mainfrom
joeyhotz:joeyhotz/get-many-workflow-runs
Jul 16, 2026
Merged

feat(world): add optional getMany() for batch run reads#2915
VaguelySerious merged 2 commits into
vercel:mainfrom
joeyhotz:joeyhotz/get-many-workflow-runs

Conversation

@joeyhotz

@joeyhotzjoeyhotz commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Description

Add optional world.runs.getMany() for one-operation snapshots of multiple workflow runs. It lets GraphQL list resolvers and bulk orchestration fetch many run snapshots in a single storage call, avoiding N+1 reads.

Results preserve input order, fetch duplicate IDs once, and use null for missing runs. getMany remains optional so existing community Worlds are source-compatible.

local/vercel: delegate to the per-ID runs.get handler via Promise.all, sharing the same read, schema validation, resolveData, and error handling.

postgres: uses a single IN (...) query for an indexed batch read.

How did you test your changes?

  • pnpm --filter @workflow/world-local test — 465 tests pass, including order, duplicate-ID, missing-ID, and metadata-only getMany coverage.
  • pnpm --filter @workflow/world-postgres exec vitest run test/storage.test.ts --testNamePattern getMany — Postgres integration coverage passes for result mapping and a single-query assertion with duplicate IDs.
  • pnpm --filter @workflow/world-vercel test — 295 tests pass, including MockAgent coverage of individual GET delegation with duplicate-ID deduplication and missing-run null handling.

PR Checklist - Required to merge

  • 📦 pnpm changeset was run with minor changesets for @workflow/world, @workflow/world-local, @workflow/world-postgres, and @workflow/world-vercel.
  • 🔒 DCO sign-off passes.
  • 📝 Ping @vercel/workflow in a comment once the PR is ready, and the above checklist is complete.

@joeyhotz
joeyhotz requested review from a team and ijjk as code ownersJuly 14, 2026 15:34
@changeset-bot

changeset-botBot commented Jul 14, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 5cec417

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

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

@vercel

vercelBot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

@joeyhotz is attempting to deploy a commit to the Vercel Labs Team on Vercel.

A member of the Team first needs to authorize it.

@joeyhotz

Copy link
Copy Markdown
ContributorAuthor

@vercel/workflow Ready for review. The PR follows the template; its remaining dependency is the paired workflow-server batch endpoint described above.

@VaguelySerious

VaguelySerious commented Jul 14, 2026

Copy link
Copy Markdown
Member

@joeyhotz Can you speak more to the motivation here? Is this useful for more than simply reducing request count over e.g. Promise.all([world.runs.get, ...])? If not, in what cases would the request count be an issue?

@joeyhotz

Copy link
Copy Markdown
ContributorAuthor

@joeyhotz Can you speak more to the motivation here? Is this useful for more than simply reducing request count over e.g. Promise.all([world.runs.get, ...])? If not, in what cases would the request count be an issue?

Hi @VaguelySerious. We're migrating from inngest to workflow sdk with the Postgres world. During the migration we decided to lean hard on the workflow sdk for storing and managing state. We have a graphql API. The reason why we need a batch fetch is we want to reduce the number of trips to the DB.

Think if you have a table of 25 entities that have a running job/previous job run behind it. For each entity you want to read the job timestamp or the job status (if it's running, or failed). We want to put this job status fetch in a data loader so the db read for the whole table is done in one trip, not 25.

@VaguelySerious

Copy link
Copy Markdown
Member

@joeyhotz I can see how this would reduce trips to the DB, and I'm mainly trying to assess what the actual performance impact is before adding a new method to the world interface for something that might have negligible impact for most users. Have you tried a Promise.all(...world.runs.get) and measured that against a tarball of this branch?

@joeyhotz

joeyhotz commented Jul 15, 2026

Copy link
Copy Markdown
ContributorAuthor

@joeyhotz I can see how this would reduce trips to the DB, and I'm mainly trying to assess what the actual performance impact is before adding a new method to the world interface for something that might have negligible impact for most users. Have you tried a Promise.all(...world.runs.get) and measured that against a tarball of this branch?

@VaguelySerious Ran a comparison locally with a local psql db here:

// Concurrent, but still N database queriesawaitPromise.all(runIds.map((id)=>world.runs.get(id,{resolveData: 'none'})));
// One database queryawaitworld.runs.getMany!(runIds,{resolveData: 'none'});
Run IDsPromise.all(runs.get)getMany
2525 queries, 1.69 ms1 query, 0.98 ms
100100 queries, 5.15 ms1 query, 1.85 ms

Promise.all still checks out one connection/query per run ID. With a 10-connection pool, 100 run reads compete with normal API traffic and execute in roughly ten pool waves. getMany uses one connection for one indexed IN (...) query.

I think the performance benefit will be higher in a remote environment too.

@joeyhotz

Copy link
Copy Markdown
ContributorAuthor

@VaguelySerious I'm new to the sdk so forgive me if I come across as naive, maybe another option is adding an ID filter to the list interface?

@joeyhotz
joeyhotzforce-pushed the joeyhotz/get-many-workflow-runs branch from 68837c0 to 96e8fddCompareJuly 15, 2026 02:21

@VaguelySeriousVaguelySerious left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Alright, we're good with shipping this, could you make one more update? world-local and world-vercel implementations duplicate some code from the runs.get handler internally. Since they essentially just Promise.all the same code, would you be able to re-use runs.get internally for those Worlds? Let me know if that doesn't make sense

@joeyhotz
joeyhotzforce-pushed the joeyhotz/get-many-workflow-runs branch from 96e8fdd to 42e446bCompareJuly 15, 2026 23:51
@joeyhotzjoeyhotz changed the title feat(world): add batch run readsfeat(world): add optional getMany() for batch run readsJul 16, 2026
@joeyhotz

Copy link
Copy Markdown
ContributorAuthor

Alright, we're good with shipping this, could you make one more update? world-local and world-vercel implementations duplicate some code from the runs.get handler internally. Since they essentially just Promise.all the same code, would you be able to re-use runs.get internally for those Worlds? Let me know if that doesn't make sense

@VaguelySerious makes sense! Have pushed the changes 👍

@joeyhotz
joeyhotzforce-pushed the joeyhotz/get-many-workflow-runs branch from 0494f11 to 81f3eebCompareJuly 16, 2026 00:22
@joeyhotz

Copy link
Copy Markdown
ContributorAuthor

pinging @vercel/workflow

@VaguelySerious

Copy link
Copy Markdown
Member

Sorry, merging is blocked on "Commits must have verified signatures". Would you be able to sign your commits and force-push?

Signed-off-by: Joey Hotz <joeyhotz1@gmail.com>
Delegate to the per-ID handler in world-local and world-vercel
getMany implementations instead of duplicating read, schema
validation, resolveData, and error-deserialization logic.
world-postgres keeps its single-query IN (...) path.
Signed-off-by: Joey Hotz <joeyhotz1@gmail.com>
@joeyhotz
joeyhotzforce-pushed the joeyhotz/get-many-workflow-runs branch from 81f3eeb to 5cec417CompareJuly 16, 2026 04:34
@joeyhotz

Copy link
Copy Markdown
ContributorAuthor

@VaguelySerious sorry, should be good now! thank you. not sure if i need to keep pinging @vercel/workflow but i will anyway

@VaguelySerious
VaguelySerious merged commit 7d29bab into vercel:mainJul 16, 2026
66 of 103 checks passed
@github-actionsgithub-actionsBot mentioned this pull request Jul 16, 2026
github-actionsBot added a commit that referenced this pull request Jul 16, 2026
Signed-off-by: Joey Hotz <joeyhotz1@gmail.com>
@github-actions

Copy link
Copy Markdown
Contributor

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

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@joeyhotz@VaguelySerious
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(world): add optional getMany() for batch run reads - #2915

Merged
VaguelySerious merged 2 commits into
vercel:mainfrom
joeyhotz:joeyhotz/get-many-workflow-runs
Jul 16, 2026
Merged

feat(world): add optional getMany() for batch run reads#2915
VaguelySerious merged 2 commits into
vercel:mainfrom
joeyhotz:joeyhotz/get-many-workflow-runs

Conversation

@joeyhotz

@joeyhotzjoeyhotz commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Description

Add optional world.runs.getMany() for one-operation snapshots of multiple workflow runs. It lets GraphQL list resolvers and bulk orchestration fetch many run snapshots in a single storage call, avoiding N+1 reads.

Results preserve input order, fetch duplicate IDs once, and use null for missing runs. getMany remains optional so existing community Worlds are source-compatible.

local/vercel: delegate to the per-ID runs.get handler via Promise.all, sharing the same read, schema validation, resolveData, and error handling.

postgres: uses a single IN (...) query for an indexed batch read.

How did you test your changes?

  • pnpm --filter @workflow/world-local test — 465 tests pass, including order, duplicate-ID, missing-ID, and metadata-only getMany coverage.
  • pnpm --filter @workflow/world-postgres exec vitest run test/storage.test.ts --testNamePattern getMany — Postgres integration coverage passes for result mapping and a single-query assertion with duplicate IDs.
  • pnpm --filter @workflow/world-vercel test — 295 tests pass, including MockAgent coverage of individual GET delegation with duplicate-ID deduplication and missing-run null handling.

PR Checklist - Required to merge

  • 📦 pnpm changeset was run with minor changesets for @workflow/world, @workflow/world-local, @workflow/world-postgres, and @workflow/world-vercel.
  • 🔒 DCO sign-off passes.
  • 📝 Ping @vercel/workflow in a comment once the PR is ready, and the above checklist is complete.

@joeyhotz
joeyhotz requested review from a team and ijjk as code ownersJuly 14, 2026 15:34
@changeset-bot

changeset-botBot commented Jul 14, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 5cec417

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

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

@vercel

vercelBot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

@joeyhotz is attempting to deploy a commit to the Vercel Labs Team on Vercel.

A member of the Team first needs to authorize it.

@joeyhotz

Copy link
Copy Markdown
ContributorAuthor

@vercel/workflow Ready for review. The PR follows the template; its remaining dependency is the paired workflow-server batch endpoint described above.

@VaguelySerious

VaguelySerious commented Jul 14, 2026

Copy link
Copy Markdown
Member

@joeyhotz Can you speak more to the motivation here? Is this useful for more than simply reducing request count over e.g. Promise.all([world.runs.get, ...])? If not, in what cases would the request count be an issue?

@joeyhotz

Copy link
Copy Markdown
ContributorAuthor

@joeyhotz Can you speak more to the motivation here? Is this useful for more than simply reducing request count over e.g. Promise.all([world.runs.get, ...])? If not, in what cases would the request count be an issue?

Hi @VaguelySerious. We're migrating from inngest to workflow sdk with the Postgres world. During the migration we decided to lean hard on the workflow sdk for storing and managing state. We have a graphql API. The reason why we need a batch fetch is we want to reduce the number of trips to the DB.

Think if you have a table of 25 entities that have a running job/previous job run behind it. For each entity you want to read the job timestamp or the job status (if it's running, or failed). We want to put this job status fetch in a data loader so the db read for the whole table is done in one trip, not 25.

@VaguelySerious

Copy link
Copy Markdown
Member

@joeyhotz I can see how this would reduce trips to the DB, and I'm mainly trying to assess what the actual performance impact is before adding a new method to the world interface for something that might have negligible impact for most users. Have you tried a Promise.all(...world.runs.get) and measured that against a tarball of this branch?

@joeyhotz

joeyhotz commented Jul 15, 2026

Copy link
Copy Markdown
ContributorAuthor

@joeyhotz I can see how this would reduce trips to the DB, and I'm mainly trying to assess what the actual performance impact is before adding a new method to the world interface for something that might have negligible impact for most users. Have you tried a Promise.all(...world.runs.get) and measured that against a tarball of this branch?

@VaguelySerious Ran a comparison locally with a local psql db here:

// Concurrent, but still N database queriesawaitPromise.all(runIds.map((id)=>world.runs.get(id,{resolveData: 'none'})));
// One database queryawaitworld.runs.getMany!(runIds,{resolveData: 'none'});
Run IDsPromise.all(runs.get)getMany
2525 queries, 1.69 ms1 query, 0.98 ms
100100 queries, 5.15 ms1 query, 1.85 ms

Promise.all still checks out one connection/query per run ID. With a 10-connection pool, 100 run reads compete with normal API traffic and execute in roughly ten pool waves. getMany uses one connection for one indexed IN (...) query.

I think the performance benefit will be higher in a remote environment too.

@joeyhotz

Copy link
Copy Markdown
ContributorAuthor

@VaguelySerious I'm new to the sdk so forgive me if I come across as naive, maybe another option is adding an ID filter to the list interface?

@joeyhotz
joeyhotzforce-pushed the joeyhotz/get-many-workflow-runs branch from 68837c0 to 96e8fddCompareJuly 15, 2026 02:21

@VaguelySeriousVaguelySerious left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Alright, we're good with shipping this, could you make one more update? world-local and world-vercel implementations duplicate some code from the runs.get handler internally. Since they essentially just Promise.all the same code, would you be able to re-use runs.get internally for those Worlds? Let me know if that doesn't make sense

@joeyhotz
joeyhotzforce-pushed the joeyhotz/get-many-workflow-runs branch from 96e8fdd to 42e446bCompareJuly 15, 2026 23:51
@joeyhotzjoeyhotz changed the title feat(world): add batch run readsfeat(world): add optional getMany() for batch run readsJul 16, 2026
@joeyhotz

Copy link
Copy Markdown
ContributorAuthor

Alright, we're good with shipping this, could you make one more update? world-local and world-vercel implementations duplicate some code from the runs.get handler internally. Since they essentially just Promise.all the same code, would you be able to re-use runs.get internally for those Worlds? Let me know if that doesn't make sense

@VaguelySerious makes sense! Have pushed the changes 👍

@joeyhotz
joeyhotzforce-pushed the joeyhotz/get-many-workflow-runs branch from 0494f11 to 81f3eebCompareJuly 16, 2026 00:22
@joeyhotz

Copy link
Copy Markdown
ContributorAuthor

pinging @vercel/workflow

@VaguelySerious

Copy link
Copy Markdown
Member

Sorry, merging is blocked on "Commits must have verified signatures". Would you be able to sign your commits and force-push?

Signed-off-by: Joey Hotz <joeyhotz1@gmail.com>
Delegate to the per-ID handler in world-local and world-vercel
getMany implementations instead of duplicating read, schema
validation, resolveData, and error-deserialization logic.
world-postgres keeps its single-query IN (...) path.
Signed-off-by: Joey Hotz <joeyhotz1@gmail.com>
@joeyhotz
joeyhotzforce-pushed the joeyhotz/get-many-workflow-runs branch from 81f3eeb to 5cec417CompareJuly 16, 2026 04:34
@joeyhotz

Copy link
Copy Markdown
ContributorAuthor

@VaguelySerious sorry, should be good now! thank you. not sure if i need to keep pinging @vercel/workflow but i will anyway

@VaguelySerious
VaguelySerious merged commit 7d29bab into vercel:mainJul 16, 2026
66 of 103 checks passed
@github-actionsgithub-actionsBot mentioned this pull request Jul 16, 2026
github-actionsBot added a commit that referenced this pull request Jul 16, 2026
Signed-off-by: Joey Hotz <joeyhotz1@gmail.com>
@github-actions

Copy link
Copy Markdown
Contributor

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

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@joeyhotz@VaguelySerious
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(world): add optional getMany() for batch run reads - #2915

Merged
VaguelySerious merged 2 commits into
vercel:mainfrom
joeyhotz:joeyhotz/get-many-workflow-runs
Jul 16, 2026
Merged

feat(world): add optional getMany() for batch run reads#2915
VaguelySerious merged 2 commits into
vercel:mainfrom
joeyhotz:joeyhotz/get-many-workflow-runs

Conversation

@joeyhotz

@joeyhotzjoeyhotz commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Description

Add optional world.runs.getMany() for one-operation snapshots of multiple workflow runs. It lets GraphQL list resolvers and bulk orchestration fetch many run snapshots in a single storage call, avoiding N+1 reads.

Results preserve input order, fetch duplicate IDs once, and use null for missing runs. getMany remains optional so existing community Worlds are source-compatible.

local/vercel: delegate to the per-ID runs.get handler via Promise.all, sharing the same read, schema validation, resolveData, and error handling.

postgres: uses a single IN (...) query for an indexed batch read.

How did you test your changes?

  • pnpm --filter @workflow/world-local test — 465 tests pass, including order, duplicate-ID, missing-ID, and metadata-only getMany coverage.
  • pnpm --filter @workflow/world-postgres exec vitest run test/storage.test.ts --testNamePattern getMany — Postgres integration coverage passes for result mapping and a single-query assertion with duplicate IDs.
  • pnpm --filter @workflow/world-vercel test — 295 tests pass, including MockAgent coverage of individual GET delegation with duplicate-ID deduplication and missing-run null handling.

PR Checklist - Required to merge

  • 📦 pnpm changeset was run with minor changesets for @workflow/world, @workflow/world-local, @workflow/world-postgres, and @workflow/world-vercel.
  • 🔒 DCO sign-off passes.
  • 📝 Ping @vercel/workflow in a comment once the PR is ready, and the above checklist is complete.

@joeyhotz
joeyhotz requested review from a team and ijjk as code ownersJuly 14, 2026 15:34
@changeset-bot

changeset-botBot commented Jul 14, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 5cec417

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

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

@vercel

vercelBot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

@joeyhotz is attempting to deploy a commit to the Vercel Labs Team on Vercel.

A member of the Team first needs to authorize it.

@joeyhotz

Copy link
Copy Markdown
ContributorAuthor

@vercel/workflow Ready for review. The PR follows the template; its remaining dependency is the paired workflow-server batch endpoint described above.

@VaguelySerious

VaguelySerious commented Jul 14, 2026

Copy link
Copy Markdown
Member

@joeyhotz Can you speak more to the motivation here? Is this useful for more than simply reducing request count over e.g. Promise.all([world.runs.get, ...])? If not, in what cases would the request count be an issue?

@joeyhotz

Copy link
Copy Markdown
ContributorAuthor

@joeyhotz Can you speak more to the motivation here? Is this useful for more than simply reducing request count over e.g. Promise.all([world.runs.get, ...])? If not, in what cases would the request count be an issue?

Hi @VaguelySerious. We're migrating from inngest to workflow sdk with the Postgres world. During the migration we decided to lean hard on the workflow sdk for storing and managing state. We have a graphql API. The reason why we need a batch fetch is we want to reduce the number of trips to the DB.

Think if you have a table of 25 entities that have a running job/previous job run behind it. For each entity you want to read the job timestamp or the job status (if it's running, or failed). We want to put this job status fetch in a data loader so the db read for the whole table is done in one trip, not 25.

@VaguelySerious

Copy link
Copy Markdown
Member

@joeyhotz I can see how this would reduce trips to the DB, and I'm mainly trying to assess what the actual performance impact is before adding a new method to the world interface for something that might have negligible impact for most users. Have you tried a Promise.all(...world.runs.get) and measured that against a tarball of this branch?

@joeyhotz

joeyhotz commented Jul 15, 2026

Copy link
Copy Markdown
ContributorAuthor

@joeyhotz I can see how this would reduce trips to the DB, and I'm mainly trying to assess what the actual performance impact is before adding a new method to the world interface for something that might have negligible impact for most users. Have you tried a Promise.all(...world.runs.get) and measured that against a tarball of this branch?

@VaguelySerious Ran a comparison locally with a local psql db here:

// Concurrent, but still N database queriesawaitPromise.all(runIds.map((id)=>world.runs.get(id,{resolveData: 'none'})));
// One database queryawaitworld.runs.getMany!(runIds,{resolveData: 'none'});
Run IDsPromise.all(runs.get)getMany
2525 queries, 1.69 ms1 query, 0.98 ms
100100 queries, 5.15 ms1 query, 1.85 ms

Promise.all still checks out one connection/query per run ID. With a 10-connection pool, 100 run reads compete with normal API traffic and execute in roughly ten pool waves. getMany uses one connection for one indexed IN (...) query.

I think the performance benefit will be higher in a remote environment too.

@joeyhotz

Copy link
Copy Markdown
ContributorAuthor

@VaguelySerious I'm new to the sdk so forgive me if I come across as naive, maybe another option is adding an ID filter to the list interface?

@joeyhotz
joeyhotzforce-pushed the joeyhotz/get-many-workflow-runs branch from 68837c0 to 96e8fddCompareJuly 15, 2026 02:21

@VaguelySeriousVaguelySerious left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Alright, we're good with shipping this, could you make one more update? world-local and world-vercel implementations duplicate some code from the runs.get handler internally. Since they essentially just Promise.all the same code, would you be able to re-use runs.get internally for those Worlds? Let me know if that doesn't make sense

@joeyhotz
joeyhotzforce-pushed the joeyhotz/get-many-workflow-runs branch from 96e8fdd to 42e446bCompareJuly 15, 2026 23:51
@joeyhotzjoeyhotz changed the title feat(world): add batch run readsfeat(world): add optional getMany() for batch run readsJul 16, 2026
@joeyhotz

Copy link
Copy Markdown
ContributorAuthor

Alright, we're good with shipping this, could you make one more update? world-local and world-vercel implementations duplicate some code from the runs.get handler internally. Since they essentially just Promise.all the same code, would you be able to re-use runs.get internally for those Worlds? Let me know if that doesn't make sense

@VaguelySerious makes sense! Have pushed the changes 👍

@joeyhotz
joeyhotzforce-pushed the joeyhotz/get-many-workflow-runs branch from 0494f11 to 81f3eebCompareJuly 16, 2026 00:22
@joeyhotz

Copy link
Copy Markdown
ContributorAuthor

pinging @vercel/workflow

@VaguelySerious

Copy link
Copy Markdown
Member

Sorry, merging is blocked on "Commits must have verified signatures". Would you be able to sign your commits and force-push?

Signed-off-by: Joey Hotz <joeyhotz1@gmail.com>
Delegate to the per-ID handler in world-local and world-vercel
getMany implementations instead of duplicating read, schema
validation, resolveData, and error-deserialization logic.
world-postgres keeps its single-query IN (...) path.
Signed-off-by: Joey Hotz <joeyhotz1@gmail.com>
@joeyhotz
joeyhotzforce-pushed the joeyhotz/get-many-workflow-runs branch from 81f3eeb to 5cec417CompareJuly 16, 2026 04:34
@joeyhotz

Copy link
Copy Markdown
ContributorAuthor

@VaguelySerious sorry, should be good now! thank you. not sure if i need to keep pinging @vercel/workflow but i will anyway

@VaguelySerious
VaguelySerious merged commit 7d29bab into vercel:mainJul 16, 2026
66 of 103 checks passed
@github-actionsgithub-actionsBot mentioned this pull request Jul 16, 2026
github-actionsBot added a commit that referenced this pull request Jul 16, 2026
Signed-off-by: Joey Hotz <joeyhotz1@gmail.com>
@github-actions

Copy link
Copy Markdown
Contributor

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

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@joeyhotz@VaguelySerious
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

feat(world): add optional getMany() for batch run reads - #2915

Merged
VaguelySerious merged 2 commits into
vercel:mainfrom
joeyhotz:joeyhotz/get-many-workflow-runs
Jul 16, 2026
Merged

feat(world): add optional getMany() for batch run reads#2915
VaguelySerious merged 2 commits into
vercel:mainfrom
joeyhotz:joeyhotz/get-many-workflow-runs

Conversation

@joeyhotz

@joeyhotzjoeyhotz commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Description

Add optional world.runs.getMany() for one-operation snapshots of multiple workflow runs. It lets GraphQL list resolvers and bulk orchestration fetch many run snapshots in a single storage call, avoiding N+1 reads.

Results preserve input order, fetch duplicate IDs once, and use null for missing runs. getMany remains optional so existing community Worlds are source-compatible.

local/vercel: delegate to the per-ID runs.get handler via Promise.all, sharing the same read, schema validation, resolveData, and error handling.

postgres: uses a single IN (...) query for an indexed batch read.

How did you test your changes?

  • pnpm --filter @workflow/world-local test — 465 tests pass, including order, duplicate-ID, missing-ID, and metadata-only getMany coverage.
  • pnpm --filter @workflow/world-postgres exec vitest run test/storage.test.ts --testNamePattern getMany — Postgres integration coverage passes for result mapping and a single-query assertion with duplicate IDs.
  • pnpm --filter @workflow/world-vercel test — 295 tests pass, including MockAgent coverage of individual GET delegation with duplicate-ID deduplication and missing-run null handling.

PR Checklist - Required to merge

  • 📦 pnpm changeset was run with minor changesets for @workflow/world, @workflow/world-local, @workflow/world-postgres, and @workflow/world-vercel.
  • 🔒 DCO sign-off passes.
  • 📝 Ping @vercel/workflow in a comment once the PR is ready, and the above checklist is complete.

@joeyhotz
joeyhotz requested review from a team and ijjk as code ownersJuly 14, 2026 15:34
@changeset-bot

changeset-botBot commented Jul 14, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 5cec417

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

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

@vercel

vercelBot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

@joeyhotz is attempting to deploy a commit to the Vercel Labs Team on Vercel.

A member of the Team first needs to authorize it.

@joeyhotz

Copy link
Copy Markdown
ContributorAuthor

@vercel/workflow Ready for review. The PR follows the template; its remaining dependency is the paired workflow-server batch endpoint described above.

@VaguelySerious

VaguelySerious commented Jul 14, 2026

Copy link
Copy Markdown
Member

@joeyhotz Can you speak more to the motivation here? Is this useful for more than simply reducing request count over e.g. Promise.all([world.runs.get, ...])? If not, in what cases would the request count be an issue?

@joeyhotz

Copy link
Copy Markdown
ContributorAuthor

@joeyhotz Can you speak more to the motivation here? Is this useful for more than simply reducing request count over e.g. Promise.all([world.runs.get, ...])? If not, in what cases would the request count be an issue?

Hi @VaguelySerious. We're migrating from inngest to workflow sdk with the Postgres world. During the migration we decided to lean hard on the workflow sdk for storing and managing state. We have a graphql API. The reason why we need a batch fetch is we want to reduce the number of trips to the DB.

Think if you have a table of 25 entities that have a running job/previous job run behind it. For each entity you want to read the job timestamp or the job status (if it's running, or failed). We want to put this job status fetch in a data loader so the db read for the whole table is done in one trip, not 25.

@VaguelySerious

Copy link
Copy Markdown
Member

@joeyhotz I can see how this would reduce trips to the DB, and I'm mainly trying to assess what the actual performance impact is before adding a new method to the world interface for something that might have negligible impact for most users. Have you tried a Promise.all(...world.runs.get) and measured that against a tarball of this branch?

@joeyhotz

joeyhotz commented Jul 15, 2026

Copy link
Copy Markdown
ContributorAuthor

@joeyhotz I can see how this would reduce trips to the DB, and I'm mainly trying to assess what the actual performance impact is before adding a new method to the world interface for something that might have negligible impact for most users. Have you tried a Promise.all(...world.runs.get) and measured that against a tarball of this branch?

@VaguelySerious Ran a comparison locally with a local psql db here:

// Concurrent, but still N database queriesawaitPromise.all(runIds.map((id)=>world.runs.get(id,{resolveData: 'none'})));
// One database queryawaitworld.runs.getMany!(runIds,{resolveData: 'none'});
Run IDsPromise.all(runs.get)getMany
2525 queries, 1.69 ms1 query, 0.98 ms
100100 queries, 5.15 ms1 query, 1.85 ms

Promise.all still checks out one connection/query per run ID. With a 10-connection pool, 100 run reads compete with normal API traffic and execute in roughly ten pool waves. getMany uses one connection for one indexed IN (...) query.

I think the performance benefit will be higher in a remote environment too.

@joeyhotz

Copy link
Copy Markdown
ContributorAuthor

@VaguelySerious I'm new to the sdk so forgive me if I come across as naive, maybe another option is adding an ID filter to the list interface?

@joeyhotz
joeyhotzforce-pushed the joeyhotz/get-many-workflow-runs branch from 68837c0 to 96e8fddCompareJuly 15, 2026 02:21

@VaguelySeriousVaguelySerious left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Alright, we're good with shipping this, could you make one more update? world-local and world-vercel implementations duplicate some code from the runs.get handler internally. Since they essentially just Promise.all the same code, would you be able to re-use runs.get internally for those Worlds? Let me know if that doesn't make sense

@joeyhotz
joeyhotzforce-pushed the joeyhotz/get-many-workflow-runs branch from 96e8fdd to 42e446bCompareJuly 15, 2026 23:51
@joeyhotzjoeyhotz changed the title feat(world): add batch run readsfeat(world): add optional getMany() for batch run readsJul 16, 2026
@joeyhotz

Copy link
Copy Markdown
ContributorAuthor

Alright, we're good with shipping this, could you make one more update? world-local and world-vercel implementations duplicate some code from the runs.get handler internally. Since they essentially just Promise.all the same code, would you be able to re-use runs.get internally for those Worlds? Let me know if that doesn't make sense

@VaguelySerious makes sense! Have pushed the changes 👍

@joeyhotz
joeyhotzforce-pushed the joeyhotz/get-many-workflow-runs branch from 0494f11 to 81f3eebCompareJuly 16, 2026 00:22
@joeyhotz

Copy link
Copy Markdown
ContributorAuthor

pinging @vercel/workflow

@VaguelySerious

Copy link
Copy Markdown
Member

Sorry, merging is blocked on "Commits must have verified signatures". Would you be able to sign your commits and force-push?

Signed-off-by: Joey Hotz <joeyhotz1@gmail.com>
Delegate to the per-ID handler in world-local and world-vercel
getMany implementations instead of duplicating read, schema
validation, resolveData, and error-deserialization logic.
world-postgres keeps its single-query IN (...) path.
Signed-off-by: Joey Hotz <joeyhotz1@gmail.com>
@joeyhotz
joeyhotzforce-pushed the joeyhotz/get-many-workflow-runs branch from 81f3eeb to 5cec417CompareJuly 16, 2026 04:34
@joeyhotz

Copy link
Copy Markdown
ContributorAuthor

@VaguelySerious sorry, should be good now! thank you. not sure if i need to keep pinging @vercel/workflow but i will anyway

@VaguelySerious
VaguelySerious merged commit 7d29bab into vercel:mainJul 16, 2026
66 of 103 checks passed
@github-actionsgithub-actionsBot mentioned this pull request Jul 16, 2026
github-actionsBot added a commit that referenced this pull request Jul 16, 2026
Signed-off-by: Joey Hotz <joeyhotz1@gmail.com>
@github-actions

Copy link
Copy Markdown
Contributor

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

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@joeyhotz@VaguelySerious