Skip to main content

Pattern 2: Pull In

NetSuite is ACTIVE. Data flows IN from external systems.


How It Works

PULL IN PATTERN
─────────────────────────────────────────────────────────────────

NETSUITE EXTERNAL SYSTEM
┌─────────────────┐ ┌─────────────────┐
│ │ HTTP GET │ │
│ Fetch Data │ ◀───────────── │ API Endpoint │
│ (Scheduled) │ JSON/XML │ │
│ │ │ │
│ Create/Update │ │ │
│ Records │ │ │
└─────────────────┘ └─────────────────┘

│ Triggered by:
├── Scheduled Script (periodic polling)
├── Map/Reduce Script (bulk import)
└── Suitelet (on-demand via button)

When to Use

ScenarioFrequencyScriptExternal System
Import Shopify ordersEvery 15 minScheduledShopify API
Sync inventory from WMSEvery hourMap/ReduceWarehouse API
Fetch exchange ratesDailyScheduledCurrency API
Import leads from marketingEvery 30 minScheduledHubSpot
On-demand sync (button)User clicksSuiteletAny API

Scenario A: Periodic Order Import

Case: Every 15 minutes, check Shopify for new orders and create Sales Orders in NetSuite.

FLOW
─────────────────────────────────────────────────────────────────

Every 15 Minutes (Scheduled)


┌─────────────────┐ ┌─────────────────┐
│ Scheduled │ ───▶ │ Shopify API │
│ Script │ GET │ /orders.json │
└────────┬────────┘ └─────────────────┘

│ For each order:

┌─────────────────┐
│ Check if exists │
│ (by external ID)│
└────────┬────────┘

┌────┴────┐
│ │
No Yes
│ │
▼ ▼
┌────────┐ ┌────────┐
│ Create │ │ Skip │
│ Order │ │ │
└────────┘ └────────┘

Script: Scheduled Script

/**
* @NApiVersion 2.1
* @NScriptType ScheduledScript
* @description Import orders from Shopify every 15 minutes
*/
define(['N/https', 'N/record', 'N/search', 'N/log'], function(https, record, search, log) {

const SHOPIFY_STORE = 'your-store.myshopify.com';
const SHOPIFY_TOKEN = 'shpat_xxxxx';

function execute(context) {
// Get last sync timestamp (from script parameter or custom record)
var lastSync = getLastSyncTime();

// Fetch new orders from Shopify
var response = https.get({
url: 'https://' + SHOPIFY_STORE + '/admin/api/2024-01/orders.json',
headers: {
'X-Shopify-Access-Token': SHOPIFY_TOKEN
}
});

var orders = JSON.parse(response.body).orders;
log.audit('Shopify Orders', 'Found ' + orders.length + ' orders');

orders.forEach(function(shopifyOrder) {
// Skip if already imported
if (orderExists(shopifyOrder.id)) {
log.debug('Skip', 'Order ' + shopifyOrder.id + ' already exists');
return;
}

try {
createSalesOrder(shopifyOrder);
log.audit('Created', 'Order ' + shopifyOrder.order_number);
} catch (e) {
log.error('Failed', 'Order ' + shopifyOrder.id + ': ' + e.message);
}
});

// Update last sync time
updateLastSyncTime();
}

function createSalesOrder(shopifyOrder) {
var salesOrder = record.create({
type: record.Type.SALES_ORDER,
isDynamic: true
});

// Find or create customer
var customerId = findOrCreateCustomer(shopifyOrder.customer);
salesOrder.setValue('entity', customerId);
salesOrder.setValue('memo', 'Shopify Order: ' + shopifyOrder.order_number);
salesOrder.setValue('custbody_shopify_order_id', shopifyOrder.id);

// Add line items
shopifyOrder.line_items.forEach(function(lineItem) {
var itemId = findItemBySku(lineItem.sku);
if (!itemId) {
log.error('Item Not Found', 'SKU: ' + lineItem.sku);
return;
}

salesOrder.selectNewLine({ sublistId: 'item' });
salesOrder.setCurrentSublistValue({ sublistId: 'item', fieldId: 'item', value: itemId });
salesOrder.setCurrentSublistValue({ sublistId: 'item', fieldId: 'quantity', value: lineItem.quantity });
salesOrder.setCurrentSublistValue({ sublistId: 'item', fieldId: 'rate', value: lineItem.price });
salesOrder.commitLine({ sublistId: 'item' });
});

return salesOrder.save();
}

function orderExists(shopifyOrderId) {
var results = search.create({
type: search.Type.SALES_ORDER,
filters: [['custbody_shopify_order_id', 'is', shopifyOrderId]]
}).run().getRange({ start: 0, end: 1 });
return results.length > 0;
}

function findItemBySku(sku) {
var results = search.create({
type: search.Type.ITEM,
filters: [['itemid', 'is', sku]],
columns: ['internalid']
}).run().getRange({ start: 0, end: 1 });
return results.length > 0 ? results[0].getValue('internalid') : null;
}

function findOrCreateCustomer(shopifyCustomer) {
// Search by email
var results = search.create({
type: search.Type.CUSTOMER,
filters: [['email', 'is', shopifyCustomer.email]]
}).run().getRange({ start: 0, end: 1 });

if (results.length > 0) {
return results[0].id;
}

// Create new customer
var customer = record.create({ type: record.Type.CUSTOMER });
customer.setValue('companyname', shopifyCustomer.first_name + ' ' + shopifyCustomer.last_name);
customer.setValue('email', shopifyCustomer.email);
return customer.save();
}

function getLastSyncTime() {
// Implementation: Read from script parameter or custom record
}

function updateLastSyncTime() {
// Implementation: Save to script parameter or custom record
}

return { execute: execute };
});

Deployment:

  • Schedule: Every 15 minutes
  • Status: Scheduled

Scenario B: On-Demand Sync Button

Case: User clicks "Sync Now" button on a form to trigger immediate data fetch.

FLOW
─────────────────────────────────────────────────────────────────

User clicks "Sync Now" button


┌─────────────────┐ ┌─────────────────┐ ┌─────────────┐
│ Client Script │ ───▶ │ Suitelet │ ───▶ │ External │
│ (Button Click) │ AJAX │ (Backend) │ GET │ API │
└─────────────────┘ └─────────────────┘ └─────────────┘

Suitelet (Backend):

/**
* @NApiVersion 2.1
* @NScriptType Suitelet
* @description On-demand sync triggered by button
*/
define(['N/https', 'N/record', 'N/log'], function(https, record, log) {

function onRequest(context) {
if (context.request.method !== 'POST') {
context.response.write(JSON.stringify({ error: 'POST required' }));
return;
}

var params = JSON.parse(context.request.body);
var recordId = params.recordId;

try {
// Fetch data from external system
var response = https.get({
url: 'https://api.external-system.com/data/' + recordId,
headers: { 'Authorization': 'Bearer TOKEN' }
});

var externalData = JSON.parse(response.body);

// Update NetSuite record
record.submitFields({
type: record.Type.CUSTOMER,
id: recordId,
values: {
'custentity_external_status': externalData.status,
'custentity_last_sync': new Date()
}
});

context.response.write(JSON.stringify({
success: true,
message: 'Synced successfully'
}));

} catch (e) {
context.response.write(JSON.stringify({
success: false,
error: e.message
}));
}
}

return { onRequest: onRequest };
});

Client Script (Button):

/**
* @NApiVersion 2.1
* @NScriptType ClientScript
*/
define(['N/https', 'N/currentRecord', 'N/ui/dialog'], function(https, currentRecord, dialog) {

function syncNow() {
var rec = currentRecord.get();

https.post.promise({
url: '/app/site/hosting/scriptlet.nl?script=123&deploy=1',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ recordId: rec.id })
}).then(function(response) {
var result = JSON.parse(response.body);
dialog.alert({ title: 'Sync Result', message: result.message });
}).catch(function(e) {
dialog.alert({ title: 'Error', message: e.message });
});
}

return {
pageInit: function() {},
syncNow: syncNow
};
});

Scenario C: Bulk Import with Map/Reduce

Case: Import thousands of products from external PIM system.

/**
* @NApiVersion 2.1
* @NScriptType MapReduceScript
* @description Bulk import products from PIM
*/
define(['N/https', 'N/record', 'N/search', 'N/log'], function(https, search, record, log) {

function getInputData() {
// Fetch product list from external system
var response = https.get({
url: 'https://api.pim-system.com/products',
headers: { 'Authorization': 'Bearer TOKEN' }
});

return JSON.parse(response.body).products;
}

function map(context) {
var product = JSON.parse(context.value);

// Check if product exists
var existingId = findProductBySku(product.sku);

context.write({
key: product.sku,
value: {
product: product,
existingId: existingId
}
});
}

function reduce(context) {
var data = JSON.parse(context.values[0]);
var product = data.product;
var existingId = data.existingId;

try {
if (existingId) {
// Update existing
record.submitFields({
type: record.Type.INVENTORY_ITEM,
id: existingId,
values: {
'displayname': product.name,
'salesprice': product.price,
'custitem_pim_last_sync': new Date()
}
});
log.debug('Updated', product.sku);
} else {
// Create new
var item = record.create({ type: record.Type.INVENTORY_ITEM });
item.setValue('itemid', product.sku);
item.setValue('displayname', product.name);
item.setValue('salesprice', product.price);
item.save();
log.debug('Created', product.sku);
}
} catch (e) {
log.error('Failed', product.sku + ': ' + e.message);
}
}

function findProductBySku(sku) {
var results = search.create({
type: search.Type.INVENTORY_ITEM,
filters: [['itemid', 'is', sku]]
}).run().getRange({ start: 0, end: 1 });
return results.length > 0 ? results[0].id : null;
}

function summarize(summary) {
log.audit('Import Complete', 'Processed: ' + summary.mapSummary.keys.length);
}

return {
getInputData: getInputData,
map: map,
reduce: reduce,
summarize: summarize
};
});

Scenario D: CDC (Change Data Capture) - Incremental Sync

Case: Only fetch records that have changed since the last sync to avoid re-processing and improve performance.

CDC PATTERN
─────────────────────────────────────────────────────────────────

First Run (Initial Load):


┌─────────────────┐ ┌─────────────────┐
│ Scheduled │ ───▶ │ External API │
│ Script │ GET │ ?since=null │
└────────┬────────┘ └─────────────────┘

│ Process all records

┌─────────────────┐
│ Store timestamp │ ← lastSyncTime = 2024-01-15T10:30:00Z
│ (Custom Record) │
└─────────────────┘

Subsequent Runs:


┌─────────────────┐
│ Read last │ ← lastSyncTime = 2024-01-15T10:30:00Z
│ sync timestamp │
└────────┬────────┘


┌─────────────────┐ ┌─────────────────┐
│ Scheduled │ ───▶ │ External API │
│ Script │ GET │ ?since=2024-01-15T10:30:00Z
└────────┬────────┘ │ &until=now │
│ └─────────────────┘

│ Only new/changed records

┌─────────────────┐
│ Process delta │
│ Update lastSync │ ← lastSyncTime = 2024-01-15T10:45:00Z
└─────────────────┘

Sync State Custom Record Setup

Create custom record customrecord_integration_sync_state:

Field IDTypePurpose
custrecord_sync_integration_idTextUnique identifier (e.g., "SHOPIFY_ORDERS")
custrecord_sync_last_timestampDate/TimeLast successful sync timestamp
custrecord_sync_last_record_idTextLast processed record ID (for cursor-based)
custrecord_sync_statusListIDLE, RUNNING, FAILED
custrecord_sync_records_processedIntegerCount from last run
custrecord_sync_last_errorTextLast error message

CDC Scheduled Script

/**
* @NApiVersion 2.1
* @NScriptType ScheduledScript
* @description CDC Import - Only fetch changed records since last sync
*/
define(['N/https', 'N/record', 'N/search', 'N/log', 'N/format'], function(https, record, search, log, format) {

const INTEGRATION_ID = 'SHOPIFY_ORDERS';
const SHOPIFY_STORE = 'your-store.myshopify.com';
const SHOPIFY_TOKEN = 'shpat_xxxxx';

function execute(context) {
var syncState = getSyncState();

// Mark as running
updateSyncStatus(syncState.id, 'RUNNING');

try {
var lastSyncTime = syncState.lastTimestamp;
var currentSyncTime = new Date();

// Build API URL with date filter
var apiUrl = buildApiUrl(lastSyncTime, currentSyncTime);

log.audit('CDC Sync Start', {
lastSync: lastSyncTime ? lastSyncTime.toISOString() : 'INITIAL',
currentSync: currentSyncTime.toISOString()
});

// Fetch only changed records
var response = https.get({
url: apiUrl,
headers: { 'X-Shopify-Access-Token': SHOPIFY_TOKEN }
});

var orders = JSON.parse(response.body).orders;
log.audit('Records Found', orders.length + ' orders changed since last sync');

var processedCount = 0;
var skippedCount = 0;

orders.forEach(function(order) {
var result = processOrder(order);
if (result.processed) {
processedCount++;
} else {
skippedCount++;
}
});

// Update sync state with new timestamp
updateSyncState(syncState.id, {
lastTimestamp: currentSyncTime,
status: 'IDLE',
recordsProcessed: processedCount,
lastError: ''
});

log.audit('CDC Sync Complete', {
processed: processedCount,
skipped: skippedCount,
nextSyncFrom: currentSyncTime.toISOString()
});

} catch (e) {
log.error('CDC Sync Failed', e.message);

updateSyncState(syncState.id, {
status: 'FAILED',
lastError: e.message
});
}
}

function buildApiUrl(lastSyncTime, currentSyncTime) {
var baseUrl = 'https://' + SHOPIFY_STORE + '/admin/api/2024-01/orders.json';
var params = [];

if (lastSyncTime) {
// Fetch orders updated since last sync
// Add small buffer (1 minute) to handle clock differences
var bufferTime = new Date(lastSyncTime.getTime() - 60000);
params.push('updated_at_min=' + encodeURIComponent(bufferTime.toISOString()));
}

// Always set upper bound to current time
params.push('updated_at_max=' + encodeURIComponent(currentSyncTime.toISOString()));

// Sort by updated_at to process in order
params.push('order=updated_at asc');

// Limit per request
params.push('limit=250');

return baseUrl + '?' + params.join('&');
}

function processOrder(shopifyOrder) {
var externalId = String(shopifyOrder.id);

// Check if order exists in NetSuite
var existingOrder = findExistingOrder(externalId);

if (existingOrder) {
// Order exists - check if we need to update
var nsLastModified = existingOrder.lastModified;
var shopifyUpdated = new Date(shopifyOrder.updated_at);

if (shopifyUpdated <= nsLastModified) {
// NetSuite is up to date, skip
return { processed: false, reason: 'UP_TO_DATE' };
}

// Update existing order
updateExistingOrder(existingOrder.id, shopifyOrder);
log.debug('Updated', 'Order ' + shopifyOrder.order_number);
return { processed: true, action: 'UPDATE' };

} else {
// New order - create it
createNewOrder(shopifyOrder);
log.debug('Created', 'Order ' + shopifyOrder.order_number);
return { processed: true, action: 'CREATE' };
}
}

function findExistingOrder(externalId) {
var results = search.create({
type: search.Type.SALES_ORDER,
filters: [['custbody_shopify_order_id', 'is', externalId]],
columns: ['internalid', 'lastmodifieddate']
}).run().getRange({ start: 0, end: 1 });

if (results.length > 0) {
return {
id: results[0].id,
lastModified: new Date(results[0].getValue('lastmodifieddate'))
};
}
return null;
}

function createNewOrder(shopifyOrder) {
var salesOrder = record.create({
type: record.Type.SALES_ORDER,
isDynamic: true
});

var customerId = findOrCreateCustomer(shopifyOrder.customer);
salesOrder.setValue('entity', customerId);
salesOrder.setValue('memo', 'Shopify: ' + shopifyOrder.order_number);
salesOrder.setValue('custbody_shopify_order_id', shopifyOrder.id);
salesOrder.setValue('custbody_shopify_updated_at', new Date(shopifyOrder.updated_at));

// Add line items
shopifyOrder.line_items.forEach(function(lineItem) {
var itemId = findItemBySku(lineItem.sku);
if (itemId) {
salesOrder.selectNewLine({ sublistId: 'item' });
salesOrder.setCurrentSublistValue({ sublistId: 'item', fieldId: 'item', value: itemId });
salesOrder.setCurrentSublistValue({ sublistId: 'item', fieldId: 'quantity', value: lineItem.quantity });
salesOrder.setCurrentSublistValue({ sublistId: 'item', fieldId: 'rate', value: lineItem.price });
salesOrder.commitLine({ sublistId: 'item' });
}
});

return salesOrder.save();
}

function updateExistingOrder(orderId, shopifyOrder) {
// Update relevant fields that might have changed
record.submitFields({
type: record.Type.SALES_ORDER,
id: orderId,
values: {
'custbody_shopify_status': shopifyOrder.financial_status,
'custbody_shopify_fulfillment': shopifyOrder.fulfillment_status,
'custbody_shopify_updated_at': new Date(shopifyOrder.updated_at)
}
});
}

// ─────────────────────────────────────────────────────────────────
// SYNC STATE MANAGEMENT
// ─────────────────────────────────────────────────────────────────

function getSyncState() {
var results = search.create({
type: 'customrecord_integration_sync_state',
filters: [['custrecord_sync_integration_id', 'is', INTEGRATION_ID]],
columns: [
'internalid',
'custrecord_sync_last_timestamp',
'custrecord_sync_last_record_id',
'custrecord_sync_status'
]
}).run().getRange({ start: 0, end: 1 });

if (results.length > 0) {
var lastTimestamp = results[0].getValue('custrecord_sync_last_timestamp');
return {
id: results[0].id,
lastTimestamp: lastTimestamp ? new Date(lastTimestamp) : null,
lastRecordId: results[0].getValue('custrecord_sync_last_record_id'),
status: results[0].getValue('custrecord_sync_status')
};
}

// Create initial sync state record
var newState = record.create({ type: 'customrecord_integration_sync_state' });
newState.setValue('custrecord_sync_integration_id', INTEGRATION_ID);
newState.setValue('custrecord_sync_status', 'IDLE');
var newId = newState.save();

return {
id: newId,
lastTimestamp: null,
lastRecordId: null,
status: 'IDLE'
};
}

function updateSyncState(stateId, values) {
var updateValues = {};

if (values.lastTimestamp !== undefined) {
updateValues['custrecord_sync_last_timestamp'] = values.lastTimestamp;
}
if (values.lastRecordId !== undefined) {
updateValues['custrecord_sync_last_record_id'] = values.lastRecordId;
}
if (values.status !== undefined) {
updateValues['custrecord_sync_status'] = values.status;
}
if (values.recordsProcessed !== undefined) {
updateValues['custrecord_sync_records_processed'] = values.recordsProcessed;
}
if (values.lastError !== undefined) {
updateValues['custrecord_sync_last_error'] = values.lastError;
}

record.submitFields({
type: 'customrecord_integration_sync_state',
id: stateId,
values: updateValues
});
}

function updateSyncStatus(stateId, status) {
updateSyncState(stateId, { status: status });
}

// Helper functions
function findOrCreateCustomer(shopifyCustomer) {
if (!shopifyCustomer) return getDefaultCustomer();

var results = search.create({
type: search.Type.CUSTOMER,
filters: [['email', 'is', shopifyCustomer.email]]
}).run().getRange({ start: 0, end: 1 });

if (results.length > 0) return results[0].id;

var customer = record.create({ type: record.Type.CUSTOMER });
customer.setValue('companyname', shopifyCustomer.first_name + ' ' + shopifyCustomer.last_name);
customer.setValue('email', shopifyCustomer.email);
return customer.save();
}

function findItemBySku(sku) {
var results = search.create({
type: search.Type.ITEM,
filters: [['itemid', 'is', sku]],
columns: ['internalid']
}).run().getRange({ start: 0, end: 1 });
return results.length > 0 ? results[0].getValue('internalid') : null;
}

function getDefaultCustomer() {
// Return a default customer ID for guest orders
return 123; // Replace with your default customer
}

return { execute: execute };
});

CDC with Cursor-Based Pagination

For APIs that use cursor/offset pagination instead of timestamps:

/**
* @NApiVersion 2.1
* @NScriptType ScheduledScript
* @description CDC with cursor-based pagination
*/
define(['N/https', 'N/record', 'N/search', 'N/log'], function(https, record, search, log) {

const INTEGRATION_ID = 'CRM_CONTACTS';

function execute(context) {
var syncState = getSyncState();
var lastCursor = syncState.lastRecordId;
var pageSize = 100;
var totalProcessed = 0;
var hasMore = true;

while (hasMore) {
// Build URL with cursor
var url = 'https://api.crm.com/contacts?limit=' + pageSize;
if (lastCursor) {
url += '&after=' + lastCursor;
}

var response = https.get({
url: url,
headers: { 'Authorization': 'Bearer TOKEN' }
});

var data = JSON.parse(response.body);
var contacts = data.results;

if (contacts.length === 0) {
hasMore = false;
break;
}

// Process each contact
contacts.forEach(function(contact) {
processContact(contact);
totalProcessed++;
});

// Update cursor to last processed ID
lastCursor = contacts[contacts.length - 1].id;

// Save progress after each page (in case of failure)
updateSyncState(syncState.id, {
lastRecordId: lastCursor,
recordsProcessed: totalProcessed
});

// Check if more pages exist
hasMore = data.hasMore || contacts.length === pageSize;

// Governance check
var remainingUsage = runtime.getCurrentScript().getRemainingUsage();
if (remainingUsage < 500) {
log.audit('Governance', 'Stopping - will resume from cursor: ' + lastCursor);
break;
}
}

log.audit('CDC Complete', 'Processed: ' + totalProcessed);
}

function processContact(contact) {
// Check if exists by external ID
var existingId = findByExternalId('customrecord_crm_contact', 'custrecord_crm_id', contact.id);

if (existingId) {
// Update
record.submitFields({
type: 'customrecord_crm_contact',
id: existingId,
values: {
'custrecord_crm_name': contact.name,
'custrecord_crm_email': contact.email,
'custrecord_crm_last_sync': new Date()
}
});
} else {
// Create
var rec = record.create({ type: 'customrecord_crm_contact' });
rec.setValue('custrecord_crm_id', contact.id);
rec.setValue('custrecord_crm_name', contact.name);
rec.setValue('custrecord_crm_email', contact.email);
rec.save();
}
}

function findByExternalId(recordType, fieldId, externalId) {
var results = search.create({
type: recordType,
filters: [[fieldId, 'is', externalId]]
}).run().getRange({ start: 0, end: 1 });
return results.length > 0 ? results[0].id : null;
}

// getSyncState and updateSyncState same as above...

return { execute: execute };
});

CDC Comparison Strategies

StrategyAPI RequirementBest For
Timestamp-basedupdated_at_min parameterMost REST APIs
Cursor-basedafter or offset parameterLarge datasets
Delta tokenAPI returns change tokenMicrosoft Graph, etc
Polling with hashNone (compare locally)APIs without filters

Handling Clock Drift

Always add a small buffer when using timestamps:

function getLastSyncWithBuffer(lastSync, bufferMinutes) {
if (!lastSync) return null;

// Subtract buffer to handle clock differences
var buffered = new Date(lastSync.getTime() - (bufferMinutes * 60 * 1000));
return buffered;
}

// Usage: fetch 5 minutes before last sync to be safe
var since = getLastSyncWithBuffer(lastSyncTime, 5);

Monitoring CDC Health

Create a saved search to monitor sync states:

Type: Custom Record - Integration Sync State
Columns: Integration ID, Last Timestamp, Status, Records Processed, Last Error
Filters: Status = FAILED OR Last Timestamp older than 1 day

Handling Pagination

For APIs that return paginated results:

function fetchAllPages() {
var allResults = [];
var page = 1;
var hasMore = true;

while (hasMore) {
var response = https.get({
url: 'https://api.example.com/items?page=' + page + '&limit=100',
headers: { 'Authorization': 'Bearer TOKEN' }
});

var data = JSON.parse(response.body);
allResults = allResults.concat(data.items);

hasMore = data.hasNextPage;
page++;

// Governance check
if (page > 50) break; // Safety limit
}

return allResults;
}

Script Selection Guide

NeedUse This Script
Periodic polling (every X minutes)Scheduled Script
Import thousands of recordsMap/Reduce Script
User-triggered syncClient Script → Suitelet
One-time migrationMap/Reduce Script
Incremental sync (only changes)Scheduled Script with CDC
Avoid duplicate processingCDC with timestamp/cursor tracking

  • Push Out - Send data to external systems
  • Receive - Accept webhooks from external systems