NewNetSuite 2025.2 — What's new
intermediateSuiteScript30 min read

NetSuite Search API: Querying Data with SuiteScript

Master the N/search module in SuiteScript 2.1. Create, run, and paginate searches programmatically. Includes SuiteQL examples for complex queries.

Prerequisites

  • SuiteScript 2.1 basics
  • Understanding of saved searches
  • SQL basics for SuiteQL section
SuiteScriptN/searchSuiteQLNetSuite DevelopmentData Access

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.

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:

OperatorUsageExample Values
is / isnotExact match'John Smith'
contains / doesnotcontainPartial text match'widget'
startswithBegins with text'SO-'
anyof / noneofMulti-select list match'SalesOrd:B', 'SalesOrd:D'
greaterthan / lessthanNumeric comparison1000
greaterthanorequalto / lessthanorequaltoInclusive comparison500
withinDate range'lastmonth', or two dates
on / before / afterSingle date comparison'1/15/2026'
isempty / isnotemptyNull checks(no value needed)
haskeywordsFull-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 true from the callback to continue to the next result. Return false (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

  1. runPaged() executes the search and returns a PagedData object with the total count and page ranges.
  2. pageSize can be between 5 and 1,000 (default is 50).
  3. pagedData.count gives the total number of results without fetching them all.
  4. pagedData.pageRanges is an array of page range objects. Each has an index property.
  5. 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) and text (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 (like getText() 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.

OperationGovernance Cost
search.create()0 units (definition only)
search.load()5 units
search.lookupFields()1 unit
ResultSet.each() callback10 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 units

4. 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 5000

To 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:


Need help building complex data queries in NetSuite? Contact our development team for expert assistance.

Need hands-on training?

Our corporate training programs go beyond tutorials with personalized instruction using your actual NetSuite environment.

Get in Touch