Skip to content

fix(core): align pagination cursor with sort order in execution list#28345

Open
tom-arnaud wants to merge 1 commit inton8n-io:masterfrom
tom-arnaud:fix/execution-pagination-order
Open

fix(core): align pagination cursor with sort order in execution list#28345
tom-arnaud wants to merge 1 commit inton8n-io:masterfrom
tom-arnaud:fix/execution-pagination-order

Conversation

@tom-arnaud
Copy link
Copy Markdown

Summary

Fixes execution list pagination skipping rows when using lastId cursor with startedAt DESC sort order.

The problem

When listing executions via GET /rest/executions with lastId pagination:

  1. findRangeWithCount does not set query.order, so the repository defaults to ORDER BY id DESC
  2. The cursor filter uses WHERE id < :lastId — an ID-based cursor
  3. But the UI displays results sorted by startedAt DESC

This works fine when IDs and startedAt are in the same order. But Wait node workflows break this assumption: executions are created at once (sequential IDs) but resume at different times (non-sequential startedAt).

Concrete example

A workflow triggered at 07:00 creates executions 158685–158773 with Wait nodes. They resume throughout the day:

ID 158690  →  startedAt 18:17  (resumes late)
ID 158725  →  startedAt 18:24  (resumes later)
ID 158772  →  startedAt 18:05  (resumes earlier)

Page 1 (sorted by startedAt DESC, limit 10) ends with id=158725 (startedAt=18:24).

Page 2 sends lastId=158725. The query becomes:

WHERE id < 158725 ORDER BY id DESC LIMIT 10

This returns only 5 rows — missing all executions with id > 158725 but startedAt < 18:24 (e.g. 158772 at 18:05, 158745 at 18:20, etc.). 41 executions become unreachable through pagination.

The fix

Two changes:

  1. execution.service.ts: findRangeWithCount now defaults to order: { startedAt: 'DESC' } when no order is specified — consistent with findLatestCurrentAndCompleted which already does this.

  2. execution.repository.ts: When ordering by startedAt, the cursor resolves lastId/firstId to their startedAt value via a subquery:

-- Before (bug):
WHERE id < :lastId ORDER BY id DESC

-- After (fix):
WHERE COALESCE(startedAt, createdAt) < (
  SELECT COALESCE(startedAt, createdAt) FROM execution_entity WHERE id = :lastId
)
ORDER BY COALESCE(startedAt, createdAt) DESC

ID-based cursors are preserved for other sort orders (e.g. order.top) where IDs are monotonic.

Test plan

  • Verified on production Postgres (428 executions) — bug returns 377 reachable rows, fix returns all 418 within the same time window
  • Full pagination simulation confirms 428/428 unique IDs covered with no gaps or duplicates
  • Existing integration tests in execution.service.integration.test.ts should still pass (same behavior when IDs and startedAt are in order)
  • Edge case: executions with NULL startedAt fall back to createdAt via COALESCE

Related

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

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

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 }
Loading

@CLAassistant
Copy link
Copy Markdown

CLAassistant commented Apr 10, 2026

CLA assistant check
All committers have signed the CLA.

@n8n-assistant n8n-assistant bot added community Authored by a community member core Enhancement outside /nodes-base and /editor-ui in linear DEPRECATED labels Apr 10, 2026
@n8n-assistant
Copy link
Copy Markdown
Contributor

n8n-assistant bot commented Apr 10, 2026

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:
• Tests are included for any new functionality, logic changes or bug fixes.
• The PR aligns with our contribution guidelines.

Regarding new nodes:
We no longer accept new nodes directly into the core codebase. Instead, we encourage contributors to follow our Community Node Submission Guide to publish nodes independently.

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:
This PR has been added to our internal tracker as "GHC-7684". While we plan to review it, we are currently unable to provide an exact timeframe. Our goal is to begin reviews within a month, but this may change depending on team priorities. We will reach out when the review begins.

Thank you again for contributing to n8n.

@tom-arnaud tom-arnaud force-pushed the fix/execution-pagination-order branch 3 times, most recently from 7c115be to c16f186 Compare April 10, 2026 17:39
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>
@tom-arnaud tom-arnaud force-pushed the fix/execution-pagination-order branch from c16f186 to 2775cc7 Compare April 10, 2026 17:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

community Authored by a community member core Enhancement outside /nodes-base and /editor-ui in linear DEPRECATED

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants