If you’ve spent any time building automations in NetSuite, you already know that almost everything revolves around records β sales orders, invoices, customers, items, journal entries. The N/record module is the backbone of SuiteScript development, and honestly, it’s the one module you can’t avoid. Whether you’re creating a new vendor bill, loading a customer record to update a field, or deleting a duplicate entry, this is the module you’ll reach for every single time.
In this guide, we’re going to walk through everything you need to know about the N/record module β from basic CRUD operations to more advanced patterns like dynamic mode, sublist manipulation, and handling body fields vs. line-level fields. By the end, you’ll have a solid mental model of how records work in SuiteScript and the code snippets to back it up.
What Is the N/record Module?
The N/record module is NetSuite’s official SuiteScript API for interacting with records in the system. It lets you programmatically do everything you could do manually through the UI β create records, load existing ones, edit field values, add sublist lines, save changes, copy records, and delete them.
It supports two main modes of operation:
- Standard mode β the default. Fields and sublists are loaded in memory, and nothing fires until you explicitly save. This is faster and generally preferred for batch operations.
- Dynamic mode β mirrors the behavior of the UI. Field change events, sourcing, and validation fire as you set values. Use this when you need the system to behave exactly like a user editing the record manually.
You load the module in your script like this:
/**
* @NApiVersion 2.1
* @NScriptType ScheduledScript
*/
define(['N/record'], function(record) {
// your code here
});
Creating a New Record
Creating a new record with N/record is pretty straightforward. You use record.create(), set your fields, and then call save(). Here’s a real-world example β creating a new customer record:
// Standard mode - create a customer
var newCustomer = record.create({
type: record.Type.CUSTOMER,
isDynamic: false
});
newCustomer.setValue({ fieldId: 'companyname', value: 'Acme Corp' });
newCustomer.setValue({ fieldId: 'email', value: 'contact@acmecorp.com' });
newCustomer.setValue({ fieldId: 'phone', value: '555-123-4567' });
newCustomer.setValue({ fieldId: 'subsidiary', value: 1 });
var customerId = newCustomer.save();
log.debug('Created customer', 'Internal ID: ' + customerId);
A couple of things worth noting here. First, the type parameter takes a record type string β you can use the record.Type enum (which gives you nice autocomplete in your IDE) or just pass the string directly like 'customer'. Second, save() returns the internal ID of the newly created record, which you’ll almost always want to capture.
Creating in Dynamic Mode
If you need field-change triggers to fire (for example, when selecting a customer on a sales order causes the billing address to auto-populate), use dynamic mode instead:
var salesOrder = record.create({
type: record.Type.SALES_ORDER,
isDynamic: true
});
salesOrder.setValue({ fieldId: 'entity', value: 42 });
salesOrder.setValue({ fieldId: 'trandate', value: new Date() });
Loading an Existing Record
Loading a record means pulling it out of the database so you can read or modify it. You need the record type and the internal ID:
var customerRec = record.load({
type: record.Type.CUSTOMER,
id: 123,
isDynamic: false
});
var companyName = customerRec.getValue({ fieldId: 'companyname' });
var email = customerRec.getValue({ fieldId: 'email' });
log.debug('Customer Info', companyName + ' | ' + email);
One thing developers get tripped up on: record.load() throws an error if the record doesn’t exist or if the user doesn’t have access to it. If you’re not sure the record exists, wrap it in a try/catch.
Read-Only Access: N/record vs. N/search
If you only need to read field values and don’t plan to edit anything, consider using N/search instead of loading the full record. record.load() pulls the entire record into memory and counts against your governance usage. For reading many records at once, a saved search is far more efficient.
Updating an Existing Record
There are two ways to update a record: load it, change the fields, and save it β or use the lighter-weight record.submitFields() for simple body field updates.
Full Load and Save
var rec = record.load({
type: record.Type.VENDOR_BILL,
id: 456
});
rec.setValue({ fieldId: 'memo', value: 'Reviewed and approved - May 2026' });
rec.setValue({ fieldId: 'approvalstatus', value: 2 });
rec.save({
enableSourcing: true,
ignoreMandatoryFields: false
});
Using submitFields for Quick Updates
record.submitFields() is a great shortcut when you only need to update a handful of body-level fields. It doesn’t load the full record into memory, so it’s faster and uses fewer governance units:
record.submitFields({
type: record.Type.CUSTOMER,
id: 123,
values: {
email: 'newemail@acmecorp.com',
phone: '555-999-0000'
},
options: {
enableSourcing: false,
ignoreMandatoryFields: true
}
});
Keep in mind that submitFields() won’t work for sublist lines β you need the full load/save approach for that. Also, if you have a user event script on the record, the beforeSubmit and afterSubmit triggers will still fire.
Working with Sublists
Sublists are the line-level data on a record β think the line items on a sales order, or the expense lines on an expense report. Working with them is a bit different from body fields.
Reading Sublist Lines
var salesOrder = record.load({
type: record.Type.SALES_ORDER,
id: 789
});
var lineCount = salesOrder.getLineCount({ sublistId: 'item' });
for (var i = 0; i < lineCount; i++) {
var itemId = salesOrder.getSublistValue({
sublistId: 'item',
fieldId: 'item',
line: i
});
var qty = salesOrder.getSublistValue({
sublistId: 'item',
fieldId: 'quantity',
line: i
});
log.debug('Line ' + i, 'Item: ' + itemId + ', Qty: ' + qty);
}
Adding a New Sublist Line
var rec = record.load({
type: record.Type.SALES_ORDER,
id: 789,
isDynamic: true
});
rec.selectNewLine({ sublistId: 'item' });
rec.setCurrentSublistValue({ sublistId: 'item', fieldId: 'item', value: 55 });
rec.setCurrentSublistValue({ sublistId: 'item', fieldId: 'quantity', value: 10 });
rec.setCurrentSublistValue({ sublistId: 'item', fieldId: 'price', value: -1 });
rec.commitLine({ sublistId: 'item' });
rec.save();
Note that selectNewLine(), setCurrentSublistValue(), and commitLine() are the dynamic-mode methods for working with sublists. In standard mode, you’d use setSublistValue() with a line index directly, but for adding brand new lines, dynamic mode is typically the cleaner approach.
Deleting a Record
Deleting is the simplest operation β you just need the type and internal ID. Be careful, because in most cases this action cannot be undone:
record.delete({
type: record.Type.CUSTOMER,
id: 123
});
NetSuite will throw an error if the record can’t be deleted β for example, if it has dependent transactions. Always handle this gracefully in production scripts.
Copying a Record
Sometimes you need to duplicate a record as a starting point β for example, copying a template sales order or duplicating an item. record.copy() handles this cleanly:
var copiedRecord = record.copy({
type: record.Type.SALES_ORDER,
id: 100
});
copiedRecord.setValue({ fieldId: 'memo', value: 'Copied from SO #100' });
var newId = copiedRecord.save();
log.debug('New record created', newId);
Common Record Types
The record.Type enum covers most standard NetSuite record types. Here are some of the most commonly used ones you’ll encounter in day-to-day development:
| Record Name | record.Type Value |
|---|---|
| Customer | record.Type.CUSTOMER |
| Vendor | record.Type.VENDOR |
| Sales Order | record.Type.SALES_ORDER |
| Purchase Order | record.Type.PURCHASE_ORDER |
| Invoice | record.Type.INVOICE |
| Vendor Bill | record.Type.VENDOR_BILL |
| Inventory Item | record.Type.INVENTORY_ITEM |
| Journal Entry | record.Type.JOURNAL_ENTRY |
| Employee | record.Type.EMPLOYEE |
| Expense Report | record.Type.EXPENSE_REPORT |
Governance and Performance Tips
Working with N/record in bulk scripts can quickly run into governance limits. Here are some practical tips to keep your scripts running efficiently:
- Use submitFields() when you can. It is far more governance-efficient than a full load/save cycle for simple field updates.
- Avoid loading records inside loops. Structure your script so you search for the data you need first, then load only records that actually require changes.
- Use Map/Reduce scripts for large datasets. The Map/Reduce script type is designed for processing large record sets with automatic batching and retry logic.
- Be mindful of dynamic mode costs. Dynamic mode triggers more system processes, which means more governance usage. Only use it when you actually need field-change sourcing to fire.
Handling Errors Gracefully
Record operations can fail for all kinds of reasons β permissions issues, missing required fields, concurrent modifications, or the record simply not existing. Always wrap your operations in try/catch blocks in production code:
try {
var rec = record.load({
type: record.Type.SALES_ORDER,
id: someId
});
rec.setValue({ fieldId: 'memo', value: 'Updated by automation' });
rec.save();
} catch (e) {
log.error({
title: 'Failed to update record ' + someId,
details: e.message
});
}
Wrapping Up
The N/record module is genuinely the heart of SuiteScript development. Once you get comfortable with the create/load/save pattern and understand when to reach for dynamic mode vs. standard mode, a huge amount of NetSuite automation becomes accessible to you.
If I had to leave you with one piece of practical advice: start simple. Get comfortable with record.load() and getValue() first, then layer in sublist manipulation once the basics click. And whenever you’re dealing with large volumes of records, always think about governance β a script that works perfectly in a sandbox with 10 records can easily hit limits in production with 10,000.
Got questions about specific record types or sublist patterns? Drop them in the community forum β we’d love to help you work through it.
Discover more from The NetSuite Pro
Subscribe to get the latest posts sent to your email.
Leave a Reply