The N/search module is the backbone of data access in SuiteScript. Whether you need to pull a single field from a customer record or aggregate thousands of transactions, the Search API gives you programmatic control over NetSuite's powerful search engine. This tutorial walks through everything from basic searches to paginated result sets and SuiteQL queries.
Overview of the N/search Module
The N/search module provides methods to create, load, run, and manage saved searches entirely in code. It mirrors the functionality of the Saved Search UI but gives you dynamic control over filters, columns, and result processing.
Core capabilities include:
- Creating searches on the fly with
search.create() - Loading existing saved searches with
search.load() - Quick field lookups with
search.lookupFields() - Paginated result handling with
runPaged()for large data sets - Global search with
search.global()for keyword-based lookups
Here is the basic module setup you will use throughout this tutorial:
/**
* @NApiVersion 2.1
* @NScriptType Suitelet
*/
define(['N/search', 'N/log'], (search, log) => {
const onRequest = (context) => {
// Search logic goes here
};
return { onRequest };
});Creating Searches Programmatically
Use search.create() to build a search from scratch. You specify the record type, filters, and columns:
const customerSearch = search.create({
type: search.Type.CUSTOMER,
filters: [
['isinactive', 'is', 'F'],
'AND',
['balance', 'greaterthan', 0]
],
columns: [
search.createColumn({ name: 'entityid' }),
search.createColumn({ name: 'companyname' }),
search.createColumn({ name: 'email' }),
search.createColumn({ name: 'balance', sort: search.Sort.DESC })
]
});The type parameter accepts a search.Type enum value or a string record ID (for custom records, use 'customrecord_your_record'). Filters define your criteria, and columns define the fields you want returned.
Saving a Programmatic Search
You can persist a search you created in code so it appears in the Saved Searches list:
const mySearch = search.create({
type: search.Type.SALES_ORDER,
title: 'Open Sales Orders - Script Generated',
filters: [
['mainline', 'is', 'T'],
'AND',
['status', 'anyof', 'SalesOrd:B', 'SalesOrd:D']
],
columns: ['tranid', 'entity', 'trandate', 'amount']
});
const searchId = mySearch.save();
log.audit('Search Saved', `ID: ${searchId}`);The save() method returns the internal ID of the new saved search. This is useful when you want to generate a search once and then load it repeatedly.
Loading Existing Saved Searches
If you already have a saved search built in the UI, load it by ID:
const existingSearch = search.load({
id: 'customsearch_open_invoices'
});
// You can modify filters after loading
existingSearch.filters.push(
search.createFilter({
name: 'trandate',
operator: search.Operator.WITHIN,
values: ['lastmonth']
})
);
// Add additional columns
existingSearch.columns.push(
search.createColumn({ name: 'memo' })
);Loading a saved search is often the best approach when business users maintain the search criteria in the UI and your script just needs to consume the results. You can also load by numeric internal ID:
const searchById = search.load({ id: 456 });Filters and Operators
Filters define the criteria for your search. Each filter needs a field name, an operator, and one or more values.
Creating Filters
You can define filters inline as arrays or use search.createFilter():
// Inline array syntax (commonly used in search.create)
const filters = [
['trandate', 'within', 'thismonth'],
'AND',
['amount', 'greaterthan', 1000],
'AND',
['entity', 'noneof', 123, 456]
];
// Object syntax with search.createFilter
const dateFilter = search.createFilter({
name: 'trandate',
operator: search.Operator.WITHIN,
values: ['lastquarter']
});
const statusFilter = search.createFilter({
name: 'status',
operator: search.Operator.ANYOF,
values: ['SalesOrd:B', 'SalesOrd:D', 'SalesOrd:E']
});Common Operators
Here are the operators you will use most often:
| Operator | Usage | Example Values |
|---|---|---|
is / isnot | Exact match | 'John Smith' |
contains / doesnotcontain | Partial text match | 'widget' |
startswith | Begins with text | 'SO-' |
anyof / noneof | Multi-select list match | 'SalesOrd:B', 'SalesOrd:D' |
greaterthan / lessthan | Numeric comparison | 1000 |
greaterthanorequalto / lessthanorequalto | Inclusive comparison | 500 |
within | Date range | 'lastmonth', or two dates |
on / before / after | Single date comparison | '1/15/2026' |
isempty / isnotempty | Null checks | (no value needed) |
haskeywords | Full-text search | 'urgent delivery' |
Join Filters
Access fields on related records using the join parameter:
// Filter sales orders where the customer's category is 'Wholesale'
const joinFilter = search.createFilter({
name: 'category',
join: 'customer',
operator: search.Operator.ANYOF,
values: [3] // Internal ID of 'Wholesale' category
});
// Filter where the item's type is 'Inventory Item'
const itemFilter = search.createFilter({
name: 'type',
join: 'item',
operator: search.Operator.ANYOF,
values: ['InvtPart']
});Filter Expressions with AND/OR Logic
For complex criteria, use filter expressions. These are nested arrays that support AND/OR grouping:
const complexSearch = search.create({
type: search.Type.SALES_ORDER,
filters: [
['mainline', 'is', 'T'],
'AND',
[
['status', 'anyof', 'SalesOrd:B'],
'OR',
[
['status', 'anyof', 'SalesOrd:D'],
'AND',
['amount', 'greaterthan', 5000]
]
],
'AND',
['trandate', 'within', 'thisyear']
],
columns: ['tranid', 'entity', 'status', 'amount']
});This translates to: mainline is true AND (status is Pending Fulfillment OR (status is Partially Fulfilled AND amount > 5000)) AND transaction date is this year.
The nesting rules are straightforward. Each group is wrapped in square brackets, and AND/OR strings separate the conditions at each level. You can nest as deep as needed, but keep readability in mind.
Formula Filters
You can also use formulas in filters for advanced logic:
const formulaFilter = search.createFilter({
name: 'formulanumeric',
operator: search.Operator.GREATERTHAN,
values: [30],
formula: 'TRUNC(SYSDATE) - TRUNC({trandate})'
});This filter returns only records where the transaction date is more than 30 days ago.
Columns and Summary Types
Columns define which fields appear in your results. Use search.createColumn() for full control:
const columns = [
search.createColumn({ name: 'tranid', label: 'Order Number' }),
search.createColumn({ name: 'entity', label: 'Customer' }),
search.createColumn({ name: 'trandate', label: 'Date' }),
search.createColumn({
name: 'amount',
label: 'Amount',
sort: search.Sort.DESC
}),
// Join column: get customer's email
search.createColumn({
name: 'email',
join: 'customer',
label: 'Customer Email'
}),
// Formula column
search.createColumn({
name: 'formulatext',
formula: "CASE WHEN {amount} > 5000 THEN 'High Value' ELSE 'Standard' END",
label: 'Order Tier'
})
];Summary (Aggregate) Columns
For grouped searches, specify a summary type on each column:
const summarySearch = search.create({
type: search.Type.SALES_ORDER,
filters: [
['mainline', 'is', 'T'],
'AND',
['trandate', 'within', 'thisyear']
],
columns: [
search.createColumn({
name: 'entity',
summary: search.Summary.GROUP,
label: 'Customer'
}),
search.createColumn({
name: 'amount',
summary: search.Summary.SUM,
label: 'Total Sales'
}),
search.createColumn({
name: 'internalid',
summary: search.Summary.COUNT,
label: 'Order Count'
}),
search.createColumn({
name: 'amount',
summary: search.Summary.AVG,
label: 'Avg Order Value'
}),
search.createColumn({
name: 'trandate',
summary: search.Summary.MAX,
label: 'Last Order Date'
})
]
});Available summary types: GROUP, SUM, COUNT, AVG, MIN, MAX.
When using summary columns, every non-aggregated column must have summary: search.Summary.GROUP. This works exactly like SQL's GROUP BY clause.
Running Searches and Processing Results
The run() Method
The simplest way to execute a search is with run(), which returns a ResultSet object. You then call each() to iterate through results:
const invoiceSearch = search.create({
type: search.Type.INVOICE,
filters: [
['status', 'anyof', 'CustInvc:A'],
'AND',
['mainline', 'is', 'T']
],
columns: ['tranid', 'entity', 'amount', 'duedate']
});
const results = [];
invoiceSearch.run().each((result) => {
results.push({
id: result.id,
tranId: result.getValue({ name: 'tranid' }),
customerName: result.getText({ name: 'entity' }),
customerId: result.getValue({ name: 'entity' }),
amount: parseFloat(result.getValue({ name: 'amount' })),
dueDate: result.getValue({ name: 'duedate' })
});
return true; // Return true to continue iterating
});
log.audit('Open Invoices', `Found ${results.length} invoices`);Key points about run().each():
- Return
truefrom the callback to continue to the next result. Returnfalse(or nothing) to stop. - Maximum 4,000 results can be retrieved this way.
getValue()returns the raw internal value.getText()returns the display text (useful for list/record fields).
Getting Results as an Array with getRange()
For smaller result sets, getRange() gives you a slice of results:
const resultSet = invoiceSearch.run();
// Get first 100 results
const first100 = resultSet.getRange({ start: 0, end: 100 });
first100.forEach((result) => {
log.debug('Invoice', result.getValue({ name: 'tranid' }));
});
// Get next 100
const next100 = resultSet.getRange({ start: 100, end: 200 });The getRange() method is limited to 1,000 results per call. The start is inclusive and end is exclusive.
Pagination with runPaged()
For large datasets, runPaged() is the right approach. It processes results in pages without hitting the 4,000-result ceiling of run().each():
const pagedSearch = search.create({
type: search.Type.TRANSACTION,
filters: [
['type', 'anyof', 'SalesOrd'],
'AND',
['mainline', 'is', 'T'],
'AND',
['trandate', 'within', 'thisyear']
],
columns: ['tranid', 'entity', 'amount', 'trandate', 'status']
});
const pagedData = pagedSearch.runPaged({ pageSize: 1000 });
log.audit('Search Results', `Total count: ${pagedData.count}`);
const allResults = [];
pagedData.pageRanges.forEach((pageRange) => {
const page = pagedData.fetch({ index: pageRange.index });
page.data.forEach((result) => {
allResults.push({
id: result.id,
tranId: result.getValue({ name: 'tranid' }),
customer: result.getText({ name: 'entity' }),
amount: result.getValue({ name: 'amount' }),
date: result.getValue({ name: 'trandate' }),
status: result.getText({ name: 'status' })
});
});
});
log.audit('Processed', `${allResults.length} total results across ${pagedData.pageRanges.length} pages`);How runPaged() Works
runPaged()executes the search and returns aPagedDataobject with the total count and page ranges.pageSizecan be between 5 and 1,000 (default is 50).pagedData.countgives the total number of results without fetching them all.pagedData.pageRangesis an array of page range objects. Each has anindexproperty.pagedData.fetch({ index })retrieves a specific page of results.
This approach lets you handle tens of thousands of results. Each fetch() call costs governance, so plan your page size accordingly.
Selective Page Processing
You do not always need every page. For example, to get just the count and first page:
const pagedData = mySearch.runPaged({ pageSize: 50 });
const totalResults = pagedData.count;
if (totalResults > 0) {
const firstPage = pagedData.fetch({ index: 0 });
firstPage.data.forEach((result) => {
// Process only the first page
});
}Search Lookups for Quick Field Access
When you need a few fields from a single record, search.lookupFields() is more efficient than loading the entire record:
// Look up customer fields by internal ID
const customerData = search.lookupFields({
type: search.Type.CUSTOMER,
id: 12345,
columns: ['companyname', 'email', 'balance', 'currency', 'salesrep']
});
log.debug('Customer', {
name: customerData.companyname,
email: customerData.email,
balance: customerData.balance,
currency: customerData.currency[0].text, // List fields return arrays
salesRep: customerData.salesrep[0].value
});Important details about lookupFields():
- Text fields return a simple string value.
- List/Record fields return an array of objects with
value(internal ID) andtext(display name). - Checkbox fields return a boolean.
- Costs 1 governance unit compared to 5 or 10 for
record.load().
This is ideal for situations where you need to read a handful of fields in a User Event or Scheduled script without the overhead of loading the full record.
// Common pattern: check a field value before proceeding
const soData = search.lookupFields({
type: search.Type.SALES_ORDER,
id: salesOrderId,
columns: ['status', 'custbody_approved']
});
if (soData.status[0].value === 'pendingFulfillment' && soData.custbody_approved) {
// Proceed with fulfillment logic
}SuiteQL: Writing SQL Queries in NetSuite
SuiteQL lets you write standard SQL queries against the NetSuite database. It uses the N/query module rather than N/search, but it is an essential tool for data access and pairs well with the Search API.
/**
* @NApiVersion 2.1
* @NScriptType Suitelet
*/
define(['N/query', 'N/log'], (query, log) => {
const onRequest = (context) => {
// Basic SuiteQL query
const results = query.runSuiteQL({
query: `
SELECT
Transaction.tranid,
Transaction.trandate,
Transaction.foreigntotal,
BUILTIN.DF(Transaction.entity) AS customer_name,
BUILTIN.DF(Transaction.status) AS status_text
FROM
Transaction
WHERE
Transaction.type = 'SalesOrd'
AND Transaction.trandate >= TO_DATE('2026-01-01', 'YYYY-MM-DD')
ORDER BY
Transaction.foreigntotal DESC
`
});
// Process results
const mappedResults = results.asMappedResults();
mappedResults.forEach((row) => {
log.debug('Order', {
tranId: row.tranid,
date: row.trandate,
total: row.foreigntotal,
customer: row.customer_name,
status: row.status_text
});
});
};
return { onRequest };
});Key SuiteQL Concepts
BUILTIN.DF(field)returns the display value of a list/record field (likegetText()in N/search).- Table names map to NetSuite record types:
Transaction,Customer,Item,Employee, etc. - Use
TO_DATE()for date comparisons to avoid format issues. - Parameterized queries prevent injection and improve readability:
const results = query.runSuiteQL({
query: `
SELECT id, companyname, email, balance
FROM Customer
WHERE balance > ?
AND isinactive = ?
`,
params: [5000, 'F']
});SuiteQL Pagination
For large SuiteQL results, use runSuiteQLPaged():
const pagedResults = query.runSuiteQLPaged({
query: `
SELECT id, tranid, foreigntotal
FROM Transaction
WHERE type = 'SalesOrd'
ORDER BY trandate DESC
`,
pageSize: 1000
});
log.audit('Total Results', pagedResults.count);
pagedResults.pageRanges.forEach((pageRange) => {
const page = pagedResults.fetch(pageRange.index);
const rows = page.data.asMappedResults();
rows.forEach((row) => {
// Process each row
});
});Practical Examples
Finding Orders by Criteria with Joins
This example finds all sales orders for customers in a specific territory, including item-level details:
/**
* Find sales orders with customer and item joins
*/
const findOrdersByTerritory = (territoryId) => {
const orderSearch = search.create({
type: search.Type.SALES_ORDER,
filters: [
['customer.territory', 'anyof', territoryId],
'AND',
['mainline', 'is', 'F'],
'AND',
['taxline', 'is', 'F'],
'AND',
['shipping', 'is', 'F'],
'AND',
['status', 'anyof', 'SalesOrd:B', 'SalesOrd:D'],
'AND',
['trandate', 'within', 'thisquarter']
],
columns: [
search.createColumn({ name: 'tranid' }),
search.createColumn({ name: 'trandate' }),
search.createColumn({ name: 'entity' }),
search.createColumn({ name: 'companyname', join: 'customer' }),
search.createColumn({ name: 'territory', join: 'customer' }),
search.createColumn({ name: 'item' }),
search.createColumn({ name: 'quantity' }),
search.createColumn({ name: 'rate' }),
search.createColumn({ name: 'amount' }),
search.createColumn({ name: 'quantityshiprecv' })
]
});
const orders = [];
orderSearch.run().each((result) => {
orders.push({
orderId: result.id,
tranId: result.getValue({ name: 'tranid' }),
date: result.getValue({ name: 'trandate' }),
customer: result.getText({ name: 'entity' }),
company: result.getValue({ name: 'companyname', join: 'customer' }),
territory: result.getText({ name: 'territory', join: 'customer' }),
item: result.getText({ name: 'item' }),
quantity: parseInt(result.getValue({ name: 'quantity' })),
rate: parseFloat(result.getValue({ name: 'rate' })),
amount: parseFloat(result.getValue({ name: 'amount' })),
shipped: parseInt(result.getValue({ name: 'quantityshiprecv' }) || 0)
});
return true;
});
return orders;
};Aggregating Sales Data
Build a summary report with grouped results:
/**
* Monthly sales summary by sales rep
*/
const getMonthlySalesSummary = () => {
const summarySearch = search.create({
type: search.Type.SALES_ORDER,
filters: [
['mainline', 'is', 'T'],
'AND',
['trandate', 'within', 'thisyear'],
'AND',
['status', 'noneof', 'SalesOrd:C'] // Exclude cancelled
],
columns: [
search.createColumn({
name: 'salesrep',
summary: search.Summary.GROUP
}),
search.createColumn({
name: 'formulatext',
summary: search.Summary.GROUP,
formula: "TO_CHAR({trandate}, 'YYYY-MM')",
label: 'Month'
}),
search.createColumn({
name: 'amount',
summary: search.Summary.SUM,
sort: search.Sort.DESC
}),
search.createColumn({
name: 'internalid',
summary: search.Summary.COUNT,
label: 'Order Count'
}),
search.createColumn({
name: 'amount',
summary: search.Summary.AVG,
label: 'Avg Order'
})
]
});
const summary = [];
summarySearch.run().each((result) => {
summary.push({
salesRep: result.getText({
name: 'salesrep',
summary: search.Summary.GROUP
}),
month: result.getValue({
name: 'formulatext',
summary: search.Summary.GROUP
}),
totalSales: parseFloat(result.getValue({
name: 'amount',
summary: search.Summary.SUM
})),
orderCount: parseInt(result.getValue({
name: 'internalid',
summary: search.Summary.COUNT
})),
avgOrder: parseFloat(result.getValue({
name: 'amount',
summary: search.Summary.AVG
}))
});
return true;
});
return summary;
};SuiteQL for Complex Multi-Table Queries
SuiteQL shines when you need joins that are awkward or impossible with the Search API:
/**
* SuiteQL: Customer purchase analysis with item categories
*/
const getCustomerPurchaseAnalysis = () => {
const results = query.runSuiteQL({
query: `
SELECT
c.id AS customer_id,
c.companyname,
BUILTIN.DF(c.territory) AS territory,
ic.name AS item_category,
COUNT(DISTINCT tl.transaction) AS order_count,
SUM(tl.netamount) AS total_spend,
MIN(t.trandate) AS first_order,
MAX(t.trandate) AS last_order,
ROUND(SUM(tl.netamount) / COUNT(DISTINCT tl.transaction), 2) AS avg_order_value
FROM
Customer c
INNER JOIN Transaction t ON t.entity = c.id
INNER JOIN TransactionLine tl ON tl.transaction = t.id
INNER JOIN Item i ON i.id = tl.item
LEFT JOIN ItemCategory ic ON ic.id = i.category
WHERE
t.type = 'SalesOrd'
AND tl.mainline = 'F'
AND tl.taxline = 'F'
AND t.trandate >= ADD_MONTHS(SYSDATE, -12)
GROUP BY
c.id, c.companyname, BUILTIN.DF(c.territory), ic.name
HAVING
SUM(tl.netamount) > 1000
ORDER BY
total_spend DESC
`
});
return results.asMappedResults();
};
/**
* SuiteQL: Inventory aging report
*/
const getInventoryAging = () => {
const results = query.runSuiteQL({
query: `
SELECT
i.itemid,
i.displayname,
BUILTIN.DF(i.location) AS warehouse,
i.quantityonhand,
i.quantityavailable,
i.quantityonorder,
CASE
WHEN i.lastpurchasedate IS NULL THEN 'No Purchase History'
WHEN TRUNC(SYSDATE) - TRUNC(i.lastpurchasedate) > 180 THEN 'Over 180 Days'
WHEN TRUNC(SYSDATE) - TRUNC(i.lastpurchasedate) > 90 THEN '91-180 Days'
WHEN TRUNC(SYSDATE) - TRUNC(i.lastpurchasedate) > 30 THEN '31-90 Days'
ELSE 'Current (0-30 Days)'
END AS aging_bucket,
i.averagecost,
ROUND(i.quantityonhand * i.averagecost, 2) AS inventory_value
FROM
Item i
WHERE
i.type = 'InvtPart'
AND i.isinactive = 'F'
AND i.quantityonhand > 0
ORDER BY
inventory_value DESC
`
});
return results.asMappedResults();
};Governance Costs and Optimization
Every search operation consumes governance units. Understanding these costs is critical for scripts that run close to their limits.
| Operation | Governance Cost |
|---|---|
search.create() | 0 units (definition only) |
search.load() | 5 units |
search.lookupFields() | 1 unit |
ResultSet.each() callback | 10 units per page of results |
runPaged() | 5 units |
PagedData.fetch() | 5 units per page |
query.runSuiteQL() | 10 units |
Optimization Strategies
1. Use lookupFields for single-record reads
// Bad: 10 governance units to load a record
const rec = record.load({ type: 'customer', id: 12345 });
const name = rec.getValue({ fieldId: 'companyname' });
// Good: 1 governance unit for a lookup
const data = search.lookupFields({
type: 'customer',
id: 12345,
columns: ['companyname']
});
const name = data.companyname;2. Filter aggressively to reduce result counts
// Bad: fetch all transactions then filter in code
const allTransactions = search.create({
type: search.Type.TRANSACTION,
filters: [['mainline', 'is', 'T']],
columns: ['tranid', 'amount', 'trandate']
});
// Good: let NetSuite do the filtering
const targetedSearch = search.create({
type: search.Type.TRANSACTION,
filters: [
['mainline', 'is', 'T'],
'AND',
['type', 'anyof', 'SalesOrd'],
'AND',
['trandate', 'within', 'thismonth'],
'AND',
['amount', 'greaterthan', 500]
],
columns: ['tranid', 'amount', 'trandate']
});3. Choose the right page size with runPaged
Larger page sizes mean fewer fetch() calls and less governance consumed. For bulk processing, use pageSize: 1000:
// Fewer pages = fewer governance units
const pagedData = mySearch.runPaged({ pageSize: 1000 });
// 5,000 results = 5 pages = 25 governance units (5 + 5 per fetch)
// vs pageSize: 50 = 100 pages = 505 governance units4. Use SuiteQL for complex aggregations
When you need multi-level grouping, subqueries, or window functions, SuiteQL is often cheaper than running multiple N/search calls:
// One SuiteQL call (10 units) instead of multiple searches
const results = query.runSuiteQL({
query: `
SELECT entity, SUM(foreigntotal) AS total,
COUNT(*) AS order_count,
RANK() OVER (ORDER BY SUM(foreigntotal) DESC) AS rank
FROM Transaction
WHERE type = 'SalesOrd' AND trandate >= TO_DATE('2026-01-01', 'YYYY-MM-DD')
GROUP BY entity
ORDER BY total DESC
FETCH FIRST 20 ROWS ONLY
`
});Common Pitfalls
The 1,000-Result Limit with getRange()
getRange() returns a maximum of 1,000 results per call. If you try to retrieve more, NetSuite silently caps the result set:
// This will only return 1,000 results even if more exist
const results = mySearch.run().getRange({ start: 0, end: 5000 });
// results.length will be 1000, not 5000To get all results beyond 1,000, use run().each() (up to 4,000) or runPaged() (unlimited):
// Correct approach for large result sets
const pagedData = mySearch.runPaged({ pageSize: 1000 });
let allResults = [];
pagedData.pageRanges.forEach((pageRange) => {
const page = pagedData.fetch({ index: pageRange.index });
page.data.forEach((result) => {
allResults.push(result);
});
});Forgetting to Return true in each()
The each() callback must return true to continue iterating. Forgetting this means you only process the first result:
// Bug: only processes one result
mySearch.run().each((result) => {
processResult(result);
// Missing return true!
});
// Correct
mySearch.run().each((result) => {
processResult(result);
return true;
});Summary Column Value Retrieval
When accessing values from summary searches, you must include the summary parameter in getValue() and getText():
// Wrong: returns null or throws an error
const total = result.getValue({ name: 'amount' });
// Correct: include the summary type
const total = result.getValue({
name: 'amount',
summary: search.Summary.SUM
});Governance Exhaustion in Loops
Running searches inside loops is a common mistake that quickly exhausts governance:
// Bad: search inside a loop
customers.forEach((customerId) => {
const orders = search.create({
type: search.Type.SALES_ORDER,
filters: [['entity', 'is', customerId]]
});
orders.run().each((r) => { /* ... */ return true; });
});
// Good: single search with OR filter for all customers
const allOrders = search.create({
type: search.Type.SALES_ORDER,
filters: [
['entity', 'anyof', ...customerIds],
'AND',
['mainline', 'is', 'T']
],
columns: ['entity', 'tranid', 'amount']
});Handling Empty Results
Always check for results before processing to avoid errors:
const pagedData = mySearch.runPaged({ pageSize: 100 });
if (pagedData.count === 0) {
log.debug('No Results', 'Search returned no matching records');
return;
}
// Safe to process results
pagedData.pageRanges.forEach((pageRange) => {
// ...
});Next Steps
Now that you can query data programmatically, explore these related topics:
- Saved Search Formulas for UI-based formula techniques
- Map/Reduce Scripts for processing search results at scale
- Custom Portlets for displaying search data on dashboards
Need help building complex data queries in NetSuite? Contact our development team for expert assistance.