The cursor is opaque and forward-only — treat it that way

HubSpot's CRM v3 list endpoints (contacts, companies, deals, and custom objects) return results with a `paging` object. When more results exist, `paging.next.after` contains the cursor value to send as the `after` query parameter on the next request; when it's absent, you've reached the last page. This is fundamentally different from offset-based pagination — you cannot jump to "page 5" or request the previous page by arithmetic, and the cursor value itself is opaque and should not be parsed or predicted.

The most common n8n mistake is building a Loop Over Items or manual HTTP Request loop with a fixed number of iterations, or a hardcoded page-size × page-count total, based on the record count observed on a previous run. HubSpot portals are live systems — records are created, updated, merged, and deleted continuously, so a count taken minutes earlier is already stale by the time a long-running workflow finishes paginating.

The correct loop condition is: keep requesting the next page using the returned `after` value until the response has no `paging.next` field at all. Any stopping condition based on a fixed iteration count, a fixed total, or reaching a specific `after` value you calculated in advance is a latent source of missed records.

Why records go missing even when the loop looks correct

A real HubSpot Community bug report describes result sets returned out of order when using the `after` cursor, which the reporter says produced both duplicated and missing rows once combined across pages. Independent of that specific bug, the same symptom appears whenever the underlying dataset changes between page requests: if a workflow queries contacts sorted by creation date and new contacts are created while the loop is running, records can shift between pages in a way that causes some to be skipped entirely and others to appear on two consecutive pages.

A second, easier-to-miss cause is client-side filtering applied per page instead of after all pages are collected. If a workflow filters out records that don't match a condition on each page individually, and the condition depends on a field that can be updated mid-run (for example, a lifecycle stage), a record can fail the filter on page 3 in a way it would have passed if evaluated a few minutes earlier or later — this looks like a missing record but is actually a filtering-timing issue, not a pagination bug.

A third cause specific to n8n is running two workflow executions of the same pagination logic concurrently — for example, a scheduled trigger firing again before the previous run finished. Each execution pages through the same live dataset independently, and if either one is interrupted partway, reconciling which records each execution actually captured becomes difficult without a shared, persisted checkpoint.

  • Filter records after collecting all pages, not incrementally per page, when the filter condition can change mid-run.
  • Sort by a field that is set once and never changes (like creation timestamp) rather than a field that updates frequently, if the API supports choosing the sort field.
  • Use n8n's workflow-level concurrency setting or an external lock to prevent two executions of the same pagination job from overlapping.
  • Log the `after` cursor value at each step so a partial run can be diagnosed after the fact.

Why the same record can be processed twice downstream

Duplicate processing is almost always a resume problem, not a HubSpot API problem. If a pagination loop fails on page 6 of 10 and the retry logic restarts the entire workflow from page 1 with no memory of already having processed pages 1 through 5, every record on those pages is fetched and pushed downstream again.

This matters more in n8n than in a simple script because a failed workflow execution in n8n is often re-triggered by re-running the whole workflow manually or by a scheduled trigger firing again on its normal interval, both of which restart from the beginning unless the workflow explicitly persists and reads a checkpoint.

The fix is to persist the last successfully processed `after` cursor somewhere outside the workflow's in-memory execution state — a small database row, a key-value store, or even a dedicated HubSpot-adjacent record — and have the workflow read that checkpoint at the start of every run, resuming from there instead of from the beginning.

If a partial pagination run also triggered downstream actions (creating records elsewhere, sending notifications), make those actions idempotent using the HubSpot record ID as a stable key — that protects you even if a checkpoint is momentarily wrong.

A checkpointed pagination pattern in n8n

Store the checkpoint (the last `after` value and a timestamp) in whatever persistent storage the rest of the workflow already uses — a database node, a simple file via the file system, or a lightweight key-value service. Read it at the very start of the workflow, before the first HubSpot request.

Structure the loop so the cursor is only persisted after the current page's records have been fully processed by every downstream step, not immediately after the page is fetched. This ensures a failure between "fetched page" and "finished processing page" causes that page to be retried rather than skipped.

When the loop finally reaches a response with no `paging.next`, clear or reset the checkpoint (or mark the run as complete) so the next scheduled execution starts a fresh pagination pass from the beginning rather than trying to resume a completed one.

On workflow start:
  Read stored checkpoint { after: string|null, complete: boolean }
  If complete === true → start a fresh pass (checkpoint = { after: null, complete: false })

Loop:
  Request page using current `after` (or first page if null)
  Process every record on this page fully (all downstream actions)
  Persist checkpoint { after: response.paging.next.after, complete: false }
  If response.paging.next is absent:
    Persist checkpoint { after: null, complete: true }
    Exit loop

Add a periodic reconciliation check instead of trusting the loop blindly

Even a correctly built cursor loop is checking a live, changing dataset, so treat an occasional discrepancy as expected rather than assuming any mismatch means the workflow is broken. Periodically compare a simple HubSpot count (using a search or list endpoint's total, when available for the object type) against the count your workflow actually processed over the same rough window, and investigate only when the gap is large or persistent rather than a handful of records that can be explained by records changing mid-run.

For anything downstream that depends on completeness — a nightly export, a compliance report, a full re-sync — build in a lightweight secondary check: spot-verify a sample of record IDs that should have been captured, or compare a total count before and after, rather than trusting an unattended loop that reports success purely because it didn't throw an error.

Verification checklist

  • Loop condition is based on the presence of `paging.next`, not a fixed page count or precomputed total.
  • Filtering happens after all pages are collected when the filter condition can change while the loop runs.
  • Only one execution of this pagination workflow can run at a time (concurrency guard in place).
  • The last successful `after` cursor is persisted outside the workflow's execution memory and read on every run.
  • Downstream actions triggered per record are idempotent, keyed by the HubSpot record ID.

Sources checked for this guide

HubSpot's paging behavior for CRM v3 endpoints comes from HubSpot's CRM API documentation. The specific out-of-order pagination bug report comes from HubSpot's own community forum. n8n pagination and cursor-handling patterns come from n8n Community discussions describing the same class of missing-record problem across different APIs, not HubSpot-specific threads, since the underlying pattern is the same regardless of API.