Skip to main content

Pattern 1: Push Out

NetSuite is ACTIVE. Data flows OUT to external systems.


How It Works

PUSH OUT PATTERN
─────────────────────────────────────────────────────────────────

NETSUITE EXTERNAL SYSTEM
┌─────────────────┐ ┌─────────────────┐
│ │ HTTP POST │ │
│ Record Saved │ ─────────────▶ │ Receive Data │
│ (Order, etc) │ JSON/XML │ (Webhook) │
│ │ │ │
└─────────────────┘ └─────────────────┘

│ Triggered by:
├── User Event Script (afterSubmit)
├── Workflow Action Script
├── Scheduled Script
└── Map/Reduce Script

When to Use

ScenarioTriggerScriptExternal System
Order to fulfillment centerOrder createdUser EventShipStation, ShipBob
Customer to CRMCustomer savedUser EventSalesforce, HubSpot
Invoice to accountingInvoice approvedWorkflow ActionQuickBooks, Xero
Daily sales to BINightly scheduleScheduledSnowflake, BigQuery
Inventory to e-commerceEvery 15 minScheduledShopify, Magento
Bulk orders to 3PLEvery hourMap/Reduce3PL API

Scenario A: Real-Time Sync on Record Save

Case: When a Sales Order is created, immediately send it to the fulfillment system.

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

User creates Sales Order


┌─────────────────┐
│ beforeSubmit │ ← Validate, modify data
└────────┬────────┘


┌─────────────────┐
│ Record Saved │ ← Committed to database
│ to Database │
└────────┬────────┘


┌─────────────────┐ ┌─────────────────┐
│ afterSubmit │ ───▶ │ External API │
│ (User Event) │ POST │ (Fulfillment) │
└─────────────────┘ └─────────────────┘

Script: User Event Script

/**
* @NApiVersion 2.1
* @NScriptType UserEventScript
* @description Sync Sales Order to fulfillment system on create
*/
define(['N/https', 'N/record', 'N/log', 'N/runtime'], function(https, record, log, runtime) {

const FULFILLMENT_API = 'https://api.fulfillment-system.com/orders';
const API_KEY = 'your-api-key'; // Store in script parameter or custom record

function afterSubmit(context) {
// Only run on create
if (context.type !== context.UserEventType.CREATE) {
return;
}

var salesOrder = context.newRecord;

// Build payload
var payload = {
externalOrderId: salesOrder.getValue('tranid'),
netsuiteId: salesOrder.id,
customer: {
name: salesOrder.getText('entity'),
email: salesOrder.getValue('email')
},
shipTo: {
address: salesOrder.getValue('shipaddress')
},
items: getLineItems(salesOrder),
total: salesOrder.getValue('total'),
createdAt: new Date().toISOString()
};

try {
var response = https.post({
url: FULFILLMENT_API,
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + API_KEY
},
body: JSON.stringify(payload)
});

if (response.code === 200 || response.code === 201) {
log.audit('Sync Success', 'Order ' + salesOrder.id + ' synced');

// Optional: Store external ID on the record
record.submitFields({
type: record.Type.SALES_ORDER,
id: salesOrder.id,
values: { 'custbody_external_order_id': JSON.parse(response.body).id }
});
} else {
throw new Error('API returned ' + response.code);
}

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

// Queue for retry - create a custom record
createRetryRecord(salesOrder.id, 'SALES_ORDER', e.message);
}
}

function getLineItems(salesOrder) {
var items = [];
var lineCount = salesOrder.getLineCount({ sublistId: 'item' });

for (var i = 0; i < lineCount; i++) {
items.push({
sku: salesOrder.getSublistValue({ sublistId: 'item', fieldId: 'item', line: i }),
quantity: salesOrder.getSublistValue({ sublistId: 'item', fieldId: 'quantity', line: i }),
rate: salesOrder.getSublistValue({ sublistId: 'item', fieldId: 'rate', line: i })
});
}
return items;
}

function createRetryRecord(recordId, recordType, errorMsg) {
var retry = record.create({ type: 'customrecord_integration_queue' });
retry.setValue('custrecord_queue_record_id', recordId);
retry.setValue('custrecord_queue_record_type', recordType);
retry.setValue('custrecord_queue_status', 'PENDING');
retry.setValue('custrecord_queue_error', errorMsg);
retry.setValue('custrecord_queue_attempts', 0);
retry.save();
}

return { afterSubmit: afterSubmit };
});

Deployment:

  • Deploy to: Sales Order
  • Execute As: Role with API permissions
  • Event Type: Create (or Create/Edit if needed)

Scenario B: Conditional Sync via Workflow

Case: Only sync invoices to payment gateway when status changes to "Approved".

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

Invoice status changes to "Approved"


┌─────────────────┐
│ Workflow │
│ Condition Met │
└────────┬────────┘


┌─────────────────┐ ┌─────────────────┐
│ Workflow Action │ ───▶ │ Payment Gateway │
│ Script │ POST │ (Stripe, etc) │
└─────────────────┘ └─────────────────┘

Script: Workflow Action Script

/**
* @NApiVersion 2.1
* @NScriptType WorkflowActionScript
* @description Send approved invoice to payment gateway
*/
define(['N/https', 'N/runtime', 'N/log'], function(https, runtime, log) {

function onAction(context) {
var invoice = context.newRecord;

var payload = {
invoiceId: invoice.getValue('tranid'),
amount: invoice.getValue('total'),
currency: invoice.getValue('currency'),
customerId: invoice.getValue('entity'),
dueDate: invoice.getValue('duedate')
};

try {
var response = https.post({
url: 'https://api.stripe.com/v1/invoices',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer sk_live_xxxxx'
},
body: JSON.stringify(payload)
});

log.audit('Payment Gateway Sync', 'Invoice ' + invoice.id + ' sent');

// Return value to workflow (optional)
return response.code === 200 ? 'SUCCESS' : 'FAILED';

} catch (e) {
log.error('Payment Gateway Error', e.message);
return 'ERROR';
}
}

return { onAction: onAction };
});

Setup:

  1. Create Workflow on Invoice record
  2. Add State with condition: Status = Approved
  3. Add Action → Custom Action → Select your script
  4. Optionally store return value in workflow field

Scenario C: Batch Export via Scheduled Script

Case: Every night at midnight, export all invoices created today to the data warehouse.

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

Midnight (Scheduled)


┌─────────────────┐
│ Saved Search: │
│ Today's Invoices│
└────────┬────────┘


┌─────────────────┐ ┌─────────────────┐
│ Scheduled │ ───▶ │ Data Warehouse │
│ Script │ POST │ (Snowflake) │
└─────────────────┘ bulk └─────────────────┘

Script: Scheduled Script

/**
* @NApiVersion 2.1
* @NScriptType ScheduledScript
* @description Nightly export of invoices to data warehouse
*/
define(['N/https', 'N/search', 'N/log', 'N/format'], function(https, search, log, format) {

function execute(context) {
// Get today's date range
var today = new Date();
var startOfDay = new Date(today.getFullYear(), today.getMonth(), today.getDate());

// Search for today's invoices
var invoiceSearch = search.create({
type: search.Type.INVOICE,
filters: [
['mainline', 'is', 'T'],
'AND',
['trandate', 'within', 'today']
],
columns: [
'tranid', 'entity', 'total', 'trandate',
'status', 'subsidiary', 'currency'
]
});

var invoices = [];

invoiceSearch.run().each(function(result) {
invoices.push({
invoiceNumber: result.getValue('tranid'),
customerId: result.getValue('entity'),
customerName: result.getText('entity'),
amount: result.getValue('total'),
date: result.getValue('trandate'),
status: result.getText('status'),
subsidiary: result.getText('subsidiary'),
currency: result.getText('currency')
});
return true; // continue iteration
});

if (invoices.length === 0) {
log.audit('Export', 'No invoices to export');
return;
}

// Send to data warehouse
try {
var response = https.post({
url: 'https://your-warehouse.snowflakecomputing.com/api/ingest',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer YOUR_TOKEN'
},
body: JSON.stringify({
table: 'netsuite_invoices',
records: invoices,
exportDate: today.toISOString()
})
});

log.audit('Export Success', invoices.length + ' invoices exported');

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

return { execute: execute };
});

Deployment:

  • Schedule: Daily at 12:00 AM
  • Status: Scheduled

Scenario D: High-Volume Batch with Map/Reduce

Case: Sync thousands of orders to 3PL system every hour with parallel processing.

Script: Map/Reduce Script

/**
* @NApiVersion 2.1
* @NScriptType MapReduceScript
* @description Bulk sync orders to 3PL with parallel processing
*/
define(['N/https', 'N/search', 'N/record', 'N/log'], function(https, search, record, log) {

function getInputData() {
// Find orders pending sync
return search.create({
type: search.Type.SALES_ORDER,
filters: [
['mainline', 'is', 'T'],
'AND',
['custbody_3pl_sync_status', 'is', 'PENDING'],
'AND',
['status', 'anyof', 'SalesOrd:B'] // Pending Fulfillment
],
columns: ['tranid', 'entity', 'total', 'shipaddress']
});
}

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

// Load full record for line items
var salesOrder = record.load({
type: record.Type.SALES_ORDER,
id: searchResult.id
});

var payload = {
orderId: salesOrder.getValue('tranid'),
netsuiteId: salesOrder.id,
items: []
};

var lineCount = salesOrder.getLineCount({ sublistId: 'item' });
for (var i = 0; i < lineCount; i++) {
payload.items.push({
sku: salesOrder.getSublistText({ sublistId: 'item', fieldId: 'item', line: i }),
qty: salesOrder.getSublistValue({ sublistId: 'item', fieldId: 'quantity', line: i })
});
}

// Write to reduce stage with order ID as key
context.write({
key: salesOrder.id,
value: payload
});
}

function reduce(context) {
var orderId = context.key;
var payload = JSON.parse(context.values[0]);

try {
var response = https.post({
url: 'https://api.3pl-provider.com/orders',
headers: {
'Content-Type': 'application/json',
'X-API-Key': 'your-3pl-api-key'
},
body: JSON.stringify(payload)
});

if (response.code === 200 || response.code === 201) {
// Update sync status
record.submitFields({
type: record.Type.SALES_ORDER,
id: orderId,
values: {
'custbody_3pl_sync_status': 'SYNCED',
'custbody_3pl_sync_date': new Date()
}
});
context.write({ key: 'success', value: orderId });
} else {
throw new Error('API error: ' + response.code);
}

} catch (e) {
record.submitFields({
type: record.Type.SALES_ORDER,
id: orderId,
values: {
'custbody_3pl_sync_status': 'FAILED',
'custbody_3pl_sync_error': e.message
}
});
context.write({ key: 'failed', value: orderId });
}
}

function summarize(summary) {
var successCount = 0;
var failCount = 0;

summary.output.iterator().each(function(key, value) {
if (key === 'success') successCount++;
if (key === 'failed') failCount++;
return true;
});

log.audit('Sync Complete', 'Success: ' + successCount + ', Failed: ' + failCount);
}

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

Scenario E: Retry Queue for Failed Syncs

Case: External system is sometimes unavailable. Queue failed requests and retry with exponential backoff.

RETRY QUEUE ARCHITECTURE
─────────────────────────────────────────────────────────────────

User Event (afterSubmit)

│ Try sync

┌─────────────────┐
│ External API │
│ Call │
└────────┬────────┘

┌────┴────┐
│ │
Success Failed
│ │
▼ ▼
┌────────┐ ┌─────────────────┐
│ Update │ │ Create Queue │
│ Record │ │ Record │
│ Status │ │ (PENDING) │
└────────┘ └────────┬────────┘

│ Every 15 minutes

┌─────────────────┐
│ Map/Reduce │
│ Retry Processor │
└────────┬────────┘

┌────┴────┐
│ │
Success Failed
│ │
▼ ▼
┌────────┐ ┌─────────────────┐
│ Mark │ │ attempts < 5? │
│ SUCCESS│ └────────┬────────┘
└────────┘ │
┌────┴────┐
│ │
Yes No
│ │
▼ ▼
┌────────┐ ┌────────┐
│ Retry │ │ Mark │
│ Later │ │ FAILED │
│(backoff)│ │(alert) │
└────────┘ └────────┘

Queue Custom Record Setup

Create custom record customrecord_integration_queue:

Field IDTypePurpose
custrecord_queue_record_idIntegerNetSuite record internal ID
custrecord_queue_record_typeTextRecord type (SALES_ORDER, INVOICE, etc)
custrecord_queue_payloadLong TextJSON payload to send
custrecord_queue_statusListPENDING, PROCESSING, SUCCESS, FAILED
custrecord_queue_attemptsIntegerNumber of retry attempts
custrecord_queue_next_retryDate/TimeWhen to retry next
custrecord_queue_errorLong TextLast error message
custrecord_queue_createdDate/TimeWhen queued
custrecord_queue_endpointTextTarget API endpoint

User Event Script with Queue Fallback

/**
* @NApiVersion 2.1
* @NScriptType UserEventScript
* @description Sync with retry queue fallback
*/
define(['N/https', 'N/record', 'N/log', 'N/runtime'], function(https, record, log, runtime) {

const API_ENDPOINT = 'https://api.external-system.com/orders';
const API_KEY = 'your-api-key';

function afterSubmit(context) {
if (context.type !== context.UserEventType.CREATE) return;

var salesOrder = context.newRecord;
var payload = buildPayload(salesOrder);

// Try immediate sync
var result = trySyncWithRetry(payload, 2); // 2 immediate retries

if (result.success) {
// Update record with external ID
record.submitFields({
type: record.Type.SALES_ORDER,
id: salesOrder.id,
values: {
'custbody_sync_status': 'SYNCED',
'custbody_external_id': result.externalId,
'custbody_sync_date': new Date()
}
});
} else {
// Queue for background retry
queueForRetry(salesOrder.id, 'SALES_ORDER', payload, result.error);

record.submitFields({
type: record.Type.SALES_ORDER,
id: salesOrder.id,
values: { 'custbody_sync_status': 'QUEUED' }
});
}
}

function trySyncWithRetry(payload, maxRetries) {
var lastError = '';

for (var attempt = 1; attempt <= maxRetries; attempt++) {
try {
var response = https.post({
url: API_ENDPOINT,
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + API_KEY
},
body: JSON.stringify(payload)
});

if (response.code >= 200 && response.code < 300) {
var body = JSON.parse(response.body);
return { success: true, externalId: body.id };
}

// Non-retryable error (4xx)
if (response.code >= 400 && response.code < 500) {
return { success: false, error: 'Client error: ' + response.code };
}

lastError = 'Server error: ' + response.code;

} catch (e) {
lastError = e.message;
}

// Brief pause between immediate retries
if (attempt < maxRetries) {
// Small delay (SuiteScript doesn't have sleep, but try/catch acts as delay)
log.debug('Retry', 'Attempt ' + attempt + ' failed, retrying...');
}
}

return { success: false, error: lastError };
}

function queueForRetry(recordId, recordType, payload, errorMsg) {
var queue = record.create({ type: 'customrecord_integration_queue' });
queue.setValue('custrecord_queue_record_id', recordId);
queue.setValue('custrecord_queue_record_type', recordType);
queue.setValue('custrecord_queue_payload', JSON.stringify(payload));
queue.setValue('custrecord_queue_status', 'PENDING');
queue.setValue('custrecord_queue_attempts', 0);
queue.setValue('custrecord_queue_next_retry', new Date());
queue.setValue('custrecord_queue_error', errorMsg);
queue.setValue('custrecord_queue_created', new Date());
queue.setValue('custrecord_queue_endpoint', API_ENDPOINT);
queue.save();

log.audit('Queued', 'Record ' + recordId + ' queued for retry');
}

function buildPayload(salesOrder) {
return {
externalOrderId: salesOrder.getValue('tranid'),
netsuiteId: salesOrder.id,
customer: salesOrder.getText('entity'),
total: salesOrder.getValue('total'),
items: getLineItems(salesOrder)
};
}

function getLineItems(salesOrder) {
var items = [];
var lineCount = salesOrder.getLineCount({ sublistId: 'item' });
for (var i = 0; i < lineCount; i++) {
items.push({
sku: salesOrder.getSublistText({ sublistId: 'item', fieldId: 'item', line: i }),
quantity: salesOrder.getSublistValue({ sublistId: 'item', fieldId: 'quantity', line: i }),
rate: salesOrder.getSublistValue({ sublistId: 'item', fieldId: 'rate', line: i })
});
}
return items;
}

return { afterSubmit: afterSubmit };
});

Map/Reduce Retry Processor

/**
* @NApiVersion 2.1
* @NScriptType MapReduceScript
* @description Process retry queue with exponential backoff
*/
define(['N/https', 'N/record', 'N/search', 'N/log', 'N/email', 'N/runtime'], function(https, record, search, log, email, runtime) {

const MAX_ATTEMPTS = 5;
const ALERT_EMAIL = 'admin@company.com';

function getInputData() {
var now = new Date();

return search.create({
type: 'customrecord_integration_queue',
filters: [
['custrecord_queue_status', 'anyof', ['PENDING', 'PROCESSING']],
'AND',
['custrecord_queue_next_retry', 'onorbefore', now],
'AND',
['custrecord_queue_attempts', 'lessthan', MAX_ATTEMPTS]
],
columns: [
'custrecord_queue_record_id',
'custrecord_queue_record_type',
'custrecord_queue_payload',
'custrecord_queue_attempts',
'custrecord_queue_endpoint',
'custrecord_queue_error'
]
});
}

function map(context) {
var result = JSON.parse(context.value);
var queueId = result.id;

// Mark as processing
record.submitFields({
type: 'customrecord_integration_queue',
id: queueId,
values: { 'custrecord_queue_status': 'PROCESSING' }
});

context.write({
key: queueId,
value: {
queueId: queueId,
recordId: result.values.custrecord_queue_record_id,
recordType: result.values.custrecord_queue_record_type,
payload: result.values.custrecord_queue_payload,
attempts: parseInt(result.values.custrecord_queue_attempts) || 0,
endpoint: result.values.custrecord_queue_endpoint
}
});
}

function reduce(context) {
var data = JSON.parse(context.values[0]);
var queueId = data.queueId;
var attempts = data.attempts + 1;

try {
var payload = JSON.parse(data.payload);

var response = https.post({
url: data.endpoint,
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + runtime.getCurrentScript().getParameter('custscript_api_key')
},
body: JSON.stringify(payload)
});

if (response.code >= 200 && response.code < 300) {
// SUCCESS
var responseBody = JSON.parse(response.body);

// Update queue record
record.submitFields({
type: 'customrecord_integration_queue',
id: queueId,
values: {
'custrecord_queue_status': 'SUCCESS',
'custrecord_queue_attempts': attempts
}
});

// Update original record
updateOriginalRecord(data.recordId, data.recordType, responseBody.id);

log.audit('Retry Success', 'Queue ' + queueId + ' succeeded on attempt ' + attempts);
context.write({ key: 'success', value: queueId });

} else {
throw new Error('HTTP ' + response.code + ': ' + response.body);
}

} catch (e) {
handleRetryFailure(queueId, attempts, e.message, data);
context.write({ key: 'failed', value: queueId });
}
}

function handleRetryFailure(queueId, attempts, errorMsg, data) {
if (attempts >= MAX_ATTEMPTS) {
// Max retries reached - mark as failed and alert
record.submitFields({
type: 'customrecord_integration_queue',
id: queueId,
values: {
'custrecord_queue_status': 'FAILED',
'custrecord_queue_attempts': attempts,
'custrecord_queue_error': errorMsg
}
});

// Update original record
record.submitFields({
type: getRecordType(data.recordType),
id: data.recordId,
values: { 'custbody_sync_status': 'FAILED' }
});

// Send alert email
sendFailureAlert(queueId, data.recordId, data.recordType, attempts, errorMsg);

log.error('Max Retries', 'Queue ' + queueId + ' failed after ' + attempts + ' attempts');

} else {
// Schedule next retry with exponential backoff
var nextRetry = calculateNextRetry(attempts);

record.submitFields({
type: 'customrecord_integration_queue',
id: queueId,
values: {
'custrecord_queue_status': 'PENDING',
'custrecord_queue_attempts': attempts,
'custrecord_queue_next_retry': nextRetry,
'custrecord_queue_error': errorMsg
}
});

log.debug('Retry Scheduled', 'Queue ' + queueId + ' retry #' + (attempts + 1) + ' at ' + nextRetry);
}
}

function calculateNextRetry(attempts) {
// Exponential backoff: 1min, 4min, 16min, 64min, 256min
var delayMinutes = Math.pow(4, attempts);
var maxDelay = 60 * 4; // Cap at 4 hours
delayMinutes = Math.min(delayMinutes, maxDelay);

var nextRetry = new Date();
nextRetry.setMinutes(nextRetry.getMinutes() + delayMinutes);
return nextRetry;
}

function updateOriginalRecord(recordId, recordType, externalId) {
record.submitFields({
type: getRecordType(recordType),
id: recordId,
values: {
'custbody_sync_status': 'SYNCED',
'custbody_external_id': externalId,
'custbody_sync_date': new Date()
}
});
}

function getRecordType(typeString) {
var typeMap = {
'SALES_ORDER': record.Type.SALES_ORDER,
'INVOICE': record.Type.INVOICE,
'CUSTOMER': record.Type.CUSTOMER,
'PURCHASE_ORDER': record.Type.PURCHASE_ORDER
};
return typeMap[typeString] || typeString;
}

function sendFailureAlert(queueId, recordId, recordType, attempts, errorMsg) {
try {
email.send({
author: -5, // System user
recipients: ALERT_EMAIL,
subject: 'Integration Sync Failed - ' + recordType + ' #' + recordId,
body: 'The following record failed to sync after ' + attempts + ' attempts:\n\n' +
'Queue ID: ' + queueId + '\n' +
'Record Type: ' + recordType + '\n' +
'Record ID: ' + recordId + '\n' +
'Attempts: ' + attempts + '\n' +
'Last Error: ' + errorMsg + '\n\n' +
'Please investigate and manually resolve.'
});
} catch (e) {
log.error('Email Failed', e.message);
}
}

function summarize(summary) {
var successCount = 0;
var failCount = 0;

summary.output.iterator().each(function(key, value) {
if (key === 'success') successCount++;
if (key === 'failed') failCount++;
return true;
});

log.audit('Retry Processing Complete',
'Success: ' + successCount + ', Failed: ' + failCount);
}

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

Backoff Schedule

AttemptDelayTotal Time Since First Try
1Immediate0
21 minute1 min
34 minutes5 min
416 minutes21 min
564 minutes1h 25min
FailedAlert sent-

Create a saved search to monitor queue status:

Type: Custom Record - Integration Queue
Columns: Record ID, Record Type, Status, Attempts, Next Retry, Error, Created
Filters: Created within last 7 days
Summary: Count by Status

Script Selection Guide

NeedUse This Script
Sync immediately when record savedUser Event (afterSubmit)
Sync only when specific field/status changesWorkflow Action Script
Sync periodically (every X minutes/hours)Scheduled Script
Sync thousands of recordsMap/Reduce Script
User clicks button to syncClient Script → Suitelet
Handle failures with retryUser Event + Queue + Map/Reduce

  • Pull In - Fetch data from external systems
  • Receive - Accept webhooks from external systems