Tips > Building Workflows

Handle Pagination with Loop Nodes and Cursor Tracking

Most APIs return paginated results -- 100 records per page with a cursor or offset to fetch the next page.

TipIntermediate2 min read

Most APIs return paginated results -- 100 records per page with a cursor or offset to fetch the next page. n8n's Loop node (or the built-in pagination in the HTTP Request node) handles this, but the built-in pagination only covers simple cases. For cursor-based pagination or APIs with non-standard pagination schemes, use a Loop node with explicit cursor tracking.

Real-world example: You need to fetch all issues from a Jira project. The API returns 50 results per page with a startAt offset and a total count.

Trigger
  |
  Loop Node (loop until all pages fetched)
  |  |
  |  +-> HTTP Request: Jira API (current page)
  |  |
  |  +-> Code Node: Check if more pages
  |  |     (outputs: "loop" back to Loop, or "done" to exit)
  |  |
  |  +-> [loop output connects back to Loop Node]
  |
  Code Node: Combine All Pages
  |
  [downstream processing]
```text
```javascript title="Code Node: Pagination Controller (inside the Loop)"
const staticData = $getWorkflowStaticData("node");

// Initialize on first iteration
if (!staticData.allResults) {
  staticData.allResults = [];
  staticData.startAt = 0;
}

const response = $input.first().json;
const pageSize = 50;

// Accumulate results
staticData.allResults.push(...response.issues);
staticData.startAt += pageSize;

// Check if there are more pages
if (staticData.startAt < response.total) {
  // More pages -- output to "loop" branch with next offset
  return [
    [{
      json: {
        startAt: staticData.startAt,
        maxResults: pageSize
      }
    }],
    []  // empty "done" branch
  ];
} else {
  // All pages fetched -- output to "done" branch
  const allResults = staticData.allResults;

  // Clean up static data
  delete staticData.allResults;
  delete staticData.startAt;

  return [
    [],  // empty "loop" branch
    allResults.map(issue => ({ json: issue }))
  ];
}
```text
```text title="HTTP Request Node Configuration (inside the Loop)"
URL:    https://your-org.atlassian.net/rest/api/3/search
Method: GET
Query Parameters:
  jql:        project = "MYPROJ" ORDER BY created DESC
  startAt:    {{ $json.startAt ?? 0 }}
  maxResults: 50
  fields:     summary,status,assignee,priority,created
```text
> **Tip: HTTP Request Built-In Pagination**
>
> For simple offset-based or header-link pagination, the HTTP Request node has a built-in **Pagination** option under the settings. Set it to "Offset" with the parameter name and increment. This handles the loop automatically without a Loop node. Use the manual loop approach only when the built-in pagination cannot express your API's pagination scheme.

This pattern guarantees you fetch all records regardless of the total count, handles APIs that return thousands of results, and keeps memory usage manageable by processing page by page.

**Related:** [Use Path Parameters in Webhook URLs for Dynamic Routing](../webhook-mastery/01-use-path-parameters-in-webhook-urls-for-dynamic-routing.md) | [Use Edit Fields in "Map Each" Mode for Simple Renames](../data-transformation/01-use-edit-fields-in-map-each-mode-for-simple-renames.md)

Want this running in your stack?

I build production n8n and Cloudflare automation for teams — the same engineering behind HarperFlow. Fixed-price, escrow-protected, US-based.