fix(core): align pagination cursor with sort order in execution list#28345
fix(core): align pagination cursor with sort order in execution list#28345tom-arnaud wants to merge 1 commit inton8n-io:masterfrom
Conversation
There was a problem hiding this comment.
No issues found across 2 files
Architecture diagram
sequenceDiagram
participant Client as UI / REST API
participant Service as ExecutionService
participant Repo as ExecutionRepository
participant DB as Database (Postgres/SQLite)
Note over Client,DB: Pagination Flow (GET /rest/executions)
Client->>Service: findRangeWithCount(query)
opt No sort order provided
Service->>Service: NEW: Default to { startedAt: 'DESC' }
end
Service->>Repo: findManyByRangeQuery(query)
alt NEW: Sort is startedAt DESC
Repo->>DB: NEW: Subquery: SELECT COALESCE(startedAt, createdAt) FROM execution WHERE id = :lastId
DB-->>Repo: cursorTimestamp
Repo->>DB: Fetch page WHERE COALESCE(startedAt, createdAt) < :cursorTimestamp ORDER BY timestamp DESC
Note right of DB: Fixes skip/gap issues from<br/>non-sequential Wait node resumes
else Other Sort Orders (e.g. top, ID)
Repo->>DB: Fetch page WHERE id < :lastId ORDER BY id DESC
end
DB-->>Repo: Page results (List of Executions)
Repo-->>Service: ExecutionEntity[]
Service->>Repo: count(query)
Repo-->>Service: total count
Service-->>Client: { results, count, estimate }
|
Hey @tom-arnaud, Thank you for your contribution. We appreciate the time and effort you’ve taken to submit this pull request. Before we can proceed, please ensure the following: Regarding new nodes: If your node integrates with an AI service that you own or represent, please email nodes@n8n.io and we will be happy to discuss the best approach. About review timelines: Thank you again for contributing to n8n. |
7c115be to
c16f186
Compare
When listing executions via `GET /rest/executions` with `lastId`
pagination, the cursor filter used `WHERE id < :lastId` while the
results were sorted by `startedAt DESC`. This mismatch caused
executions to be skipped when their IDs did not follow `startedAt`
order — which happens with Wait node executions that are all created
at once (sequential IDs) but resume at different times throughout
the day (non-sequential startedAt).
Changes:
1. **execution.repository.ts — `toQueryBuilder()`**: When ordering by
startedAt, the cursor now resolves `lastId`/`firstId` to their
startedAt value via a subquery, with ID as tiebreaker for rows
sharing the same startedAt timestamp (keyset pagination).
2. **execution.repository.ts — `toQueryBuilderWithAnnotations()`**: Added
missing `id DESC` tiebreaker to the outer query's ORDER BY, ensuring
deterministic ordering after annotation joins.
3. **execution.service.ts — `findRangeWithCount()`**: Defaults to
`order: { startedAt: 'DESC' }` when no order is specified, consistent
with `findLatestCurrentAndCompleted`.
4. **execution.service.ts — `findLatestCurrentAndCompleted()`**: Strips
`lastId`/`firstId` from the `currentQuery` so running/new executions
are always fetched regardless of pagination cursor. Previously, the
cursor was inherited and excluded running executions with id >= lastId.
5. **executions.store.ts**: Frontend sort now uses `startedAt` (with
`createdAt` fallback and `id` tiebreaker) instead of `createdAt`,
aligning with the backend sort order so `loadMore()` sends the
correct cursor ID.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
c16f186 to
2775cc7
Compare
Summary
Fixes execution list pagination skipping rows when using
lastIdcursor withstartedAt DESCsort order.The problem
When listing executions via
GET /rest/executionswithlastIdpagination:findRangeWithCountdoes not setquery.order, so the repository defaults toORDER BY id DESCWHERE id < :lastId— an ID-based cursorstartedAt DESCThis works fine when IDs and
startedAtare in the same order. But Wait node workflows break this assumption: executions are created at once (sequential IDs) but resume at different times (non-sequentialstartedAt).Concrete example
A workflow triggered at 07:00 creates executions 158685–158773 with Wait nodes. They resume throughout the day:
Page 1 (sorted by
startedAt DESC, limit 10) ends withid=158725(startedAt=18:24).Page 2 sends
lastId=158725. The query becomes:This returns only 5 rows — missing all executions with
id > 158725butstartedAt < 18:24(e.g. 158772 at 18:05, 158745 at 18:20, etc.). 41 executions become unreachable through pagination.The fix
Two changes:
execution.service.ts:findRangeWithCountnow defaults toorder: { startedAt: 'DESC' }when no order is specified — consistent withfindLatestCurrentAndCompletedwhich already does this.execution.repository.ts: When ordering bystartedAt, the cursor resolveslastId/firstIdto theirstartedAtvalue via a subquery:ID-based cursors are preserved for other sort orders (e.g.
order.top) where IDs are monotonic.Test plan
execution.service.integration.test.tsshould still pass (same behavior when IDs and startedAt are in order)NULLstartedAt fall back tocreatedAtviaCOALESCERelated
This affects any workflow using Wait nodes, Send and Wait, or any pattern where executions are bulk-created then resume later.
🤖 Generated with Claude Code