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:
- Schedule trigger (Execute Workflow Daily) starts the run.
- Init Offset (Code) sets the offset to 0 and
hasMoreto true in static data. - Has More Records? (IF) checks
hasMore. True goes round the loop; false goes to the end. - Retrieve Paging Offset and LastExportDate (Code) reads the offset and the last export date from static data.
- NetSuite list node fetches one page: limit 20, offset from the previous step.
- Split Customers Array turns the page's
itemsinto one item per customer. - NS: Customer - Get record fetches each customer in full by ID, because the list call returns only IDs.
- Is Company? (IF) sends companies and individuals down different Salesforce paths.
- Salesforce upserts write the Account, and for individuals also the Contact.
- Update Paging Offset (Code) adds 20 to the offset, copies
hasMorefrom the NetSuite response, and the flow returns to step 3. - When
hasMoreis 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
}
];
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:
- NS: Customer - Get All records uses only the limit and offset. It exports every customer, every run.
- NS: Customer - Get Delta records adds a
qfilter so NetSuite returns only customers modified since the last run.
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:
- The first run is not a full load. With no saved date, the flow falls back to yesterday. For an initial load of everything, connect Get All for one run, then switch to Delta. The end of the run saves today's date either way.
- The date format is
MM/dd/yyyy. If the delta call returns nothing when you expect results, check which date format your NetSuite account expects.
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:
- Company: one Account, named from
companyName. - Individual: an Account named from first and last name, then a Contact (last name, first name, email) linked to it.
What to change before you use it
This is a working pattern, not a finished product. Before pointing it at a real account:
- Create the external ID field. Salesforce needs a custom field called
External_ID__con both Account and Contact, set as an external ID. (n8n's Salesforce node names the Contact's account fieldacconuntId; the export keeps that spelling.) - Check the Contact's external ID. As exported, the Contact upsert uses the output of the Account upsert, which is the Salesforce Account ID, as its external ID. That keeps reruns from creating duplicates, but it means the Contact's
External_ID__cholds a Salesforce ID, not the NetSuite customer ID. If you want the NetSuite ID there, reference it from the Is Company? node instead, for example{{ $('Is Company?').item.json.id }}. We haven't tested that change. - Keep the page size in step. The number 20 appears twice: as the list node's
limitand as the+20in Update Paging Offset. If you change one, change the other, or pages will be skipped or repeated. - Set the schedule. The trigger node is named Execute Workflow Daily. Check its interval and set the time that suits your NetSuite load.
- Publish it. As covered above, the saved date and offset only persist once the workflow is published.
- Add error handling. This version has none. See our n8n error handling guide before running it unattended.
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.
- In n8n, open the workflow menu and choose Import from file.
- Select the NetSuite and Salesforce credentials on each node. The file contains no credentials, only placeholders.
- Add the
External_ID__cfield in Salesforce. - 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.