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