If there’s one SuiteScript module you’ll use in practically every project you ever touch, it’s N/search. Want to find all open sales orders over a certain amount? N/search. Need to pull a list of customers with outstanding balances before running a script? N/search. Building a Suitelet that renders a filtered list of inventory items? Still N/search.
And yet, despite how fundamental it is, the N/search module trips up developers constantly β especially when it comes to filtering logic, column types, and iteration patterns. This guide covers everything you need to use it confidently, from the basics through to performance patterns that matter in production.
What Is the N/search Module?
The N/search module is NetSuite’s programmatic equivalent of Saved Searches. It lets you query virtually any record type in the system β customers, transactions, employees, inventory items, custom records β and return structured result sets that you can iterate over, aggregate, or act on.
You access it in SuiteScript 2.x by requiring the module at the top of your script:
/**
* @NApiVersion 2.1
* @NScriptType ScheduledScript
*/
define(['N/search'], (search) => {
const execute = (context) => {
// your search logic here
};
return { execute };
});
Creating a Search: The Two Main Approaches
There are two ways to run a search in SuiteScript: you can create one from scratch using search.create(), or you can load an existing Saved Search by its ID using search.load().
Loading a saved search is the simpler option and is great when the search was already configured by a user or another developer. Creating one programmatically gives you full dynamic control β useful when the filters or columns need to change based on runtime data.
Loading a Saved Search
const mySearch = search.load({
id: 'customsearch_open_invoices'
});
mySearch.run().each((result) => {
log.debug('Invoice ID', result.id);
return true; // return true to continue iteration
});
Creating a Search Programmatically
const invoiceSearch = search.create({
type: search.Type.INVOICE,
filters: [
['status', search.Operator.ANYOF, 'CustInvc:A'], // Open invoices
'AND',
['amountremaining', search.Operator.GREATERTHAN, '1000']
],
columns: [
search.createColumn({ name: 'tranid' }),
search.createColumn({ name: 'entity', label: 'Customer' }),
search.createColumn({ name: 'amountremaining' }),
search.createColumn({ name: 'duedate', sort: search.Sort.ASC })
]
});
invoiceSearch.run().each((result) => {
const invoiceNum = result.getValue('tranid');
const customer = result.getText('entity');
const balance = result.getValue('amountremaining');
log.debug('Invoice', `${invoiceNum} | ${customer} | ${balance}`);
return true;
});
Notice the difference between getValue() and getText(). For fields that store internal IDs (like list fields, select fields, or entity references), getValue() returns the internal ID and getText() returns the human-readable label. Using the wrong one is one of the most common N/search bugs.
Understanding Filters
Filters can be written in two styles: the array expression style (shown above) or the FilterExpression object style. The array style is more readable and supports complex AND/OR logic natively. The object style uses search.createFilter() and is more verbose.
For compound logic, the array style uses string connectors:
filters: [
['subsidiary', search.Operator.ANYOF, '1'],
'AND',
[
['status', search.Operator.ANYOF, 'CustInvc:A'],
'OR',
['status', search.Operator.ANYOF, 'CustInvc:B']
]
]
When filtering on joined record fields (e.g., a field from the customer record on a transaction search), use the join syntax:
search.createColumn({ name: 'email', join: 'customer' })
Iterating Over Results: .each() vs. getRange()
The .run().each() pattern is the standard way to iterate, but it has a hard limit of 4,000 results. For most use cases that’s fine. But if you’re working with large datasets β bulk exports, full account-wide reports β you’ll need to page through results using getRange().
const mySearch = search.create({ type: 'customer', filters: [], columns: ['internalid', 'companyname'] });
const searchResults = mySearch.run();
let start = 0;
const pageSize = 1000;
let resultSlice;
do {
resultSlice = searchResults.getRange({ start, end: start + pageSize });
resultSlice.forEach((result) => {
log.debug('Customer', result.getValue('companyname'));
});
start += pageSize;
} while (resultSlice.length === pageSize);
Alternatively, for very large result sets you can use search.runPaged(), which provides a built-in paging interface and is more governance-friendly than manually slicing with getRange().
const pagedSearch = search.create({
type: search.Type.CUSTOMER,
columns: ['internalid', 'companyname']
}).runPaged({ pageSize: 1000 });
pagedSearch.pageRanges.forEach((pageRange) => {
const page = pagedSearch.fetch({ index: pageRange.index });
page.data.forEach((result) => {
log.debug('Customer', result.getValue('companyname'));
});
});
Searching Custom Records
Custom record types are fully searchable. Use the customrecord_ prefix followed by your record’s script ID:
const customSearch = search.create({
type: 'customrecord_project_log',
filters: [
['custrecord_status', search.Operator.ANYOF, '1'] // Active
],
columns: [
search.createColumn({ name: 'name' }),
search.createColumn({ name: 'custrecord_assigned_to' }),
search.createColumn({ name: 'custrecord_due_date' })
]
});
customSearch.run().each((result) => {
log.debug('Project Log', result.getValue('name'));
return true;
});
Governance and Performance Tips
Each search.create() or search.load() call costs governance units, and the iteration itself costs more as the result count grows. Here are the patterns that matter most in production:
Only request the columns you need. Pulling ten columns when you only need two wastes both governance and memory. Be explicit about your column definitions.
Push as much logic into the filters as possible. It’s tempting to pull everything and filter in JavaScript, but every extra record you retrieve costs governance. Let the search engine do the heavy lifting.
Cache saved search results when appropriate. If you’re loading the same saved search repeatedly in a single script execution, load it once, store the results in a variable, and reuse them rather than running the search multiple times.
Use Map/Reduce scripts for large datasets. If your search reliably returns thousands of records and you need to process each one, a Map/Reduce script is far better suited than a Scheduled Script. The map stage distributes work across multiple queues and dramatically increases your throughput ceiling.
A Note on Summary Searches
N/search also supports summary-type columns β the equivalent of GROUP BY and aggregate functions in SQL. You can use summary: search.Summary.SUM, COUNT, MAX, MIN, or GROUP on your columns to return aggregated data without iterating every raw record:
const summarySearch = search.create({
type: search.Type.TRANSACTION,
filters: [['type', search.Operator.ANYOF, 'CustInvc']],
columns: [
search.createColumn({ name: 'entity', summary: search.Summary.GROUP }),
search.createColumn({ name: 'amount', summary: search.Summary.SUM })
]
});
summarySearch.run().each((result) => {
const customer = result.getText({ name: 'entity', summary: search.Summary.GROUP });
const total = result.getValue({ name: 'amount', summary: search.Summary.SUM });
log.debug(`${customer}`, `Total: ${total}`);
return true;
});
Summary searches can be a significant performance win when you need totals or counts β they return far fewer rows than iterating every raw record and doing the math yourself.
Common Mistakes to Avoid
The most frequent N/search mistakes I see in production codebases are: forgetting to return true inside .each() (which stops iteration after the first result), using getValue() on a text field when you needed getText(), and ignoring the 4,000-result cap on .each() leading to silent data loss on larger accounts.
Another subtle one: filter operators are case-sensitive in some contexts, and using the wrong operator type for a field (e.g., using IS on a multi-select field instead of ANYOF) will return no results without throwing an error. When your search comes back empty and you’re sure there should be data, your filters are the first place to check.
The N/search module rewards the time you invest in understanding it. Once it clicks β filters, joins, summaries, paging β you’ll find yourself reaching for it constantly. If there’s a specific search pattern you’re struggling with, drop it in the comments and I’ll do my best to help.
Discover more from The NetSuite Pro
Subscribe to get the latest posts sent to your email.
Leave a Reply