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 Case | Example |
|---|---|
| Reports | Monthly sales by region |
| Dashboards | Real-time KPI displays |
| Portlets | Customer-facing data |
| Scripts | Data source for automation |
| Forms | Dropdown field sources |
| Workflows | Condition 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
| Summary | Description |
|---|---|
| GROUP | Group results by this column |
| COUNT | Count of records |
| SUM | Sum of numeric values |
| AVG | Average of numeric values |
| MIN | Minimum value |
| MAX | Maximum value |
Using Saved Searches in Scripts
Load and Run Saved Search
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 Type | Join Alias | Joins To | Example Field |
|---|---|---|---|
| Transaction | customer | Customer record | email, companyname |
| Transaction | item | Item record | itemid, description |
| Transaction | salesrep | Employee record | email, firstname |
| Transaction | CUSTBODY_xxx | Custom record | Any field on custom record |
| Transaction | CUSTCOL_xxx | Custom record (line) | Any field on custom record |
| Custom Record | CUSTRECORD_xxx | Related record | Depends on target record |
| Customer | salesrep | Employee | firstname, email |
| Item | vendor | Vendor | companyname, email |
[!TIP] The join alias for custom fields is the field ID in uppercase. For example,
custbody_cust_orderbecomesCUSTBODY_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
| Practice | Description |
|---|---|
| Meaningful names | Descriptive script IDs and titles |
| Limit columns | Only include needed columns |
| Optimize filters | Use indexed fields in filters |
| Set audience | Restrict to appropriate roles |
| Document purpose | Include clear descriptions |
| Test performance | Check execution time |
Next Steps
- Workflows - Use searches in workflow conditions
- Suitelet Development - Display search results
- Scheduled Script - Process search results