At a Glance — n8n NetSuite Pagination

  • The loop: list call with limit and offset, then an IF node on hasMore, then offset plus page size, then back to the list call
  • Where the offset lives: workflow static data, written by small Code nodes before and after each page
  • Full export or delta: the same flow works with either of two list nodes. The delta version adds a q filter on lastModifiedDate and remembers the date of the last run
  • Why upsert: a date-based delta re-fetches records already synced earlier that day, so the Salesforce writes match on External_ID__c instead of creating new records
  • The catch: n8n only saves static data for a published workflow started by a trigger, not when you test by hand
  • Download: importable workflow JSON, with the credentials removed

The short answer: limit, offset, hasMore

NetSuite's REST record API returns lists one page at a time. For n8n NetSuite pagination, you call the list operation with a limit and an offset, look at the hasMore flag in the response, add the page size to the offset, and call again until hasMore is false. The n8n NetSuite community node's customer list operation accepts limit, offset and q (NetSuite's filter) as parameters, so the whole loop can be built from standard n8n nodes. If you haven't set the node up yet, start with our n8n NetSuite integration guide.

The only awkward part is remembering the offset between passes. n8n doesn't have a built-in "page counter" for custom loops, so this workflow keeps it in workflow static data, a small store the workflow can read and write from Code nodes.

How the NetSuite pagination loop works

The workflow is a daily NetSuite-to-Salesforce customer sync, but the pattern is the same for any record type. In order:

  1. Schedule trigger (Execute Workflow Daily) starts the run.
  2. Init Offset (Code) sets the offset to 0 and hasMore to true in static data.
  3. Has More Records? (IF) checks hasMore. True goes round the loop; false goes to the end.
  4. Retrieve Paging Offset and LastExportDate (Code) reads the offset and the last export date from static data.
  5. NetSuite list node fetches one page: limit 20, offset from the previous step.
  6. Split Customers Array turns the page's items into one item per customer.
  7. NS: Customer - Get record fetches each customer in full by ID, because the list call returns only IDs.
  8. Is Company? (IF) sends companies and individuals down different Salesforce paths.
  9. Salesforce upserts write the Account, and for individuals also the Contact.
  10. Update Paging Offset (Code) adds 20 to the offset, copies hasMore from the NetSuite response, and the flow returns to step 3.
  11. When hasMore is false, Update LastExportDate saves today's date and the run ends.

Each page therefore costs one list call plus up to 20 record calls. If you run into NetSuite request limits, lower the page size.

Keeping the NetSuite pagination offset in static data

Three small Code nodes manage the state. They use $getWorkflowStaticData('global'), which is shared by every node in the workflow.

Init Offset runs once at the start of each run:

// initialize staticData object
const workflowStaticData = $getWorkflowStaticData('global');

workflowStaticData.offset = 0
workflowStaticData.hasMore = true

return [
  {
      offset: workflowStaticData.offset,
      hasMore: workflowStaticData.hasMore
  }
];

Retrieve Paging Offset and LastExportDate feeds the list call. If no export date has been saved yet, it falls back to yesterday:

const workflowStaticData = $getWorkflowStaticData('global');
const lastExportDate = workflowStaticData.lastExportDate ? workflowStaticData.lastExportDate : $today.minus({days: 1}).toFormat('MM/dd/yyyy')

return [
  {
      offset: workflowStaticData.offset,
      lastExportDate: lastExportDate
  }
];

Update Paging Offset runs after each page. It moves the offset on and copies hasMore from whichever list node ran:

const workflowStaticData = $getWorkflowStaticData('global');

workflowStaticData.offset = workflowStaticData.offset+20
workflowStaticData.hasMore = $('NS: Customer - Get All records').isExecuted ? $('NS: Customer - Get All records').first().json.hasMore: $('NS: Customer - Get Delta records').first().json.hasMore

return [
  {
      offset: workflowStaticData.offset,
      hasMore: workflowStaticData.hasMore
  }
];
Static data only persists in a published workflow

n8n's documentation says it plainly: "Static data isn't available when testing workflows. The workflow must be published and called by a trigger or webhook to save static data." It also describes the feature as experimental and possibly unreliable under high-frequency executions. The saved export date only carries over from day to day once the workflow is published and running on its schedule. See n8n's static data documentation.

Full export or only what changed

The workflow contains two NetSuite list nodes, and you connect one of them between Retrieve Paging Offset and Split Customers Array:

The delta filter, as set in the node's q field:

lastModifiedDate ON_OR_AFTER "{{ $json.lastExportDate }}"

In the download, the delta node is the one that is connected. Two details to know:

Why the Salesforce writes are upserts

The saved date has no time in it, only a day. So a run that starts tomorrow asks for everything modified on or after the day of the last run, which includes records already synced earlier on that day. Handling a customer twice is therefore normal in this design, and the Salesforce writes have to cope with it.

That is what the upsert is for. Each Salesforce node uses the upsert operation with External_ID__c as the external ID field and the NetSuite customer ID as its value. The first time, Salesforce creates the record. After that, it updates it. Re-processing the same NetSuite customer never creates a second Account. (The same principle, make the write safe to repeat, is what stops duplicate actions in agent workflows; see race conditions in n8n agent workflows.)

NetSuite customers can be companies or individuals, and Salesforce models them differently, so the flow branches on isPerson:

What to change before you use it

This is a working pattern, not a finished product. Before pointing it at a real account:

Download the workflow

netsuite-customers-to-salesforce.json contains 18 nodes: the loop, both NetSuite list nodes (Delta connected, Get All not connected), the Salesforce branches, and sticky notes marking the NetSuite and Salesforce sections.

  1. In n8n, open the workflow menu and choose Import from file.
  2. Select the NetSuite and Salesforce credentials on each node. The file contains no credentials, only placeholders.
  3. Add the External_ID__c field in Salesforce.
  4. Connect the list node you want, set the schedule, and publish.

The NetSuite nodes use the n8n NetSuite community node (n8n-nodes-netsuite-rest), a verified community node that works on n8n Cloud and self-hosted n8n and authenticates with OAuth 2.0.

What was tested

This workflow was tested during development of the node. We have also checked the node settings in the file against the current release (0.1.15, whose code is unchanged from 0.1.14): the Customer resource, the get /customer/{id} operation, and the limit, offset and q parameters of the list operation all still exist. The downloadable file combines the delta and full-export variants of the same flow, with identifying details removed and the notes on the canvas tidied, so import it into a test instance first.

If you need a NetSuite-to-Salesforce sync that handles volume, errors and retries properly, Entech builds n8n and NetSuite integrations and can build and run it for you. See also our n8n Salesforce integration page.