Skip to main content

Saved Searches

Saved Searches are pre-configured queries that can be reused in reports, dashboards, scripts, and other NetSuite features.


When to Use Saved Searches

Use CaseExample
ReportsMonthly sales by region
DashboardsReal-time KPI displays
PortletsCustomer-facing data
ScriptsData source for automation
FormsDropdown field sources
WorkflowsCondition evaluation

Saved Search Architecture

┌─────────────────────────────────────────────────────────────────────────────┐
│ SAVED SEARCH STRUCTURE │
└─────────────────────────────────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────────────────────┐
│ SAVED SEARCH DEFINITION │
│ customsearch_pending_approvals │
└──────────────────────────────┬───────────────────────────────────┘

┌─────────────────────┼─────────────────────┐
│ │ │
▼ ▼ ▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ FILTERS │ │ COLUMNS │ │ SETTINGS │
│ (Criteria) │ │ (Results) │ │ (Options) │
└────────┬────────┘ └────────┬────────┘ └────────┬────────┘
│ │ │
▼ ▼ ▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ • Status = Pend │ │ • Transaction # │ │ • Public │
│ • Date > Today │ │ • Customer │ │ • Audience │
│ • Amount > 1000 │ │ • Amount │ │ • Update freq │
└─────────────────┘ │ • Date │ │ • Alert │
│ • Approver │ └─────────────────┘
└─────────────────┘

Basic Saved Search XML

<?xml version="1.0" encoding="UTF-8"?>
<savedsearch scriptid="customsearch_pending_approvals">
<definition>
<!-- Search Type -->
<searchtype>Transaction</searchtype>

<!-- Search Title and Access -->
<title>Pending Approvals</title>
<description>All transactions pending approval</description>
<ispublic>T</ispublic>
<isdefaultsearch>F</isdefaultsearch>

<!-- Result Columns -->
<searchcolumns>
<column>
<name>tranid</name>
<label>Document Number</label>
</column>
<column>
<name>trandate</name>
<label>Date</label>
<sort>DESC</sort>
</column>
<column>
<name>entity</name>
<label>Customer/Vendor</label>
</column>
<column>
<name>amount</name>
<label>Amount</label>
</column>
<column>
<name>custbody_approval_status</name>
<label>Approval Status</label>
</column>
</searchcolumns>

<!-- Search Filters -->
<searchfilters>
<filter>
<name>mainline</name>
<operator>is</operator>
<values>T</values>
</filter>
<filter>
<name>custbody_approval_status</name>
<operator>anyof</operator>
<values>1</values> <!-- Pending -->
</filter>
<filter>
<name>trandate</name>
<operator>onorafter</operator>
<values>daysago30</values>
</filter>
</searchfilters>

</definition>

<!-- Role Access -->
<audience>
<allroles>F</allroles>
<audslctrole>
<role>Administrator</role>
</audslctrole>
<audslctrole>
<role>A/P Clerk</role>
</audslctrole>
</audience>

</savedsearch>

Filter Operators

┌─────────────────────────────────────────────────────────────────────────────┐
│ FILTER OPERATORS │
└─────────────────────────────────────────────────────────────────────────────┘

┌────────────────────┬─────────────────────────────────────────────────────────┐
│ TEXT OPERATORS │ │
├────────────────────┼─────────────────────────────────────────────────────────┤
│ is │ Exact match │
│ isnot │ Not equal │
│ contains │ Text contains value │
│ doesnotcontain │ Text does not contain │
│ startswith │ Text starts with │
│ isempty │ Field is empty │
│ isnotempty │ Field has value │
└────────────────────┴─────────────────────────────────────────────────────────┘

┌────────────────────┬─────────────────────────────────────────────────────────┐
│ NUMBER OPERATORS │ │
├────────────────────┼─────────────────────────────────────────────────────────┤
│ equalto │ Equal to value │
│ notequalto │ Not equal to value │
│ lessthan │ Less than value │
│ lessthanorequalto │ Less than or equal │
│ greaterthan │ Greater than value │
│ greaterthanorequalto │ Greater than or equal │
│ between │ Within range (two values) │
│ notbetween │ Outside range │
└────────────────────┴─────────────────────────────────────────────────────────┘

┌────────────────────┬─────────────────────────────────────────────────────────┐
│ DATE OPERATORS │ │
├────────────────────┼─────────────────────────────────────────────────────────┤
│ on │ On specific date │
│ noton │ Not on date │
│ before │ Before date │
│ onorbefore │ On or before date │
│ after │ After date │
│ onorafter │ On or after date │
│ within │ Between two dates │
│ notwithin │ Outside date range │
└────────────────────┴─────────────────────────────────────────────────────────┘

┌────────────────────┬─────────────────────────────────────────────────────────┐
│ SELECT OPERATORS │ │
├────────────────────┼─────────────────────────────────────────────────────────┤
│ anyof │ Any of the listed values │
│ noneof │ None of the listed values │
└────────────────────┴─────────────────────────────────────────────────────────┘

Relative Date Values

┌─────────────────────────────────────────────────────────────────────────────┐
│ RELATIVE DATE VALUES │
└─────────────────────────────────────────────────────────────────────────────┘

today → Current date
yesterday → Previous day
tomorrow → Next day

thisweek → Current week
lastweek → Previous week
nextweek → Following week

thismonth → Current month
lastmonth → Previous month
nextmonth → Following month

thisquarter → Current quarter
lastquarter → Previous quarter
nextquarter → Following quarter

thisyear → Current year
lastyear → Previous year
nextyear → Following year

daysago7 → 7 days ago
daysago30 → 30 days ago
daysago90 → 90 days ago

daysfromnow7 → 7 days from now
daysfromnow30 → 30 days from now

Complete Example: Sales Report

<?xml version="1.0" encoding="UTF-8"?>
<savedsearch scriptid="customsearch_sales_by_region">
<definition>
<searchtype>Transaction</searchtype>
<title>Sales by Region - Monthly</title>
<description>Monthly sales summary grouped by region</description>
<ispublic>T</ispublic>

<!-- Grouping and Summary Columns -->
<searchcolumns>
<column>
<name>custbody_region</name>
<label>Region</label>
<summary>GROUP</summary>
</column>
<column>
<name>salesrep</name>
<label>Sales Rep</label>
<summary>GROUP</summary>
</column>
<column>
<name>internalid</name>
<label>Order Count</label>
<summary>COUNT</summary>
</column>
<column>
<name>amount</name>
<label>Total Sales</label>
<summary>SUM</summary>
</column>
<column>
<name>amount</name>
<label>Average Order</label>
<summary>AVG</summary>
</column>
</searchcolumns>

<searchfilters>
<!-- Transaction Type -->
<filter>
<name>type</name>
<operator>anyof</operator>
<values>SalesOrd</values>
</filter>
<!-- Main line only -->
<filter>
<name>mainline</name>
<operator>is</operator>
<values>T</values>
</filter>
<!-- This month -->
<filter>
<name>trandate</name>
<operator>within</operator>
<values>thismonth</values>
</filter>
<!-- Not cancelled -->
<filter>
<name>status</name>
<operator>noneof</operator>
<values>SalesOrd:C</values>
</filter>
</searchfilters>

</definition>

<audience>
<allroles>F</allroles>
<audslctrole>
<role>Sales Manager</role>
</audslctrole>
<audslctrole>
<role>Administrator</role>
</audslctrole>
</audience>

</savedsearch>

Summary Types

SummaryDescription
GROUPGroup results by this column
COUNTCount of records
SUMSum of numeric values
AVGAverage of numeric values
MINMinimum value
MAXMaximum value

Using Saved Searches in Scripts

const search = require('N/search');

const runSavedSearch = () => {
// Load by script ID
const savedSearch = search.load({
id: 'customsearch_pending_approvals'
});

// Or load by internal ID
// const savedSearch = search.load({ id: 1234 });

const results = [];

savedSearch.run().each((result) => {
results.push({
id: result.id,
tranId: result.getValue('tranid'),
date: result.getValue('trandate'),
entity: result.getText('entity'),
amount: result.getValue('amount'),
status: result.getText('custbody_approval_status')
});

return true; // Continue to next result
});

return results;
};

Modify Saved Search Filters

const search = require('N/search');

const runWithDateFilter = (startDate, endDate) => {
const savedSearch = search.load({
id: 'customsearch_pending_approvals'
});

// Add additional filters
savedSearch.filters.push(
search.createFilter({
name: 'trandate',
operator: search.Operator.WITHIN,
values: [startDate, endDate]
})
);

// Run the modified search
const results = [];

savedSearch.run().each((result) => {
results.push({
id: result.id,
tranId: result.getValue('tranid'),
amount: result.getValue('amount')
});

return true;
});

return results;
};

Get Saved Search ID

// Find saved search by script ID
const findSearchId = (scriptId) => {
const searchSearch = search.create({
type: 'savedsearch',
filters: [
['id', 'is', scriptId]
],
columns: ['internalid']
});

let internalId = null;

searchSearch.run().each((result) => {
internalId = result.getValue('internalid');
return false;
});

return internalId;
};

Search Result Pagination

┌─────────────────────────────────────────────────────────────────────────────┐
│ RESULT PAGINATION │
└─────────────────────────────────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────────────────────┐
│ run().each() │
│ ────────────────────────────────────────────────────────────────│
│ • Processes up to 4,000 results │
│ • Stops when callback returns false │
│ • Most common approach │
└──────────────────────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────────────────────┐
│ run().getRange({ start, end }) │
│ ────────────────────────────────────────────────────────────────│
│ • Gets specific range of results │
│ • Maximum 1,000 per call │
│ • Use for pagination │
└──────────────────────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────────────────────┐
│ runPaged({ pageSize }) │
│ ────────────────────────────────────────────────────────────────│
│ • Handles large result sets │
│ • Page size: 5-1000 │
│ • Returns PagedData object │
└──────────────────────────────────────────────────────────────────┘

Paged Search Example

const runPagedSearch = () => {
const savedSearch = search.load({
id: 'customsearch_all_customers'
});

const pagedData = savedSearch.runPaged({
pageSize: 100
});

log.debug('Total Results', pagedData.count);
log.debug('Total Pages', pagedData.pageRanges.length);

const allResults = [];

pagedData.pageRanges.forEach((pageRange) => {
const page = pagedData.fetch({ index: pageRange.index });

page.data.forEach((result) => {
allResults.push({
id: result.id,
name: result.getValue('companyname')
});
});
});

return allResults;
};

Joined Searches

Search across related records using joins.

Standard Record Joins

<?xml version="1.0" encoding="UTF-8"?>
<savedsearch scriptid="customsearch_customer_orders">
<definition>
<searchtype>Transaction</searchtype>
<title>Customer Orders with Details</title>

<searchcolumns>
<!-- Transaction columns -->
<column>
<name>tranid</name>
</column>
<column>
<name>trandate</name>
</column>
<column>
<name>amount</name>
</column>

<!-- Joined customer columns -->
<column>
<name>companyname</name>
<join>customer</join>
</column>
<column>
<name>email</name>
<join>customer</join>
</column>
<column>
<name>salesrep</name>
<join>customer</join>
</column>

<!-- Joined item columns (for line items) -->
<column>
<name>itemid</name>
<join>item</join>
</column>
</searchcolumns>

<searchfilters>
<filter>
<name>type</name>
<operator>anyof</operator>
<values>SalesOrd</values>
</filter>

<!-- Filter by joined field -->
<filter>
<name>custentity_region</name>
<join>customer</join>
<operator>anyof</operator>
<values>1</values>
</filter>
</searchfilters>

</definition>
</savedsearch>

Custom Record Joins

Join transactions to custom records using custom body/column fields.

Scenario 1: Transaction → Custom Record (via lookup field)

TRANSACTION JOINED TO CUSTOM RECORD
═══════════════════════════════════════════════════════════════════════════════

Sales Order Customer Order Details
┌─────────────────────────────────┐ ┌────────────────────────────────┐
│ SO-12345 │ │ ORD-001 │
│ custbody_cust_order: ORD-001 ──┼──────▶│ custrecord_co_gift_msg │
│ │ │ custrecord_co_delivery_instr │
└─────────────────────────────────┘ └────────────────────────────────┘

Search Type: Transaction
Join Alias: CUSTBODY_CUST_ORDER (the field ID that links to custom record)
<?xml version="1.0" encoding="UTF-8"?>
<savedsearch scriptid="customsearch_orders_with_gift_details">
<definition>
<searchtype>Transaction</searchtype>
<title>Orders with Gift Details</title>

<searchcolumns>
<!-- Transaction columns -->
<column>
<name>tranid</name>
<label>Order Number</label>
</column>
<column>
<name>entity</name>
<label>Customer</label>
</column>
<column>
<name>amount</name>
<label>Total</label>
</column>

<!-- Joined Custom Record columns -->
<column>
<name>name</name>
<join>CUSTBODY_CUST_ORDER</join>
<label>Customer Order Ref</label>
</column>
<column>
<name>custrecord_co_gift_msg</name>
<join>CUSTBODY_CUST_ORDER</join>
<label>Gift Message</label>
</column>
<column>
<name>custrecord_co_delivery_instr</name>
<join>CUSTBODY_CUST_ORDER</join>
<label>Delivery Instructions</label>
</column>
</searchcolumns>

<searchfilters>
<filter>
<name>type</name>
<operator>anyof</operator>
<values>SalesOrd</values>
</filter>
<filter>
<name>mainline</name>
<operator>is</operator>
<values>T</values>
</filter>
<!-- Filter by custom record field -->
<filter>
<name>custrecord_co_gift_msg</name>
<join>CUSTBODY_CUST_ORDER</join>
<operator>isnotempty</operator>
</filter>
</searchfilters>

</definition>
</savedsearch>

Scenario 2: Custom Record → Transaction (Reverse Join)

CUSTOM RECORD WITH PARENT TRANSACTION
═══════════════════════════════════════════════════════════════════════════════

Customer Order Details Sales Order
┌────────────────────────────────┐ ┌─────────────────────────────────┐
│ ORD-001 │ │ SO-12345 │
│ custrecord_co_transaction: ────┼───────▶│ tranid, entity, amount, etc. │
└────────────────────────────────┘ └─────────────────────────────────┘

Search Type: Custom Record (customrecord_cust_order)
Join Alias: CUSTRECORD_CO_TRANSACTION (the field linking to transaction)
<?xml version="1.0" encoding="UTF-8"?>
<savedsearch scriptid="customsearch_custorder_with_so">
<definition>
<searchtype>customrecord_cust_order</searchtype>
<title>Customer Orders with Sales Order Details</title>

<searchcolumns>
<!-- Custom record columns -->
<column>
<name>name</name>
<label>Order Reference</label>
</column>
<column>
<name>custrecord_co_gift_msg</name>
<label>Gift Message</label>
</column>

<!-- Joined Transaction columns -->
<column>
<name>tranid</name>
<join>CUSTRECORD_CO_TRANSACTION</join>
<label>Sales Order #</label>
</column>
<column>
<name>entity</name>
<join>CUSTRECORD_CO_TRANSACTION</join>
<label>Customer</label>
</column>
<column>
<name>total</name>
<join>CUSTRECORD_CO_TRANSACTION</join>
<label>Order Total</label>
</column>
<column>
<name>status</name>
<join>CUSTRECORD_CO_TRANSACTION</join>
<label>SO Status</label>
</column>
</searchcolumns>

<searchfilters>
<!-- Filter by joined transaction status -->
<filter>
<name>status</name>
<join>CUSTRECORD_CO_TRANSACTION</join>
<operator>anyof</operator>
<values>SalesOrd:B</values> <!-- Pending Fulfillment -->
</filter>
</searchfilters>

</definition>
</savedsearch>

Scenario 3: Custom Record → Custom Record Join

CUSTOM RECORD TO CUSTOM RECORD
═══════════════════════════════════════════════════════════════════════════════

Delivery Schedule Customer Order Details
┌────────────────────────────────┐ ┌────────────────────────────────┐
│ DEL-001 │ │ ORD-001 │
│ custrecord_del_custorder: ─────┼───────▶│ custrecord_co_gift_msg │
│ custrecord_del_date │ │ custrecord_co_transaction │
└────────────────────────────────┘ └────────────────────────────────┘

Search Type: customrecord_delivery_sched
Join Alias: CUSTRECORD_DEL_CUSTORDER
<?xml version="1.0" encoding="UTF-8"?>
<savedsearch scriptid="customsearch_delivery_with_orders">
<definition>
<searchtype>customrecord_delivery_sched</searchtype>
<title>Delivery Schedule with Order Details</title>

<searchcolumns>
<column>
<name>name</name>
<label>Delivery ID</label>
</column>
<column>
<name>custrecord_del_date</name>
<label>Delivery Date</label>
</column>

<!-- Joined Customer Order columns -->
<column>
<name>name</name>
<join>CUSTRECORD_DEL_CUSTORDER</join>
<label>Customer Order Ref</label>
</column>
<column>
<name>custrecord_co_gift_msg</name>
<join>CUSTRECORD_DEL_CUSTORDER</join>
<label>Gift Message</label>
</column>
</searchcolumns>

</definition>
</savedsearch>

SuiteScript: Custom Record Joins

/**
* Search transactions with joined custom record data
* @NApiVersion 2.1
*/
define(['N/search'], function(search) {

function searchOrdersWithGiftDetails() {
const orderSearch = search.create({
type: search.Type.SALES_ORDER,
filters: [
['mainline', 'is', 'T'],
'AND',
// Filter by joined custom record field
['custbody_cust_order.custrecord_co_gift_msg', 'isnotempty', '']
],
columns: [
'tranid',
'entity',
'amount',
// Joined custom record columns
search.createColumn({
name: 'name',
join: 'custbody_cust_order'
}),
search.createColumn({
name: 'custrecord_co_gift_msg',
join: 'custbody_cust_order'
}),
search.createColumn({
name: 'custrecord_co_delivery_instr',
join: 'custbody_cust_order'
})
]
});

const results = [];

orderSearch.run().each(function(result) {
results.push({
orderId: result.id,
orderNumber: result.getValue('tranid'),
customer: result.getText('entity'),
amount: result.getValue('amount'),
// Get joined values
customerOrder: result.getValue({
name: 'name',
join: 'custbody_cust_order'
}),
giftMessage: result.getValue({
name: 'custrecord_co_gift_msg',
join: 'custbody_cust_order'
}),
deliveryInstructions: result.getValue({
name: 'custrecord_co_delivery_instr',
join: 'custbody_cust_order'
})
});

return true; // Continue
});

return results;
}

return {
searchOrdersWithGiftDetails: searchOrdersWithGiftDetails
};
});

Common Join Aliases

Search TypeJoin AliasJoins ToExample Field
TransactioncustomerCustomer recordemail, companyname
TransactionitemItem recorditemid, description
TransactionsalesrepEmployee recordemail, firstname
TransactionCUSTBODY_xxxCustom recordAny field on custom record
TransactionCUSTCOL_xxxCustom record (line)Any field on custom record
Custom RecordCUSTRECORD_xxxRelated recordDepends on target record
CustomersalesrepEmployeefirstname, email
ItemvendorVendorcompanyname, email

[!TIP] The join alias for custom fields is the field ID in uppercase. For example, custbody_cust_order becomes CUSTBODY_CUST_ORDER.


Formula Columns

<searchcolumns>
<!-- Formula: Days since order -->
<column>
<name>formulanumeric</name>
<formula>{today}-{trandate}</formula>
<label>Days Outstanding</label>
</column>

<!-- Formula: Concatenate fields -->
<column>
<name>formulatext</name>
<formula>{tranid} || ' - ' || {entity}</formula>
<label>Order Reference</label>
</column>

<!-- Formula: Conditional -->
<column>
<name>formulatext</name>
<formula>CASE WHEN {amount} > 10000 THEN 'High Value' ELSE 'Standard' END</formula>
<label>Order Type</label>
</column>
</searchcolumns>

Search in Suitelet (Results Display)

const showSearchResults = (context) => {
const form = serverWidget.createForm({
title: 'Pending Approvals'
});

// Load and run search
const savedSearch = search.load({
id: 'customsearch_pending_approvals'
});

// Add sublist
const sublist = form.addSublist({
id: 'results',
type: serverWidget.SublistType.LIST,
label: 'Results'
});

// Add columns matching search
sublist.addField({ id: 'tranid', type: serverWidget.FieldType.TEXT, label: 'Document #' });
sublist.addField({ id: 'date', type: serverWidget.FieldType.DATE, label: 'Date' });
sublist.addField({ id: 'entity', type: serverWidget.FieldType.TEXT, label: 'Entity' });
sublist.addField({ id: 'amount', type: serverWidget.FieldType.CURRENCY, label: 'Amount' });

// Populate from search
let line = 0;
savedSearch.run().each((result) => {
sublist.setSublistValue({ id: 'tranid', line: line, value: result.getValue('tranid') || '' });
sublist.setSublistValue({ id: 'date', line: line, value: result.getValue('trandate') || '' });
sublist.setSublistValue({ id: 'entity', line: line, value: result.getText('entity') || '' });
sublist.setSublistValue({ id: 'amount', line: line, value: result.getValue('amount') || 0 });

line++;
return line < 500; // Limit results
});

context.response.writePage(form);
};

Import Saved Searches

# Import all saved searches
suitecloud object:import --type savedsearch --destinationfolder Objects

# Import specific search
suitecloud object:import --type savedsearch --scriptid customsearch_pending_approvals --destinationfolder Objects

Best Practices

PracticeDescription
Meaningful namesDescriptive script IDs and titles
Limit columnsOnly include needed columns
Optimize filtersUse indexed fields in filters
Set audienceRestrict to appropriate roles
Document purposeInclude clear descriptions
Test performanceCheck execution time

Next Steps