Skip to content

Allow runs.list({ status }) to accept an array - #3707

Open
unusdon wants to merge 3 commits into
vercel:mainfrom
unusdon:fix/runs-list-status-array
Open

Allow runs.list({ status }) to accept an array#3707
unusdon wants to merge 3 commits into
vercel:mainfrom
unusdon:fix/runs-list-status-array

Conversation

@unusdon

@unusdon unusdon commented Aug 21, 2026

Copy link
Copy Markdown

Fixes #3667.

What changed

ListWorkflowRunsParams.status now accepts WorkflowRunStatus | WorkflowRunStatus[], so callers can express set filters (e.g. "not terminal") without restating the status vocabulary in application code.

// Before — one call per status
for (const status of ['pending', 'running'] as const) {
  const page = await runs.list({ status, ... });
  // ...
}

// After — single call
const page = await runs.list({ status: ['pending', 'running'], ... });

Both world backends fan the array out server-side:

  • @workflow/world-localArray.isArray + .includes on the fs filter
  • @workflow/world-postgres — dispatches to Drizzle's inArray() when the caller passes an array, eq() otherwise (both already imported for the TERMINAL_WORKFLOW_RUN_STATUSES path, so no new deps)

Backwards-compatible — the single-string form still works, and existing callers/tests need no updates apart from the two reenqueue tests whose call-count assertions reflected the old 2×2-page shape.

reenqueueActiveRuns now issues a single call

The immediate consumer: the recovery path in @workflow/world. Its per-status loop collapses into one call:

-  for (const status of ['pending', 'running'] as const) {
-    let cursor: string | undefined;
-    let hasMore = true;
-    while (hasMore) {
-      const page = await runs.list({ status, resolveData: 'none', pagination: { cursor } });
-      // ...
-    }
+  let cursor: string | undefined;
+  let hasMore = true;
+  while (hasMore) {
+    const page = await runs.list({
+      status: ['pending', 'running'],
+      resolveData: 'none',
+      pagination: { cursor },
+    });
+    // ...
   }

Tests

@workflow/world (recovery.test.ts):

  • Existing createRuns mock updated to accept array or string
  • New test explicitly asserts: runs.list is called exactly once with status: ['pending', 'running']

@workflow/world-postgres (reenqueue.test.ts):

  • mockRunsList handles both single-string and array
  • pages through all active runs retargeted: expects 2 list calls (single filter × 2 pages) instead of 4 (2 statuses × 2 pages), and 2 enqueues both from the first page

Verified locally:

  • @workflow/world recovery tests: 3/3 pass
  • @workflow/world-local full suite: 549/549 pass
  • @workflow/world-postgres reenqueue tests: 12/12 pass
  • All 3 touched packages typecheck clean

(I could not run the test:e2e matrix or the postgres testcontainer suite locally — those need Docker + a preview deploy.)

Changeset

.changeset/runs-list-status-array.md@workflow/world minor (public API widened), @workflow/world-local and @workflow/world-postgres patch (impl-side).

Notes

  • Followed the shape suggested in the issue (status?: WorkflowRunStatus | WorkflowRunStatus[]) rather than the alternative terminal?: boolean, since the array form composes with any subset (not just terminal/non-terminal) and keeps status ownership inside the package
  • Did not touch #2978's startup-recovery scope concern; happy to follow up if you have a preferred direction

@unusdon
unusdon requested a review from a team as a code owner August 21, 2026 02:34
@changeset-bot

changeset-bot Bot commented Aug 21, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: bfa7e86

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

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

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

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

@vercel

vercel Bot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

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

A member of the Team first needs to authorize it.

Comment thread packages/world-local/src/storage/runs-storage.ts Outdated
ListWorkflowRunsParams.status now accepts WorkflowRunStatus | WorkflowRunStatus[]
so callers can express set filters (e.g. 'not terminal') without having to
issue one paginated list() per status. Both world backends fan the array
out server-side.

- @workflow/world: widen the type
- @workflow/world-local: fs filter uses Array.isArray + includes
- @workflow/world-postgres: dispatch to inArray() when the caller passes an array,
  eq() otherwise — inArray is already imported for the TERMINAL_WORKFLOW_RUN_STATUSES
  path so no new deps
- @workflow/world (recovery.ts): reenqueueActiveRuns collapses its per-status
  loop into a single call with status: ['pending', 'running'], which is what
  vercel#3667 identified as the immediate consumer benefit

Backwards-compatible: the single-string form still works — existing callers
and existing tests need no updates apart from the two reenqueue tests whose
call-count assertions reflected the old 2×2 shape.

Tests updated + added:
- packages/world/src/recovery.test.ts: mock accepts array or string; new
  test asserts the single-call behaviour with status: ['pending', 'running']
- packages/world-postgres/src/reenqueue.test.ts: mockRunsList and the
  'pages through all active runs' assertion updated for the single-call shape

All 3 world tests + 549 world-local tests + 12 world-postgres reenqueue tests
pass. All 3 touched packages typecheck clean.

Signed-off-by: unusdon <unusdon@gmail.com>
@unusdon
unusdon force-pushed the fix/runs-list-status-array branch from 895b74e to 0da6539 Compare August 21, 2026 03:02

@karthikscale3 karthikscale3 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.

thanks for the PR. this change mostly looks good to me except for 2 issues that i have flagged. before we can merge this, we need to build parity for this capability for workflows running on vercel too so we do not advertise a public API capability that the vercel world doesn't implement. will take it back to the team and get back to you by early next week. please take care of the review comments in the meantime.

@karthikscale3 karthikscale3 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.

world-local: status: [] skips the filter and returns every run. Since omitted status already means “unfiltered,” an explicit empty set should return no runs (or be rejected), and must match Postgres behavior.

world-postgres: inArray(..., []) returns no rows, unlike world-local. The shared Storage['runs'].list contract needs one documented empty-array semantic and a cross-backend test.

Per @karthikscale3's review — the empty-array semantic was divergent:
- world-local: skipped the filter (returned every run)
- world-postgres: SQL `inArray(..., [])` → `IN ()` → returned no rows

Fixed world-local to match world-postgres: empty array matches no runs.
Callers who want the unfiltered set must omit the field, same as the
single-status form. Documented the contract on ListWorkflowRunsParams.

Added mirrored 'runs > list' tests to both backends' storage.test.ts:
- single-status filter
- array-of-statuses filter (matches any)
- empty array → 0 runs (the specific case @karthikscale3 flagged)
- omitted status field → filter unset

world-local + world recovery tests: 252/252 pass locally. world-postgres
storage tests need Docker/testcontainers so I couldn't run them, but the
test additions mirror world-local's shape 1:1.

world-vercel parity noted separately in the review — leaving that piece
for the team as discussed.

Signed-off-by: unusdon <unusdon@gmail.com>
@unusdon

unusdon commented Aug 21, 2026

Copy link
Copy Markdown
Author

Thanks @karthikscale3 — pushed f53fba7 addressing both flagged issues.

Empty-array semantic normalised to "no runs" in both backends. Root cause was on my side: the world-local filter had a statuses.length > 0 && guard that short-circuited the empty case to "match everything", diverging from world-postgres's SQL IN () → false. Dropped the guard so [].includes(x) returns false, mirrors postgres exactly. Documented the contract on ListWorkflowRunsParams.status:

Empty-array semantics: status: [] matches no runs (mirrors SQL IN ()). To leave the filter unset, omit the field entirely — that behaviour is unchanged from single-status callers.

Cross-backend tests added. Mirrored 4 runs > list cases into both packages/world-local/src/storage.test.ts and packages/world-postgres/test/storage.test.ts:

  • Filters by a single status
  • Filters by an array of statuses (matches any)
  • Returns no runs when status is [] (the specific case you flagged)
  • Leaves the filter unset when status field is omitted

world-local + world recovery tests: 252/252 pass locally. world-postgres storage tests need Docker/testcontainers so I couldn't run them from my machine, but the test additions mirror world-local's shape 1:1 — happy to iterate if CI surfaces anything.

On world-vercel parity — understood, leaving that for the team as you suggested. If it's useful, the postgres-side approach transfers directly: dispatch to whatever the vercel world's equivalent of inArray is (probably a status IN clause in the underlying query) when the caller passes an array, else the single-status path. Happy to open a follow-up PR once you have the direction, or just let it ride with the world team's changes.

Comment thread .changeset/runs-list-status-array.md Outdated
Signed-off-by: Peter Wielander <mittgfu@gmail.com>
@VaguelySerious

Copy link
Copy Markdown
Member

@unusdon Commits must have a verified signature for us to be able to merge them. Can you have your agent force-push this PR with a new squashed and signed commit?

@VaguelySerious VaguelySerious changed the title Allow runs.list({ status }) to accept an array (fixes #3667) Allow runs.list({ status }) to accept an array Aug 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

runs.list cannot express a non-terminal status filter

3 participants