Key Takeaways
- Not all NetSuite data is accessible through N/search: Reference lists like countries, currencies, and terms are stored as select-option metadata, not as searchable record types.
- getSelectOptions() is the correct API for this data: It returns an array of {value, text} objects from a field’s option list. It only works when the record is loaded or created in dynamic mode.
- Two approaches work for country lists: Via a custom field on any record or via the address subrecord’s built-in country field. The return values differ: custom fields return internal IDs; the address subrecord country field returns country abbreviations (e.g., ‘US’, ‘GB’).
- record.create() in dynamic mode avoids saving an unwanted record: You create the record in memory, read the select options, and discard it. No record is saved, no governance units wasted on a record.save() call.
- The same pattern applies beyond countries: Any field typed as List/Record can be accessed this way if it is not directly queryable via N/search.
The Problem: What N/search Cannot Reach
NetSuite’s N/search module is the standard tool for querying record data. It works well for transactions, entity records, and custom records. It does not work for reference lists that NetSuite treats as field metadata rather than independent record types.
Examples of data that cannot be queried directly via N/search:
- Countries: The full list of countries NetSuite recognizes in address fields
- Currency symbols: Available currencies configured in the account
- Payment terms: The terms available in the Terms field on customer and transaction records
- Units of measure: Available units in the Units field on items
- State/province lists: State options that vary based on selected country
These are stored as select options on fields, not as separate records. To retrieve them programmatically, you need a record in dynamic mode and the getSelectOptions() method.
For a broader picture of how to navigate NetSuite’s record and field structure before writing scripts, the NetSuite Records Browser is the reference tool that maps all standard and custom record types, their fields, and field IDs including which fields are typed as List/Record.
The Core API: getField and getSelectOptions
How getSelectOptions Works
getSelectOptions() is a method on a field object retrieved via record.getField(). It returns all available options for a select-type field as an array of objects. Each object has two properties:
- value: The internal identifier NetSuite uses for that option (internal ID or abbreviation depending on the field)
- text: The human-readable label displayed in the UI
Critical requirement: getSelectOptions() only works on records loaded or created in dynamic mode. If you create or load a record without isDynamic: true, the method returns an empty array or throws an error.
Method 1: Via a Custom Field
This approach creates a custom body field on any record, sets its type to List/Record, and targets the data source you want. It gives you access to any list that can be used as a custom field source.
Step 1: Create the Custom Field
- Go to Customization > Lists, Records, & Fields > Body Fields > New
- Set Type to List/Record
- In the List/Record dropdown, select the list you want to retrieve (e.g., Country)
- Set Applies To to any convenient record type (Customer works well)
- Save the field. Note the field ID, it will follow the pattern custbody_[your_field_name]
Step 2: Retrieve the Options via SuiteScript 2.x
/**
* @NApiVersion 2.x
* @NScriptType Restlet
*/
define(['N/record'], (record) => {
const getCountriesList = () => {
const customerRec = record.create({
type: record.Type.CUSTOMER,
isDynamic: true // Required — getSelectOptions only works in dynamic mode
});
const countryFieldObj = customerRec.getField({
fieldId: 'custbody_custom_country_field' // Replace with your custom field ID
});
return countryFieldObj.getSelectOptions();
// Returns: [{ value: '1', text: 'Afghanistan' }, { value: '2', text: 'Albania' }, ...]
};
The record.create() call does not save a record. It instantiates the record object in memory so the field metadata is accessible. No record is written to the database, and no record ID is consumed.
The return value of getSelectOptions() for a custom List/Record field targeting the Country list will have numeric internal IDs as the value: { value: ‘1’, text: ‘Afghanistan’ }. These internal IDs match what NetSuite uses when setting or reading the field value programmatically.
Method 2: Via the Address Subrecord’s Built-In Country Field
If you do not want to create a custom field, you can reach the same country list through the address subrecord that already exists on Customer records. The addressbook sublist contains an addressbookaddress subrecord, which has a built-in country field.
/**
* @NApiVersion 2.x
* @NScriptType Restlet
*/
define(['N/record'], (record) => {
const getCountriesViaAddress = () => {
const customerRec = record.create({
type: record.Type.CUSTOMER,
isDynamic: true
});
const custSublistRec = customerRec.getCurrentSublistSubrecord({
sublistId: 'addressbook',
fieldId: 'addressbookaddress'
});
const countryFieldObj = custSublistRec.getField({
fieldId: 'country'
});
return countryFieldObj.getSelectOptions();
// Returns: [{ value: 'AF', text: 'Afghanistan' }, { value: 'AL', text: 'Albania' }, ...]
};
Important difference: The value field in this approach returns the country abbreviation (ISO 3166-1 alpha-2 code), not the internal ID. ‘US’ instead of a number like ‘225’. Use Method 1 if your downstream code expects internal IDs. Use Method 2 if you need the abbreviations used in address fields (billcountry, shipcountry).
Comparing the Two Approaches
Method 1 — Custom Field: Returns internal IDs as values. Requires creating a custom field. Flexible: works for any list available as a custom field source, not just countries.
Method 2 — Address Subrecord: Returns ISO abbreviations as values. No custom field needed. Works immediately if you have a Customer record available. Only accessible via this specific subrecord path.
The choice depends on what your code does with the data. If you are setting a billcountry field on an address (which accepts abbreviations), Method 2’s return values are ready to use directly. If you are populating a custom country field that stores the internal ID, use Method 1.
Extending the Pattern Beyond Countries
The same getField + getSelectOptions pattern works for any field typed as List/Record. Countries are one example. Other common use cases:
Payment Terms
const getTermsList = () => {
const customerRec = record.create({
type: record.Type.CUSTOMER,
isDynamic: true
});
const termsField = customerRec.getField({ fieldId: 'terms' });
return termsField.getSelectOptions();
// Returns configured payment terms: Net 30, Net 60, etc.
});
Subsidiaries
const getSubsidiaryList = () => {
const customerRec = record.create({
type: record.Type.CUSTOMER,
isDynamic: true
});
const subsidiaryField = customerRec.getField({ fieldId: 'subsidiary' });
return subsidiaryField.getSelectOptions();
// Returns subsidiaries the current user has access to
});
For subsidiaries and other permission-gated lists, getSelectOptions returns only the options the currently executing user (or script user) has permission to see. If the script runs as an administrator, it returns all options. If it runs as a restricted user, it returns only the options that user can access.
Common Errors and How to Fix Them
getSelectOptions returns an empty array
Cause: The record was not loaded or created in dynamic mode.
Fix: Ensure isDynamic: true is set in your record.create() or record.load() call. This is the most common cause.
getField returns null
Cause: The field ID is incorrect, or the field does not exist on that record type.
Fix: Verify the field ID using the NetSuite Records Browser or by viewing the field definition in the UI. For custom fields, the ID starts with cust, confirm the full field ID, including the prefix.
getCurrentSublistSubrecord throws an error
Cause: The subrecord does not exist yet in a newly created record because no address line has been initialized.
Fix: This method requires the subrecord to be accessible in context. For a new record created with record.create(), you may need to use selectNewLine() on the addressbook sublist first to initialize the address subrecord before calling getCurrentSublistSubrecord.
SuiteScript Version Compatibility
The code examples in this guide use SuiteScript 2.x syntax (define, require, N/record module). The getField and getSelectOptions methods are also available in SuiteScript 1.0 as nlapiCreateRecord() and .getField().getSelectOptions(), but SuiteScript 1.0 is deprecated, and new scripts should use 2.x or 2.1.
For a full overview of script types, module syntax, and when to use each version, see the SuiteScript essentials getting started guide, which covers 2.0 vs 2.1 differences, module loading, and script deployment steps.
When This Pattern Is the Right Tool
Use getField + getSelectOptions when:
- You need to populate a UI dropdown from a NetSuite list that cannot be queried via N/search
- You are building a Suitelet or Restlet that needs to return list data to an external interface
- You need to validate whether a value is a valid option in a select field before saving
- You are building a custom form and need to mirror a NetSuite field’s options in a custom UI element
Do not use this pattern for data that is searchable via N/search. If the record type appears in N/search as a searchable type, use a search instead. It is more performant and returns richer data. This pattern is specifically for lists that exist only as field metadata and are not queryable as record types.
For data that falls into a more complex retrieval scenario see the complete SuiteScript guide covering all script types, including when to use Scheduled Scripts, Map/Reduce, and Restlets versus the simpler create-and-read pattern shown here.
Summary
NetSuite stores some reference data (countries, terms, currencies, units) as field select options rather than queryable record types. N/search does not reach this data. The correct API is record.getField({ fieldId }).getSelectOptions(), called on a record instantiated in dynamic mode.
Two approaches work for the country list specifically. A custom field targeting the Country list returns internal IDs. The built-in address subrecord country field returns ISO abbreviations. Choose based on what your downstream code expects. The same pattern extends to any field typed as List/Record: terms, subsidiaries, units of measure, currencies, and other reference lists that behave the same way.