📑 Contents
- 1. JavaScript Syntax Quick Reference
- 2. Hello World - Complete User Event Script
- 3. Client Script - Field Validation & Defaults
- 4. Working with Sublists
- 5. Search Examples
- 6. Scheduled Script with Governance
- 7. Map/Reduce - Payment Processing
- 8. Suitelet - Custom Form
- 9. RESTlet - CRUD Operations
- 10. Workflow Action Script
- 11. Governance Reference
- 12. Real-World Example: Invoice Accrual JE ⭐
1. JavaScript Syntax Quick Reference
Creating Objects
// Anonymous object syntax - used everywhere in SuiteScript
var myObject = {
property1: 'value1',
property2: 123,
property3: function() { return 'Hello'; }
};
The Define Statement
// Standard SuiteScript 2.x module pattern
define(['N/record', 'N/log'], function(record, log) {
// Private functions (not exported)
function helperFunction() {
return 'I am internal';
}
// Entry point function
function afterSubmit(context) {
helperFunction();
log.debug('Executed', 'Script ran!');
}
// Export entry points
return {
afterSubmit: afterSubmit
};
});
Truthy/Falsey Checks
// These are all "falsey" in JavaScript:
// 0, "", null, undefined, NaN, false
// Quick empty check:
if (!someValue) {
// someValue is empty, null, undefined, 0, or false
}
// Equivalent to:
if (someValue == 0 || someValue == "" || someValue == null ||
someValue == undefined || someValue == NaN) {
}
Search Expressions (Shorthand)
// Filter expression syntax: [fieldId, operator, value]
var filters = [
['type', 'anyof', 'SalesOrd'],
'AND',
['mainline', 'is', true],
'AND',
['status', 'noneof', 'SalesOrd:C'] // Not Cancelled
];
// Column expression: just field IDs
var columns = ['tranid', 'entity', 'total', 'status'];
2. Hello World - Complete User Event Script
📚 See Module 2, Module 3/**
* @NApiVersion 2.1
* @NScriptType UserEventScript
* @NModuleScope SameAccount
*
* Complete Hello World script demonstrating:
* - Required annotations
* - Module loading
* - Context object usage
* - All three User Event entry points
* - Logging at different levels
*
* Deploy to: Customer record
*/
define(['N/log', 'N/record'], function(log, record) {
/**
* beforeLoad - Triggered when record is loaded for view/edit
* Use for: Adding buttons, hiding fields, modifying form
*
* @param {Object} context
* @param {Record} context.newRecord - Current record (read-only in View mode)
* @param {string} context.type - UserEventType enum value
* @param {Form} context.form - Form object for UI manipulation
*/
function beforeLoad(context) {
log.debug({
title: 'beforeLoad Triggered',
details: 'Type: ' + context.type + ' | Record ID: ' + context.newRecord.id
});
// Example: Add a custom button (only when editing)
if (context.type === context.UserEventType.EDIT) {
context.form.addButton({
id: 'custpage_mybutton',
label: 'My Custom Button',
functionName: 'myClientFunction' // Must exist in deployed Client Script
});
}
}
/**
* beforeSubmit - Triggered before record is saved to database
* Use for: Validation, setting calculated values, blocking saves
*
* @param {Object} context
* @param {Record} context.newRecord - Record being saved (writable)
* @param {Record} context.oldRecord - Previous record state (on edit/delete)
* @param {string} context.type - 'create', 'edit', 'delete', etc.
*/
function beforeSubmit(context) {
// Only run on create and edit
if (context.type !== context.UserEventType.CREATE &&
context.type !== context.UserEventType.EDIT) {
return;
}
var rec = context.newRecord;
var companyName = rec.getValue('companyname');
// Validation example: require company name
if (!companyName) {
throw 'Company name is required!'; // Blocks save with error
}
// Set a calculated field
rec.setValue({
fieldId: 'custentity_last_modified',
value: new Date()
});
log.audit('Customer Saved', 'Company: ' + companyName);
}
/**
* afterSubmit - Triggered after record is saved
* Use for: Creating related records, sending emails, external integrations
*
* @param {Object} context
* @param {Record} context.newRecord - Saved record (read-only)
* @param {Record} context.oldRecord - Previous state (on edit)
* @param {string} context.type - Trigger type
*/
function afterSubmit(context) {
var rec = context.newRecord;
log.debug({
title: 'afterSubmit Complete',
details: JSON.stringify({
id: rec.id,
type: context.type,
company: rec.getValue('companyname')
})
});
// Example: Create a follow-up task for new customers
if (context.type === context.UserEventType.CREATE) {
var task = record.create({ type: record.Type.TASK });
task.setValue('title', 'Follow up with new customer');
task.setValue('company', rec.id);
task.setValue('priority', 'HIGH');
var taskId = task.save();
log.audit('Task Created', 'Task ID: ' + taskId);
}
}
return {
beforeLoad: beforeLoad,
beforeSubmit: beforeSubmit,
afterSubmit: afterSubmit
};
});
3. Client Script - Field Validation & Defaults
📚 See Module 4/**
* @NApiVersion 2.1
* @NScriptType ClientScript
* @NModuleScope SameAccount
*
* Complete Client Script demonstrating all common entry points
*
* Deploy to: Customer record
* Custom fields required:
* - custentity_apply_coupon (Checkbox)
* - custentity_coupon_code (Free-form text)
*/
define(['N/currentRecord', 'N/log'], function(currentRecord, log) {
/**
* pageInit - Triggered when page loads
* Use for: Setting defaults, enabling/disabling fields
*/
function pageInit(context) {
var rec = context.currentRecord;
var mode = context.mode; // 'create', 'edit', 'view', 'copy'
log.debug('Page Initialized', 'Mode: ' + mode);
// Disable coupon code field initially
var couponField = rec.getField({ fieldId: 'custentity_coupon_code' });
if (couponField) {
couponField.isDisabled = true;
}
// Set default value on new records
if (mode === 'create') {
rec.setValue({
fieldId: 'category',
value: 1 // Default customer category
});
}
}
/**
* fieldChanged - Triggered when any field value changes
* Use for: Field dependencies, enabling/disabling other fields
*/
function fieldChanged(context) {
var rec = context.currentRecord;
var fieldId = context.fieldId;
// Only react to Apply Coupon checkbox
if (fieldId === 'custentity_apply_coupon') {
var applyCoupon = rec.getValue('custentity_apply_coupon');
var couponField = rec.getField({ fieldId: 'custentity_coupon_code' });
if (applyCoupon) {
// Enable coupon code field
couponField.isDisabled = false;
} else {
// Disable and clear coupon code
couponField.isDisabled = true;
rec.setValue({
fieldId: 'custentity_coupon_code',
value: ''
});
}
}
}
/**
* validateField - Triggered when leaving a field
* Use for: Field-level validation
* Return: true to accept, false to reject (keeps focus on field)
*/
function validateField(context) {
var rec = context.currentRecord;
var fieldId = context.fieldId;
if (fieldId === 'custentity_coupon_code') {
var applyCoupon = rec.getValue('custentity_apply_coupon');
var couponCode = rec.getValue('custentity_coupon_code');
// Validate coupon code length if apply coupon is checked
if (applyCoupon && couponCode && couponCode.length !== 5) {
alert('Coupon code must be exactly 5 characters.');
return false; // Keep focus on field
}
}
return true; // Allow field change
}
/**
* saveRecord - Triggered when user clicks Save
* Use for: Form-level validation before submission
* Return: true to save, false to cancel
*/
function saveRecord(context) {
var rec = context.currentRecord;
var applyCoupon = rec.getValue('custentity_apply_coupon');
var couponCode = rec.getValue('custentity_coupon_code');
// Require coupon code if Apply Coupon is checked
if (applyCoupon && (!couponCode || couponCode.length !== 5)) {
alert('Please enter a valid 5-character coupon code.');
return false; // Cancel save
}
// Confirm save
return confirm('Are you sure you want to save this customer?');
}
/**
* lineInit - Triggered when a sublist line is selected/created
* Use for: Setting line defaults
*/
function lineInit(context) {
var rec = context.currentRecord;
var sublistId = context.sublistId;
// Example: Default quantity on custom sublist
if (sublistId === 'recmachcustrecord_product_pref_customer') {
var qty = rec.getCurrentSublistValue({
sublistId: sublistId,
fieldId: 'custrecord_prod_pref_qty'
});
// Default to 1 if empty
if (!qty || isNaN(parseInt(qty))) {
rec.setCurrentSublistValue({
sublistId: sublistId,
fieldId: 'custrecord_prod_pref_qty',
value: 1
});
}
}
}
/**
* validateLine - Triggered when committing a sublist line
* Return: true to accept line, false to reject
*/
function validateLine(context) {
var rec = context.currentRecord;
var sublistId = context.sublistId;
if (sublistId === 'recmachcustrecord_product_pref_customer') {
var qty = parseInt(rec.getCurrentSublistValue({
sublistId: sublistId,
fieldId: 'custrecord_prod_pref_qty'
}));
if (qty > 10) {
alert('Preferred quantity cannot exceed 10.');
return false;
}
}
return true;
}
return {
pageInit: pageInit,
fieldChanged: fieldChanged,
validateField: validateField,
saveRecord: saveRecord,
lineInit: lineInit,
validateLine: validateLine
};
});
4. Working with Sublists
📚 See Module 6/**
* @NApiVersion 2.1
* @NScriptType UserEventScript
*
* Demonstrates sublist manipulation on Sales Orders
*/
define(['N/log', 'N/record'], function(log, record) {
function afterSubmit(context) {
if (context.type !== context.UserEventType.CREATE) return;
var salesOrder = context.newRecord;
// ==== READING SUBLISTS ====
// Get line count
var lineCount = salesOrder.getLineCount({ sublistId: 'item' });
log.debug('Item Lines', 'Total: ' + lineCount);
// Loop through all lines
var orderTotal = 0;
for (var i = 0; i < lineCount; i++) {
// Get values from specific line
var itemId = salesOrder.getSublistValue({
sublistId: 'item',
fieldId: 'item',
line: i
});
var itemName = salesOrder.getSublistText({
sublistId: 'item',
fieldId: 'item',
line: i
});
var quantity = salesOrder.getSublistValue({
sublistId: 'item',
fieldId: 'quantity',
line: i
});
var amount = salesOrder.getSublistValue({
sublistId: 'item',
fieldId: 'amount',
line: i
});
orderTotal += parseFloat(amount) || 0;
log.debug('Line ' + (i + 1),
itemName + ' | Qty: ' + quantity + ' | Amount: ' + amount);
}
log.audit('Order Total Calculated', orderTotal);
}
/**
* Adding lines to a sublist (dynamic mode required)
*/
function addItemToOrder(orderId, itemId, quantity) {
// Load in dynamic mode for sublist manipulation
var order = record.load({
type: record.Type.SALES_ORDER,
id: orderId,
isDynamic: true
});
// Select a new line
order.selectNewLine({ sublistId: 'item' });
// Set values on current line
order.setCurrentSublistValue({
sublistId: 'item',
fieldId: 'item',
value: itemId
});
order.setCurrentSublistValue({
sublistId: 'item',
fieldId: 'quantity',
value: quantity
});
// Commit the line
order.commitLine({ sublistId: 'item' });
// Save changes
order.save();
}
return {
afterSubmit: afterSubmit
};
});
5. Search Examples
📚 See Module 7/**
* @NApiVersion 2.1
* @NScriptType ScheduledScript
*
* Comprehensive search examples
*/
define(['N/search', 'N/log', 'N/record'], function(search, log, record) {
function execute(context) {
// ==== LOAD SAVED SEARCH ====
var savedSearch = search.load({ id: 'customsearch_my_customers' });
// ==== CREATE SEARCH FROM SCRATCH ====
var customerSearch = search.create({
type: search.Type.CUSTOMER,
filters: [
['isinactive', 'is', 'F'],
'AND',
['salesrep', 'noneof', '@NONE@'], // Has a sales rep
'AND',
['datecreated', 'within', 'lastmonth']
],
columns: [
search.createColumn({ name: 'entityid' }),
search.createColumn({ name: 'companyname' }),
search.createColumn({ name: 'email' }),
search.createColumn({ name: 'salesrep' }),
// Join to related record
search.createColumn({
name: 'phone',
join: 'salesrep' // Get sales rep's phone
}),
// Summary column
search.createColumn({
name: 'internalid',
summary: search.Summary.COUNT
})
]
});
// ==== RUN WITH EACH (up to 4000 results) ====
var resultCount = 0;
customerSearch.run().each(function(result) {
var entityId = result.getValue('entityid');
var company = result.getValue('companyname');
var salesRepId = result.getValue('salesrep');
var salesRepName = result.getText('salesrep'); // Display value
log.debug('Customer', company + ' | Rep: ' + salesRepName);
resultCount++;
return true; // Continue to next result (false stops iteration)
});
log.audit('Search Complete', 'Found ' + resultCount + ' customers');
// ==== GETRANGE (for pagination) ====
var results = customerSearch.run().getRange({
start: 0,
end: 100
});
log.debug('First 100', results.length + ' results');
// ==== PAGED EXECUTION (for 4000+ results) ====
var pagedData = customerSearch.runPaged({ pageSize: 1000 });
log.debug('Paged Search', 'Total results: ' + pagedData.count);
pagedData.pageRanges.forEach(function(pageRange) {
var page = pagedData.fetch({ index: pageRange.index });
page.data.forEach(function(result) {
// Process each result
});
});
// ==== LOOKUP FIELDS (single record, 1 unit) ====
var customerData = search.lookupFields({
type: search.Type.CUSTOMER,
id: '123',
columns: ['companyname', 'email', 'salesrep']
});
log.debug('Lookup Result', JSON.stringify(customerData));
// Returns: { companyname: 'ABC Corp', email: 'info@abc.com', salesrep: [{value: '5', text: 'John Smith'}] }
}
return { execute: execute };
});
6. Scheduled Script with Governance
📚 See Module 8, Module 14/**
* @NApiVersion 2.1
* @NScriptType ScheduledScript
*
* Scheduled script with governance monitoring and rescheduling
*/
define(['N/search', 'N/record', 'N/runtime', 'N/task', 'N/log'],
function(search, record, runtime, task, log) {
// Governance threshold - reschedule before running out
var GOVERNANCE_THRESHOLD = 500; // Out of 10,000 units
function execute(context) {
var script = runtime.getCurrentScript();
// Log remaining governance at start
log.audit('Script Started', 'Remaining units: ' + script.getRemainingUsage());
// Search for records to process
var mySearch = search.create({
type: 'customrecord_my_queue',
filters: [['custrecord_processed', 'is', 'F']],
columns: ['internalid', 'name']
});
var processedCount = 0;
mySearch.run().each(function(result) {
// Check governance BEFORE processing
var remaining = script.getRemainingUsage();
if (remaining < GOVERNANCE_THRESHOLD) {
log.audit('Governance Low', 'Rescheduling. Processed: ' + processedCount);
rescheduleScript();
return false; // Stop iteration
}
// Process the record
try {
processRecord(result.id);
processedCount++;
// Log progress every 100 records
if (processedCount % 100 === 0) {
log.audit('Progress', processedCount + ' records. Remaining: ' + remaining);
}
} catch (e) {
log.error('Processing Error', 'Record ' + result.id + ': ' + e.message);
}
return true; // Continue to next
});
log.audit('Script Complete', 'Processed ' + processedCount + ' records');
}
function processRecord(recordId) {
// Load record (10 units)
var rec = record.load({
type: 'customrecord_my_queue',
id: recordId
});
// Do processing...
rec.setValue('custrecord_processed', true);
rec.setValue('custrecord_processed_date', new Date());
// Save record (20 units)
rec.save();
}
function rescheduleScript() {
var scriptTask = task.create({
taskType: task.TaskType.SCHEDULED_SCRIPT,
scriptId: runtime.getCurrentScript().id,
deploymentId: runtime.getCurrentScript().deploymentId
});
var taskId = scriptTask.submit();
log.audit('Rescheduled', 'New task ID: ' + taskId);
}
return { execute: execute };
});
7. Map/Reduce - Payment Processing
📚 See Module 9/**
* @NApiVersion 2.1
* @NScriptType MapReduceScript
*
* Calculate total payments per customer
* Demonstrates all four M/R stages
*/
define(['N/search', 'N/log', 'N/runtime'], function(search, log, runtime) {
/**
* STAGE 1: getInputData
* Returns data to be processed (search, array, or object)
* Automatic governance handling
*/
function getInputData() {
log.audit('M/R Started', 'getInputData executing');
// Return a search - each result becomes a map() input
return search.create({
type: 'transaction',
filters: [
['type', 'anyof', 'CustPymt'],
'AND',
['mainline', 'is', true]
],
columns: ['entity', 'status', 'amount']
});
// Alternative: Return saved search by ID
// return search.load({ id: 'customsearch_payments' });
// Alternative: Return array
// return [{ id: 1, name: 'A' }, { id: 2, name: 'B' }];
}
/**
* STAGE 2: map
* Process each input individually
* Write key-value pairs for reduce stage
*/
function map(context) {
// context.value is JSON string of search result
var searchResult = JSON.parse(context.value);
// Extract values from search result structure
var customerId = searchResult.values.entity.value;
var customerName = searchResult.values.entity.text;
var status = searchResult.values.status.value;
var amount = parseFloat(searchResult.values.amount) || 0;
// Write key-value pair
// All values with same key go to same reduce()
context.write({
key: customerId,
value: JSON.stringify({
name: customerName,
status: status,
amount: amount
})
});
}
/**
* STAGE 3: reduce
* Process all values for each unique key
*/
function reduce(context) {
var customerId = context.key;
var values = context.values; // Array of all values for this key
var depositedTotal = 0;
var undepositedTotal = 0;
var customerName = '';
// Process all payments for this customer
values.forEach(function(valueStr) {
var payment = JSON.parse(valueStr);
customerName = payment.name;
if (payment.status === 'deposited') {
depositedTotal += payment.amount;
} else {
undepositedTotal += payment.amount;
}
});
// Log results for this customer
log.audit('Customer Totals', customerName +
' | Deposited: $' + depositedTotal.toFixed(2) +
' | Undeposited: $' + undepositedTotal.toFixed(2));
// Optionally write to summarize stage
context.write({
key: customerId,
value: depositedTotal + undepositedTotal
});
}
/**
* STAGE 4: summarize
* Final processing, error handling, reporting
*/
function summarize(summary) {
// Log overall statistics
log.audit('Summary', JSON.stringify({
usage: summary.usage,
concurrency: summary.concurrency,
yields: summary.yields
}));
// Check for errors
if (summary.inputSummary.error) {
log.error('Input Error', summary.inputSummary.error);
}
summary.mapSummary.errors.iterator().each(function(key, error) {
log.error('Map Error', 'Key: ' + key + ' Error: ' + error);
return true;
});
summary.reduceSummary.errors.iterator().each(function(key, error) {
log.error('Reduce Error', 'Key: ' + key + ' Error: ' + error);
return true;
});
// Process final output
var grandTotal = 0;
summary.output.iterator().each(function(key, value) {
grandTotal += parseFloat(value) || 0;
return true;
});
log.audit('Grand Total', '$' + grandTotal.toFixed(2));
}
return {
getInputData: getInputData,
map: map,
reduce: reduce,
summarize: summarize
};
});
8. Suitelet - Custom Form
📚 See Module 12/**
* @NApiVersion 2.1
* @NScriptType Suitelet
*
* Custom form with processing
*/
define(['N/ui/serverWidget', 'N/search', 'N/log', 'N/redirect'],
function(serverWidget, search, log, redirect) {
function onRequest(context) {
if (context.request.method === 'GET') {
// Display the form
showForm(context);
} else {
// Process form submission
processForm(context);
}
}
function showForm(context) {
var form = serverWidget.createForm({
title: 'Customer Lookup'
});
// Add fields (custpage_ prefix required!)
form.addField({
id: 'custpage_customer',
type: serverWidget.FieldType.SELECT,
label: 'Select Customer',
source: 'customer' // Populates from customer list
});
form.addField({
id: 'custpage_start_date',
type: serverWidget.FieldType.DATE,
label: 'Start Date'
});
form.addField({
id: 'custpage_end_date',
type: serverWidget.FieldType.DATE,
label: 'End Date'
});
// Add field group
var filterGroup = form.addFieldGroup({
id: 'custpage_filters',
label: 'Filters'
});
// Checkbox
var activeOnly = form.addField({
id: 'custpage_active_only',
type: serverWidget.FieldType.CHECKBOX,
label: 'Active Transactions Only',
container: 'custpage_filters'
});
activeOnly.defaultValue = 'T';
// Add submit button
form.addSubmitButton({ label: 'Search' });
// Add sublist for results
var sublist = form.addSublist({
id: 'custpage_results',
type: serverWidget.SublistType.LIST,
label: 'Search Results'
});
sublist.addField({
id: 'custpage_tranid',
type: serverWidget.FieldType.TEXT,
label: 'Transaction #'
});
sublist.addField({
id: 'custpage_date',
type: serverWidget.FieldType.DATE,
label: 'Date'
});
sublist.addField({
id: 'custpage_amount',
type: serverWidget.FieldType.CURRENCY,
label: 'Amount'
});
context.response.writePage(form);
}
function processForm(context) {
var request = context.request;
// Get submitted values
var customerId = request.parameters.custpage_customer;
var startDate = request.parameters.custpage_start_date;
var endDate = request.parameters.custpage_end_date;
log.debug('Form Submitted', JSON.stringify({
customer: customerId,
startDate: startDate,
endDate: endDate
}));
// Redirect to customer record
redirect.toRecord({
type: 'customer',
id: customerId
});
// Or redirect back to suitelet with results
// redirect.toSuitelet({
// scriptId: 'customscript_my_suitelet',
// deploymentId: 'customdeploy_my_suitelet',
// parameters: { custid: customerId }
// });
}
return { onRequest: onRequest };
});
9. RESTlet - CRUD Operations
📚 See Module 13/**
* @NApiVersion 2.1
* @NScriptType Restlet
*
* RESTful API for Customer records
*/
define(['N/record', 'N/search', 'N/log'], function(record, search, log) {
/**
* GET - Retrieve customer data
* URL params come as requestParams object
*/
function get(requestParams) {
var customerId = requestParams.id;
if (!customerId) {
return { error: true, message: 'Customer ID required' };
}
try {
var customer = record.load({
type: record.Type.CUSTOMER,
id: customerId
});
return {
success: true,
data: {
id: customer.id,
entityId: customer.getValue('entityid'),
companyName: customer.getValue('companyname'),
email: customer.getValue('email'),
phone: customer.getValue('phone'),
balance: customer.getValue('balance')
}
};
} catch (e) {
log.error('GET Error', e.message);
return { error: true, message: e.message };
}
}
/**
* POST - Create new customer
* Request body comes as requestBody object
*/
function post(requestBody) {
try {
var customer = record.create({
type: record.Type.CUSTOMER
});
customer.setValue('companyname', requestBody.companyName);
customer.setValue('email', requestBody.email);
if (requestBody.phone) {
customer.setValue('phone', requestBody.phone);
}
var customerId = customer.save();
return {
success: true,
id: customerId,
message: 'Customer created'
};
} catch (e) {
log.error('POST Error', e.message);
return { error: true, message: e.message };
}
}
/**
* PUT - Update existing customer
*/
function put(requestBody) {
if (!requestBody.id) {
return { error: true, message: 'Customer ID required' };
}
try {
var customer = record.load({
type: record.Type.CUSTOMER,
id: requestBody.id
});
// Update only provided fields
if (requestBody.companyName) {
customer.setValue('companyname', requestBody.companyName);
}
if (requestBody.email) {
customer.setValue('email', requestBody.email);
}
if (requestBody.phone) {
customer.setValue('phone', requestBody.phone);
}
customer.save();
return { success: true, message: 'Customer updated' };
} catch (e) {
log.error('PUT Error', e.message);
return { error: true, message: e.message };
}
}
/**
* DELETE - Remove customer
*/
function doDelete(requestParams) {
if (!requestParams.id) {
return { error: true, message: 'Customer ID required' };
}
try {
record.delete({
type: record.Type.CUSTOMER,
id: requestParams.id
});
return { success: true, message: 'Customer deleted' };
} catch (e) {
log.error('DELETE Error', e.message);
return { error: true, message: e.message };
}
}
return {
get: get,
post: post,
put: put,
'delete': doDelete // 'delete' is reserved word
};
});
10. Workflow Action Script
📚 See Module 11/**
* @NApiVersion 2.1
* @NScriptType WorkflowActionScript
*
* Custom workflow action to update related records
* Attach directly to workflow (no deployment needed)
*/
define(['N/record', 'N/runtime', 'N/log'], function(record, runtime, log) {
function onAction(context) {
// Get the record triggering the workflow
var salesOrder = context.newRecord;
var orderId = salesOrder.id;
// Get script parameter from workflow
var script = runtime.getCurrentScript();
var orderDate = script.getParameter({ name: 'custscript_urk_order_date' });
// Get values from the sales order
var customerId = salesOrder.getValue('entity');
var lineCount = salesOrder.getLineCount({ sublistId: 'item' });
// Build notes for customer record
var notes = 'Last Order Date: ' + orderDate + '\\n';
notes += 'Unique items ordered: ' + lineCount;
try {
// Load and update customer
var customer = record.load({
type: record.Type.CUSTOMER,
id: customerId
});
customer.setValue({
fieldId: 'comments',
value: notes
});
customer.save();
log.audit('Workflow Action', 'Updated customer ' + customerId);
// Return value for workflow branching
return 'SUCCESS';
} catch (e) {
log.error('Workflow Action Error', e.message);
return 'ERROR';
}
}
return { onAction: onAction };
});
11. Governance Reference
📚 See Module 14Script Type Limits
| Script Type | Governance Limit |
|---|---|
| Client Script | 1,000 units |
| User Event | 1,000 units |
| Suitelet | 1,000 units |
| RESTlet | 5,000 units |
| Scheduled Script | 10,000 units |
| Map/Reduce | Automatic (per stage) |
Common Operation Costs
| Operation | Entity/CRM | Transaction | Custom |
|---|---|---|---|
| record.load() | 5 units | 10 units | 2 units |
| record.save() | 10 units | 20 units | 4 units |
| record.delete() | 20 units | 20 units | 4 units |
| record.submitFields() | 2 units (recommended!) | ||
| search.create().run() | 5 units | ||
| search.lookupFields() | 1 unit (recommended!) | ||
| ResultSet.each() | 10 units per 1000 results | ||
| email.send() | 20 units | ||
| http.get/post() | 10 units | ||
✅ Optimization Tips
- Use
record.submitFields()(2 units) instead of load+save (15+ units) - Use
search.lookupFields()(1 unit) for single record reads - Offload heavy processing to Scheduled or Map/Reduce scripts
- Check
getRemainingUsage()before expensive operations - Use script parameters instead of hardcoded values
12. Real-World Example: Invoice Accrual Journal Entry
🏢 Production Script Overview
This is a real production Workflow Action Script that creates accrual Journal Entries when Invoices are created. It demonstrates 10 of 16 modules in action, including dynamic record creation, sublist iteration, searches, script parameters, and error handling with email notifications.
Modules Demonstrated
| Module | Concepts Used |
|---|---|
| 0-2 | JSDoc annotations, define(), module loading pattern |
| 3 | getValue(), getSublistValue(), getSublistText(), setValue() |
| 4 | Entry points (onAction for Workflow Action) |
| 5 | N/record, N/search, N/email, N/runtime modules |
| 6 | Sublists: getLineCount(), selectNewLine(), setCurrentSublistValue(), commitLine() |
| 7 | search.create() with filters, run().each() |
| 10 | Script parameters via runtime.getCurrentScript().getParameter() |
| 11 | Workflow Action Script type |
| 14 | try/catch/finally, log.error(), email error notifications |
| 15 | Modular helper functions, comprehensive logging |
Complete Annotated Script
/**
* @NApiVersion 2.x
* @NScriptType workflowactionscript
* ═══════════════════════════════════════════════════════════════════════════
* 📚 MODULE 2: Required JSDoc Annotations
* - @NApiVersion 2.x tells NetSuite which API version
* - @NScriptType workflowactionscript defines the script type
* ═══════════════════════════════════════════════════════════════════════════
*
* @Author: Bruno
* @Created: 2020-08-19
* @ScriptName: WA_Make_Accruals_JE_on_Inv_Create
* @Filename: WA_Make_Accruals_JE_on_Inv_Create.js
* @ScriptID: customscript_wa_make_je_on_inv_create
*
* @SandBox:
* @FileID: [SANDBOX_FILE_ID]
* @InternalURL: https://[ACCOUNT_ID]-sb1.app.netsuite.com/app/common/scripting/script.nl?id=[SCRIPT_ID]
* @DeploymentURL: https://[ACCOUNT_ID]-sb1.app.netsuite.com/app/common/scripting/scriptrecord.nl?id=[DEPLOY_ID]
*
* @Production:
* @FileID: [PROD_FILE_ID]
* @InternalURL: https://[ACCOUNT_ID].app.netsuite.com/app/common/scripting/script.nl?id=[SCRIPT_ID]
* @DeploymentURL: https://[ACCOUNT_ID].app.netsuite.com/app/common/scripting/scriptrecord.nl?id=[DEPLOY_ID]
*
* @AdditionalInfo:
* Accrual account: [ACCRUAL_ACCOUNT_ID] "Accrued Revenue (Invoice Pending)"
*
* @modifications
* Version Author Date Remarks
* v1.0.0 Bruno 2020-08-19 Initial version
* v1.1.0 Bruno 2020-08-27 Recreate all line fields from invoice on JE
* v1.2.0 Bruno 2020-09-02 Set JE trandate = inv trandate
*/
// ═══════════════════════════════════════════════════════════════════════════
// 📚 MODULE 2 & 5: The define() Statement
// - First parameter: Array of module dependencies
// - Second parameter: Callback function receiving module objects
// - N/record: Create, load, save records
// - N/search: Query NetSuite data
// - N/email: Send email notifications
// - N/runtime: Access script parameters and execution context
// ═══════════════════════════════════════════════════════════════════════════
define(['N/record', 'N/search', 'N/email', 'N/runtime'],
function(record, search, email, runtime) {
// 📚 MODULE 10: Get script object ONCE at the top for efficiency
// This gives us access to script parameters defined in the deployment
var scriptObj = runtime.getCurrentScript();
/**
* ═══════════════════════════════════════════════════════════════════════
* 📚 MODULE 11: Workflow Action Script Entry Point
*
* onAction(context) is triggered when:
* - The workflow reaches this custom action
* - Can be triggered Before Record Submit or After Record Submit
*
* context.newRecord - The record that triggered the workflow
* Return value - Can be used for workflow branching (1, 'SUCCESS', etc.)
* ═══════════════════════════════════════════════════════════════════════
*/
function onAction(context) {
// 📚 MODULE 14: Wrap everything in try/catch for error handling
try {
// 📚 MODULE 11: Get the triggering record from workflow context
var newRec = context.newRecord;
// 📚 MODULE 15: Debug logging - log the record ID and structure
log.debug('newRec', newRec.id + ' >> ' + JSON.stringify(newRec));
// 📚 MODULE 3: getValue() to read body field
// Check if this invoice already has an accrual JE linked
var existingJE;
if (newRec.getValue({fieldId: 'custbody_accruals_journal_entry'})) {
// If field has value, verify JE still exists via search
existingJE = checkForExistingJE(newRec.id);
} else {
existingJE = newRec.getValue({fieldId: 'custbody_accruals_journal_entry'});
}
log.debug('existingJE', JSON.stringify(existingJE));
// Only create new JE if one doesn't exist
var jeID;
if (existingJE == '') {
jeID = createNewJE(newRec);
}
log.debug('jeID', jeID);
// 📚 MODULE 5: record.submitFields() - Update without loading record
// This is MORE EFFICIENT than load() + setValue() + save()
// Only costs 2 governance units vs 15+ for load/save!
if (jeID) {
record.submitFields({
type: record.Type.INVOICE,
id: newRec.id,
values: {
custbody_accruals_journal_entry: jeID
},
options: {
enableSourcing: false,
ignoreMandatoryFields: true
}
});
}
} catch (err01) {
// 📚 MODULE 14: Log errors with context
log.debug('onAction.err01', JSON.stringify(err01));
} finally {
// 📚 MODULE 11: Return value for workflow branching
// 1 = success, workflow can use this to determine next state
return 1;
}
}
/***********************************************/
/*********** BEGIN UTILITY FUNCTIONS ***********/
/***********************************************/
/**
* ═══════════════════════════════════════════════════════════════════════
* 📚 MODULE 7: Search for Existing Journal Entry
*
* Uses search.create() to find any JE already linked to this invoice.
* Demonstrates:
* - Filter expressions with AND operators
* - Multiple filter conditions
* - run().each() for processing results
* - Returning early with 'return false'
* ═══════════════════════════════════════════════════════════════════════
*/
function checkForExistingJE(invID) {
var jeID = '';
try {
// 📚 MODULE 7: search.create() with filter expression syntax
// Format: [fieldId, operator, value]
// Use "AND" string between filter arrays
var jeSrchObj = search.create({
type: "journalentry",
filters: [
["type", "anyof", "Journal"],
"AND",
// Custom column linking JE line to original invoice
["custcol_je_attached_invoice", "anyof", invID],
"AND",
// mainline=T gets header, not individual lines
["mainline", "is", "T"],
"AND",
// Exclude reversed JEs
["isreversal", "is", "F"],
"AND",
["reversaldate", "isempty", ""]
]
});
// 📚 MODULE 7: run().each() processes up to 4000 results
// Return false to stop after first match (we only need one)
jeSrchObj.run().each(function(r) {
log.debug('jeSrchObj.r', JSON.stringify(r));
jeID = r.id;
return false; // Stop iterating - found what we need
});
} catch (err01) {
// 📚 MODULE 14: log.error() for errors (shows in red in logs)
log.error('checkForExistingJE.err01', JSON.stringify(err01));
} finally {
return jeID;
}
}
/**
* ═══════════════════════════════════════════════════════════════════════
* 📚 MODULE 5 & 6: Create New Journal Entry from Invoice
*
* This function demonstrates:
* - Dynamic record creation with record.create()
* - Reading sublist data with getLineCount() and getSublistValue()
* - Writing sublist data with selectNewLine(), setCurrentSublistValue(), commitLine()
* - Error handling with email notification
* ═══════════════════════════════════════════════════════════════════════
*/
function createNewJE(invRec) {
var jeID = '';
try {
// ─────────────────────────────────────────────────────────────
// 📚 MODULE 3: Read header fields from the invoice
// ─────────────────────────────────────────────────────────────
var invSub = invRec.getValue({fieldId: 'subsidiary'});
var invEntity = invRec.getValue({fieldId: 'entity'});
var revRecDate = invRec.getValue({fieldId: 'custbody30'});
var tranDate = invRec.getValue({fieldId: 'trandate'});
var today = new Date().toISOString();
var memo = 'Created from invoice: ' + invRec.getValue({fieldId: 'tranid'}) +
' (invID: ' + invRec.id + ') on ' + today;
// 📚 MODULE 6: getLineCount() returns number of sublist lines
var numItems = invRec.getLineCount({sublistId: 'item'});
// 📚 MODULE 15: Debug logging for troubleshooting
log.debug('invSub', invSub);
log.debug('invEntity', invEntity);
log.debug('memo', memo);
log.debug('numItems', numItems);
// ─────────────────────────────────────────────────────────────
// 📚 MODULE 5: Create new record with record.create()
// isDynamic: true = use selectNewLine/commitLine pattern
// defaultValues: Pre-populate fields during creation
// ─────────────────────────────────────────────────────────────
var jeRec = record.create({
type: record.Type.JOURNAL_ENTRY,
isDynamic: true,
defaultValues: {
subsidiary: invSub,
customform: [JE_FORM_ID]
}
});
// 📚 MODULE 3: setValue() to set header fields
jeRec.setValue({fieldId: 'trandate', value: tranDate});
jeRec.setValue({fieldId: 'memo', value: invRec.getValue({fieldId: 'tranid'})});
if (revRecDate != '') {
jeRec.setValue({fieldId: 'custbody30', value: revRecDate});
}
var invAmount = 0;
// ─────────────────────────────────────────────────────────────
// 📚 MODULE 6: Loop through invoice lines to create JE lines
// ─────────────────────────────────────────────────────────────
for (var i = 0; i < numItems; i++) {
// 📚 MODULE 6: getSublistValue() reads line item fields
// Parameters: sublistId, fieldId, line (0-based index)
var itemID = invRec.getSublistValue({
sublistId: 'item',
fieldId: 'item',
line: i
}) || '';
var description = invRec.getSublistValue({
sublistId: 'item',
fieldId: 'description',
line: i
}) || '';
var amount = invRec.getSublistValue({
sublistId: 'item',
fieldId: 'amount',
line: i
}) || 0.00;
// Additional line fields for complete data transfer
var role = invRec.getSublistValue({sublistId: 'item', fieldId: 'custcol9', line: i}) || '';
var payStart = invRec.getSublistValue({sublistId: 'item', fieldId: 'custcol7', line: i}) || '';
var payEnd = invRec.getSublistValue({sublistId: 'item', fieldId: 'custcol8', line: i}) || '';
var qty = invRec.getSublistValue({sublistId: 'item', fieldId: 'quantity', line: i}) || '';
var rate = invRec.getSublistValue({sublistId: 'item', fieldId: 'rate', line: i}) || 0.00;
var dept = invRec.getSublistValue({sublistId: 'item', fieldId: 'department', line: i}) || '';
var classID = invRec.getSublistValue({sublistId: 'item', fieldId: 'class', line: i}) || '';
var loc = invRec.getSublistValue({sublistId: 'item', fieldId: 'location', line: i}) || '';
// 📚 MODULE 6: getSublistText() returns display value (not ID)
var taxCode = invRec.getSublistText({
sublistId: 'item',
fieldId: 'taxcode',
line: i
}) || '';
// Determine which account to use (custom override or default)
var itemAcct = '';
var customAcct = invRec.getSublistValue({
sublistId: 'item',
fieldId: 'custcol_item_account',
line: i
});
if (customAcct != '' && customAcct !== undefined) {
itemAcct = customAcct;
} else {
itemAcct = invRec.getSublistValue({
sublistId: 'item',
fieldId: 'account',
line: i
});
}
// Skip lines without an account
if (!itemAcct) continue;
// Running total for the debit side
invAmount = parseFloat(invAmount) + parseFloat(amount);
// ─────────────────────────────────────────────────────────────
// 📚 MODULE 6: CREDIT LINE - Dynamic mode sublist pattern
// 1. selectNewLine() - Start a new line
// 2. setCurrentSublistValue() - Set field values
// 3. commitLine() - Save the line
// ─────────────────────────────────────────────────────────────
jeRec.selectNewLine({sublistId: 'line'});
// Required fields for JE line
jeRec.setCurrentSublistValue({
sublistId: 'line',
fieldId: 'account',
value: itemAcct
});
jeRec.setCurrentSublistValue({
sublistId: 'line',
fieldId: 'credit',
value: amount
});
jeRec.setCurrentSublistValue({
sublistId: 'line',
fieldId: 'entity',
value: invEntity
});
// Link back to original invoice for audit trail
jeRec.setCurrentSublistValue({
sublistId: 'line',
fieldId: 'custcol_je_attached_invoice',
value: invRec.id
});
// Optional fields - only set if they have values
if (description != '') {
jeRec.setCurrentSublistValue({
sublistId: 'line',
fieldId: 'memo',
value: description
});
}
if (itemID != '') {
jeRec.setCurrentSublistValue({
sublistId: 'line',
fieldId: 'custcol_invoice_item',
value: itemID
});
}
if (dept != '') {
jeRec.setCurrentSublistValue({
sublistId: 'line',
fieldId: 'department',
value: dept
});
}
if (classID != '') {
jeRec.setCurrentSublistValue({
sublistId: 'line',
fieldId: 'class',
value: classID
});
}
if (loc != '') {
jeRec.setCurrentSublistValue({
sublistId: 'line',
fieldId: 'location',
value: loc
});
}
// 📚 MODULE 6: commitLine() saves the current line
jeRec.commitLine({sublistId: 'line'});
}
// ─────────────────────────────────────────────────────────────
// DEBIT LINE - Single line to Accrual Account
// This balances all the credit lines above
// ─────────────────────────────────────────────────────────────
jeRec.selectNewLine({sublistId: 'line'});
jeRec.setCurrentSublistValue({
sublistId: 'line',
fieldId: 'account',
value: [ACCRUAL_ACCOUNT_ID] // Accrued Revenue account
});
jeRec.setCurrentSublistValue({
sublistId: 'line',
fieldId: 'debit',
value: invAmount // Total of all credit lines
});
jeRec.setCurrentSublistValue({
sublistId: 'line',
fieldId: 'entity',
value: invEntity
});
jeRec.setCurrentSublistValue({
sublistId: 'line',
fieldId: 'custcol_je_attached_invoice',
value: invRec.id
});
jeRec.setCurrentSublistValue({
sublistId: 'line',
fieldId: 'memo',
value: memo
});
jeRec.commitLine({sublistId: 'line'});
log.debug('jeRec', JSON.stringify(jeRec));
// 📚 MODULE 5: save() commits the record to database
// Returns the internal ID of the new record
jeID = jeRec.save();
} catch (err01) {
// ─────────────────────────────────────────────────────────────
// 📚 MODULE 14: Error Handling with Email Notification
// In production, always notify someone when critical scripts fail!
// ─────────────────────────────────────────────────────────────
log.error('createNewJE.err01', JSON.stringify(err01));
var mailSubject = 'Invoice ' + invRec.getValue('tranid') +
' Accrual JE Creation Error.';
var mailBody = err01.message;
// 📚 MODULE 10: Get script parameters for configurable values
// This allows changing recipients without modifying code!
var toEmails = scriptObj.getParameter('custscript_je_create_notify_emails');
var emailArray = toEmails.split(",");
// 📚 MODULE 5: email.send() for notifications
// relatedRecords links the email to the transaction for easy access
email.send({
author: scriptObj.getParameter('custscript_error_email_from'),
recipients: emailArray,
subject: mailSubject,
body: mailBody,
relatedRecords: {
transactionId: Number(invRec.id)
}
});
} finally {
return jeID;
}
}
/***********************************************/
/************ END UTILITY FUNCTIONS ************/
/***********************************************/
// 📚 MODULE 2: Return object exports the entry point function
return {
onAction: onAction
};
});
⚠️ Configuration Required
Before deploying, replace these placeholders with your actual values:
[ACCOUNT_ID]- Your NetSuite account number[SANDBOX_FILE_ID],[PROD_FILE_ID]- File Cabinet internal IDs[SCRIPT_ID],[DEPLOY_ID]- Script and deployment internal IDs[ACCRUAL_ACCOUNT_ID]- Internal ID of your Accrued Revenue GL account[JE_FORM_ID]- Internal ID of your Journal Entry custom form
📘 Script Parameters Required
Create these parameters in the Script Record → Parameters subtab:
| ID | Type | Description |
|---|---|---|
custscript_je_create_notify_emails | Free-Form Text | Comma-separated email addresses for error notifications |
custscript_error_email_from | List/Record (Employee) | Employee to send error emails from |
✅ Key Patterns Demonstrated
- Modular Design - Main function calls helper functions for readability
- Defensive Coding - Check for empty values before setting fields
- Audit Trail - Link JE back to source invoice via custom column
- Configurable Values - Use script parameters instead of hardcoded IDs
- Error Recovery - Email notifications when critical operations fail
- Efficient Updates - Use submitFields() instead of load/save pattern