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.

Most APIs return paginated results with a cursor or offset for the next page. For cursor-based or non-standard schemes that the HTTP Request node's built-in pagination cannot express, use a Loop node with explicit cursor tracking: store results and the offset in workflow static data, and branch to a loop or done output based on the total count.

When do you need manual pagination?

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.

How do you track a cursor with a Loop node?

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]
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 }))
  ];
}
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

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.

When can you use built-in pagination instead?

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.

Related: Use Path Parameters in Webhook URLs for Dynamic Routing · Use Edit Fields in "Map Each" Mode for Simple Renames

Showcase builds

19 complete workflows from my own projects, each with its n8n workflow JSON to import. Showcase entries link the file at the end of the article.

See the showcase builds

Keep reading

190 entries grouped by topic, from first workflow to queue mode. Free, no signup.

Browse the encyclopedia

Need it built?

I design, build and run n8n systems for clients. Every engagement starts with a $1,500 diagnostic audit, credited toward the build.

Book an introductory call