If there’s one module I use in almost every SuiteScript project, it’s N/record. Whether I’m creating a new customer, loading a sales order to update a field, or copying a record for a workflow workaround β it all goes through N/record. This module is the foundation of record manipulation in SuiteScript 2.x, and understanding it deeply will make you a significantly better NetSuite developer.
This guide covers everything: creating, loading, copying, submitting, and deleting records, working with sublists and subrecords, and the common mistakes that catch developers off guard.
What Is the N/record Module?
The N/record module is NetSuite’s SuiteScript 2.x API for interacting with records. It lets you programmatically create, read, update, and delete any record type in your account β from standard transaction types like invoices and sales orders, to custom records you’ve built yourself.
Load it like any other module:
/**
* @NApiVersion 2.1
* @NScriptType Suitelet
*/
define(['N/record'], function(record) {
// record is now available
});
record.create() β Creating New Records
The most common starting point. record.create() instantiates a new record object in memory. Nothing is saved to the database until you call save().
/**
* @NApiVersion 2.1
* @NScriptType Suitelet
*/
define(['N/record'], function(record) {
function onRequest(context) {
// Create a new customer record
var newCustomer = record.create({
type: record.Type.CUSTOMER,
isDynamic: true // use dynamic mode to trigger field sourcing
});
newCustomer.setValue({ fieldId: 'companyname', value: 'Acme Corp' });
newCustomer.setValue({ fieldId: 'subsidiary', value: 1 });
newCustomer.setValue({ fieldId: 'email', value: 'contact@acmecorp.com' });
var customerId = newCustomer.save();
log.debug('Created customer', 'Internal ID: ' + customerId);
}
return { onRequest: onRequest };
});
Key parameters for record.create():
- type β The record type. Use the
record.Typeenum (e.g.,record.Type.SALES_ORDER) or a string for custom records (e.g.,'customrecord_my_record'). - isDynamic β Boolean. When
true, the record behaves like it does in the UI β field sourcing, default values, and validation all fire. Whenfalse(default), you get better performance but no automatic field sourcing. - defaultValues β An object of field values to pre-set when the record initializes (useful for setting the entity on a transaction before the record is fully built).
record.load() β Loading an Existing Record
record.load() fetches an existing record from the database by its internal ID. You get back a full record object you can read from or write to.
/**
* @NApiVersion 2.1
* @NScriptType Suitelet
*/
define(['N/record'], function(record) {
function onRequest(context) {
var soId = context.request.parameters.so_id;
// Load a sales order
var salesOrder = record.load({
type: record.Type.SALES_ORDER,
id: soId,
isDynamic: false // faster reads when you don't need UI behavior
});
var entityId = salesOrder.getValue({ fieldId: 'entity' });
var entityName = salesOrder.getText({ fieldId: 'entity' });
var amount = salesOrder.getValue({ fieldId: 'amount' });
log.debug('Sales Order', 'Customer: ' + entityName + ', Amount: ' + amount);
}
return { onRequest: onRequest };
});
Use getValue() to get the raw internal value of a field (e.g., an internal ID for a list field). Use getText() to get the display label (e.g., the customer name instead of the ID).
setValue() and getValue() β Reading and Writing Body Fields
These are the workhorses of field interaction. Both methods take an options object with a fieldId property.
/**
* @NApiVersion 2.1
* @NScriptType UserEventScript
*/
define(['N/record'], function(record) {
function beforeSubmit(context) {
var rec = context.newRecord;
// Read a field value
var memo = rec.getValue({ fieldId: 'memo' });
var statusText = rec.getText({ fieldId: 'status' });
// Write a field value
rec.setValue({
fieldId: 'memo',
value: 'Updated by script on ' + new Date().toISOString(),
ignoreFieldChange: false // set true to skip field-change event (dynamic mode only)
});
log.debug('Field update', 'Memo was: ' + memo + ', Status: ' + statusText);
}
return { beforeSubmit: beforeSubmit };
});
The ignoreFieldChange parameter only applies in dynamic mode. When true, it skips any field-change client events triggered by setting the field, which can speed up bulk field updates significantly.
Working with Sublists
Sublists (the line-item sections on a record like the Items sublist on a Sales Order) require their own set of methods. You navigate sublists by line index, which starts at 0.
/**
* @NApiVersion 2.1
* @NScriptType Suitelet
*/
define(['N/record'], function(record) {
function onRequest(context) {
var salesOrder = record.create({
type: record.Type.SALES_ORDER,
isDynamic: true,
defaultValues: { entity: 123 } // pre-set customer
});
salesOrder.setValue({ fieldId: 'trandate', value: new Date() });
// Add a line to the Item sublist
salesOrder.selectNewLine({ sublistId: 'item' });
salesOrder.setCurrentSublistValue({
sublistId: 'item',
fieldId: 'item',
value: 456 // internal ID of the item
});
salesOrder.setCurrentSublistValue({
sublistId: 'item',
fieldId: 'quantity',
value: 2
});
salesOrder.setCurrentSublistValue({
sublistId: 'item',
fieldId: 'rate',
value: 99.99
});
salesOrder.commitLine({ sublistId: 'item' }); // required in dynamic mode
// Add a second line
salesOrder.selectNewLine({ sublistId: 'item' });
salesOrder.setCurrentSublistValue({ sublistId: 'item', fieldId: 'item', value: 789 });
salesOrder.setCurrentSublistValue({ sublistId: 'item', fieldId: 'quantity', value: 1 });
salesOrder.setCurrentSublistValue({ sublistId: 'item', fieldId: 'rate', value: 149.00 });
salesOrder.commitLine({ sublistId: 'item' });
var newSoId = salesOrder.save();
log.debug('Sales Order created', newSoId);
}
return { onRequest: onRequest };
});
In dynamic mode, the flow is: selectNewLine() β setCurrentSublistValue() (repeat for each field) β commitLine(). In static mode, use setSublistValue() with a line index parameter instead β no select/commit needed.
// Static mode sublist example (isDynamic: false)
var rec = record.load({ type: record.Type.SALES_ORDER, id: 100, isDynamic: false });
var lineCount = rec.getLineCount({ sublistId: 'item' });
for (var i = 0; i < lineCount; i++) {
var itemId = rec.getSublistValue({ sublistId: 'item', fieldId: 'item', line: i });
var qty = rec.getSublistValue({ sublistId: 'item', fieldId: 'quantity', line: i });
log.debug('Line ' + i, 'Item: ' + itemId + ', Qty: ' + qty);
}
record.submitFields() β Lightweight Field Updates
If you only need to update one or two fields on a record, loading the entire record and saving it is wasteful. record.submitFields() lets you update specific fields in a single call without loading the full record object.
/**
* @NApiVersion 2.1
* @NScriptType ScheduledScript
*/
define(['N/record', 'N/search'], function(record, search) {
function execute(context) {
// Find all customers missing a phone number
var results = search.create({
type: search.Type.CUSTOMER,
filters: [['phone', 'isempty', '']],
columns: ['internalid']
});
results.run().each(function(result) {
var custId = result.id;
// Update just the memo field without loading the full record
record.submitFields({
type: record.Type.CUSTOMER,
id: custId,
values: {
memo: 'Phone number missing β needs follow-up'
},
options: {
enableSourcing: false, // skip field sourcing for speed
ignoreMandatoryFields: true // skip mandatory field validation
}
});
log.debug('Updated customer', custId);
return true;
});
}
return { execute: execute };
});
record.submitFields() is much faster than load/setValue/save for bulk updates. The governance cost is also lower. The trade-off is that it doesn’t support sublists β only body fields.
record.copy() β Copying an Existing Record
Need to duplicate a record? record.copy() creates a new in-memory copy of an existing record, pre-filled with the source record’s values. You can then modify it before saving.
/**
* @NApiVersion 2.1
* @NScriptType Suitelet
*/
define(['N/record'], function(record) {
function onRequest(context) {
var sourceOrderId = 500;
// Copy an existing sales order
var copiedOrder = record.copy({
type: record.Type.SALES_ORDER,
id: sourceOrderId,
isDynamic: true
});
// Modify the copy before saving
copiedOrder.setValue({ fieldId: 'memo', value: 'Copied from SO #' + sourceOrderId });
copiedOrder.setValue({ fieldId: 'trandate', value: new Date() });
var newOrderId = copiedOrder.save();
log.debug('Copied order', 'New ID: ' + newOrderId);
}
return { onRequest: onRequest };
});
record.delete() β Deleting a Record
record.delete() permanently removes a record. Use it carefully β there’s no undo.
/**
* @NApiVersion 2.1
* @NScriptType Suitelet
*/
define(['N/record'], function(record) {
function onRequest(context) {
var recordId = context.request.parameters.record_id;
record.delete({
type: record.Type.CUSTOMER,
id: recordId
});
log.audit('Record deleted', 'Customer ID: ' + recordId);
}
return { onRequest: onRequest };
});
record.attach() and record.detach() β Managing Relationships
You can link records together (or unlink them) using record.attach() and record.detach(). A common use case is attaching a contact to a customer or linking a file to a record.
/**
* @NApiVersion 2.1
* @NScriptType Suitelet
*/
define(['N/record'], function(record) {
function onRequest(context) {
// Attach a contact to a customer
record.attach({
record: {
type: record.Type.CONTACT,
id: 77
},
to: {
type: record.Type.CUSTOMER,
id: 123
}
});
// Detach the same contact from the customer
record.detach({
record: {
type: record.Type.CONTACT,
id: 77
},
from: {
type: record.Type.CUSTOMER,
id: 123
}
});
}
return { onRequest: onRequest };
});
Real-World Example: Auto-Create a Follow-Up Task on Invoice Creation
Here’s a practical scenario: every time a new invoice is created, automatically create a task assigned to the sales rep to follow up in 7 days.
/**
* @NApiVersion 2.1
* @NScriptType UserEventScript
*/
define(['N/record'], function(record) {
function afterSubmit(context) {
if (context.type !== context.UserEventType.CREATE) return;
var invoice = context.newRecord;
var invoiceId = invoice.id;
var salesRep = invoice.getValue({ fieldId: 'salesrep' });
var entity = invoice.getText({ fieldId: 'entity' });
var amount = invoice.getValue({ fieldId: 'amount' });
if (!salesRep) return; // no sales rep assigned, skip
// Calculate due date: 7 days from today
var dueDate = new Date();
dueDate.setDate(dueDate.getDate() + 7);
// Create a follow-up task
var task = record.create({
type: record.Type.TASK,
isDynamic: true
});
task.setValue({ fieldId: 'title', value: 'Follow up with ' + entity + ' re: Invoice' });
task.setValue({ fieldId: 'assigned', value: salesRep });
task.setValue({ fieldId: 'duedate', value: dueDate });
task.setValue({ fieldId: 'priority', value: 'MEDIUM' });
task.setValue({
fieldId: 'message',
value: 'Invoice for $' + amount + ' created. Please follow up to confirm receipt and answer any questions.'
});
// Link task to the invoice
task.setValue({ fieldId: 'transaction', value: invoiceId });
var taskId = task.save();
log.audit('Task created', 'Follow-up task ID: ' + taskId + ' for invoice ' + invoiceId);
}
return { afterSubmit: afterSubmit };
});
Real-World Example: Bulk Update Item Prices from a Scheduled Script
A scheduled script that reads a custom record containing price adjustments and applies them to inventory items using record.submitFields() for efficiency:
/**
* @NApiVersion 2.1
* @NScriptType ScheduledScript
*/
define(['N/record', 'N/search'], function(record, search) {
function execute(context) {
// Search for pending price adjustment records
var adjustments = search.create({
type: 'customrecord_price_adjustment',
filters: [['custrecord_status', 'anyof', 'PENDING']],
columns: ['custrecord_item_id', 'custrecord_new_price', 'internalid']
});
adjustments.run().each(function(result) {
var itemId = result.getValue('custrecord_item_id');
var newPrice = result.getValue('custrecord_new_price');
var adjustmentId = result.id;
try {
// Update the item's base price using submitFields
record.submitFields({
type: record.Type.INVENTORY_ITEM,
id: itemId,
values: { baseprice: newPrice },
options: { enableSourcing: false, ignoreMandatoryFields: true }
});
// Mark the adjustment as processed
record.submitFields({
type: 'customrecord_price_adjustment',
id: adjustmentId,
values: { custrecord_status: 'PROCESSED' }
});
log.audit('Price updated', 'Item ' + itemId + ' set to ' + newPrice);
} catch (e) {
log.error('Price update failed', 'Item ' + itemId + ': ' + e.message);
record.submitFields({
type: 'customrecord_price_adjustment',
id: adjustmentId,
values: { custrecord_status: 'ERROR' }
});
}
return true;
});
}
return { execute: execute };
});
Common Pitfalls with N/record
Forgetting commitLine() in dynamic mode: If you use selectNewLine() and setCurrentSublistValue() but forget commitLine(), your sublist line won’t be saved. This is one of the most common bugs in sublist code.
Using isDynamic: true when you don’t need it: Dynamic mode triggers field sourcing and validation on every setValue() call. For read-heavy operations or bulk processing, use isDynamic: false β it can be 5β10x faster.
Calling record.load() inside a loop: Every record.load() call costs governance units. Loading records inside a .each() loop on a search will burn through your governance quickly. Use record.submitFields() for bulk updates, or batch your lookups with search.lookupFields().
Not checking for null before getValue(): On optional fields, getValue() can return an empty string or null. Always guard against this before using the value in logic like arithmetic or string operations.
Assuming record types are always the enum: Custom records don’t have a record.Type enum entry. Use the string internal ID directly, e.g., 'customrecord_my_record'. Same applies to some less common standard record types.
Quick Reference: N/record Methods
| Method | Use Case | Key Parameters |
|---|---|---|
record.create() |
Create a new record | type, isDynamic, defaultValues |
record.load() |
Load an existing record by ID | type, id, isDynamic |
record.copy() |
Copy an existing record | type, id, isDynamic |
record.delete() |
Delete a record permanently | type, id |
record.submitFields() |
Update body fields without loading | type, id, values, options |
record.attach() |
Link two records together | record, to |
record.detach() |
Unlink two records | record, from |
rec.getValue() |
Get a body field value | fieldId |
rec.getText() |
Get a body field display label | fieldId |
rec.setValue() |
Set a body field value | fieldId, value, ignoreFieldChange |
rec.getSublistValue() |
Get a sublist field (static mode) | sublistId, fieldId, line |
rec.setSublistValue() |
Set a sublist field (static mode) | sublistId, fieldId, line, value |
rec.selectNewLine() |
Start a new sublist line (dynamic) | sublistId |
rec.setCurrentSublistValue() |
Set current line field (dynamic) | sublistId, fieldId, value |
rec.commitLine() |
Save current line to sublist (dynamic) | sublistId |
rec.getLineCount() |
Count sublist lines | sublistId |
rec.save() |
Persist the record to the database | enableSourcing, ignoreMandatoryFields |
Wrapping Up
The N/record module is at the core of nearly every meaningful SuiteScript operation. Once you understand the difference between dynamic and static mode, know your way around sublists, and learn when to use submitFields() instead of a full load/save cycle, you’ll be writing faster and more reliable scripts across the board.
My personal rule: use static mode (isDynamic: false) for anything performance-sensitive, and only switch to dynamic when you need field sourcing to fire. And always reach for submitFields() when you’re only touching a handful of body fields β it’s one of the best performance optimizations available in SuiteScript.
Discover more from The NetSuite Pro
Subscribe to get the latest posts sent to your email.
Leave a Reply