Loading different transaction records in one NetSuite script is straightforward only when every result uses the same record type. Once a search returns sales orders, invoices, cash sales, credit memos, or purchase orders together, the script must load each record with the correct internal type and ID. The reliable pattern is to retrieve the record type from each search result, normalize it through a small mapping or validation layer, call `record.load()` with that type and ID, and handle unsupported or inaccessible records without stopping the entire batch.
SuiteScript: Load Multiple Transaction Types Safely
NetSuite identifies a transaction by both its internal ID and its record type. The internal ID alone is not sufficient for `record.load()`. A script that passes an invoice ID while assuming the record is a sales order can fail, load the wrong kind of record in a loosely controlled process, or produce inconsistent downstream logic.
The core SuiteScript 2.1 pattern looks like this:
const transaction = record.load({
type: transactionType,
id: transactionId,
isDynamic: false
});The important detail is that `transactionType` must be determined for each individual result. A mixed transaction search should not rely on one hardcoded value such as `record.Type.SALES_ORDER`.
For general NetSuite API selection and integration architecture, see our guidance on choosing the right NetSuite API for an integration. This article focuses specifically on the record-loading problem inside SuiteScript, especially when one search or batch includes several transaction types.
Why mixed transaction searches require different handling
A transaction saved search can combine multiple transaction types through filters, joins, or broad criteria. For example, a search might include:
Sales orders
Invoices
Cash sales
Credit memos
Vendor bills
Purchase orders
Item fulfillments
These records share some common fields, including internal ID, transaction number, date, and status. They do not share every field, sublist, workflow state, or supported operation.
An invoice and a purchase order both have transaction numbers, but their accounting behavior and line-level data differ. A cash sale does not follow the same fulfillment lifecycle as a sales order. A vendor bill uses vendor and expense information that does not exist on a customer invoice.
That distinction affects more than the initial load. After loading a record, the script must also decide which fields and sublists are safe to read or update. A field such as `entity` is broadly useful across transaction records, while a sublist such as `item`, `expense`, or `inventory` requires record-specific logic.
The safest design separates three concerns:
Identify the record type and internal ID.
Load the record using the correct type.
Apply only the fields and operations supported by that type.
Keeping those responsibilities separate prevents a long conditional block from becoming the script's only source of control.
How do you get the transaction type from a search result?
The most direct option is to use the record type exposed by the search result, together with the result ID. In SuiteScript 2.x, a search result commonly provides the record identifier through `result.id` and the record type through `result.recordType`.
A basic search-processing function can look like this:
define(['N/record', 'N/log'], (record, log) => {
const processResult = (result) => {
const transactionId = result.id;
const transactionType = result.recordType;
if (!transactionId || !transactionType) {
throw new Error('Search result is missing an ID or record type');
}
const loadedRecord = record.load({
type: transactionType,
id: transactionId,
isDynamic: false
});
log.debug({
title: 'Transaction loaded',
details: {
type: transactionType,
id: transactionId
}
});
return loadedRecord;
};
return {
processResult
};
});The exact search definition still matters. If the search is intended to return several transaction types, make that intent visible in the saved search filters or in the script's search construction. Do not assume that a generic transaction search will always produce the same population after an administrator changes a filter.
For high-control scripts, include the transaction type as part of the script's output or processing key. A key such as `invoice:12345` is safer than `12345`, because it remains meaningful when logs, retries, or queues contain records from different types.
Use a record-type map when the search does not expose enough context
Some processing designs do not begin with a search result that directly supplies a type. The script might receive a queue entry, a CSV row, a custom record, or a parameter containing an internal ID and a business classification. In that case, use an explicit map rather than guessing from the ID.
const TRANSACTION_TYPES = Object.freeze({
salesOrder: record.Type.SALES_ORDER,
invoice: record.Type.INVOICE,
cashSale: record.Type.CASH_SALE,
creditMemo: record.Type.CREDIT_MEMO,
purchaseOrder: record.Type.PURCHASE_ORDER,
vendorBill: record.Type.VENDOR_BILL
});
const loadByCategory = (category, id) => {
const type = TRANSACTION_TYPES[category];
if (!type) {
throw new Error(`Unsupported transaction category: ${category}`);
}
return record.load({
type,
id,
isDynamic: false
});
};This map creates a controlled boundary between external input and the NetSuite record API. It also makes code review easier because supported types are visible in one place.
Use `record.Type` constants when the relevant constant exists. They reduce spelling errors and communicate intent clearly. When a record type is not represented by a convenient constant, use the documented record type string and keep it inside the same controlled map. Do not allow arbitrary user-provided type strings to flow directly into `record.load()` without validation.
Choose the correct loading mode
`isDynamic` determines how SuiteScript interacts with the loaded record.
With `isDynamic: false`, the record uses standard mode. Field and sublist operations are generally more explicit, and the script does not depend on UI-style sourcing or line selection. This is usually the better default for batch processing, validation, and read-heavy operations.
With `isDynamic: true`, the record uses dynamic mode. The script works more like a user interacting with the NetSuite form, including line selection and field sourcing behavior. Dynamic mode is useful when the script must reproduce interactive line-entry behavior, but it introduces more state and more opportunities for record-specific differences.
A mixed-type loader should not choose dynamic mode merely because one transaction type needs it. Instead, make the mode part of the processing rule:
const LOAD_RULES = Object.freeze({
salesorder: { isDynamic: false },
invoice: { isDynamic: false },
cashsale: { isDynamic: false },
purchaseorder: { isDynamic: true }
});
const loadTransaction = (type, id) => {
const rules = LOAD_RULES[type] || { isDynamic: false };
return record.load({
type,
id,
isDynamic: rules.isDynamic
});
};This approach makes an exception visible. It also prevents a single global setting from silently affecting every record in a mixed batch.
How should a script handle fields that differ by transaction type?
A mixed-type script should normalize common data first, then branch only where the business logic truly differs. This is more maintainable than treating every transaction as a completely unrelated object.
A normalization function might return a consistent internal shape:
const normalizeTransaction = (loadedRecord, type) => {
const common = {
id: loadedRecord.id,
type,
tranId: loadedRecord.getValue({ fieldId: 'tranid' }),
entity: loadedRecord.getValue({ fieldId: 'entity' }),
subsidiary: loadedRecord.getValue({ fieldId: 'subsidiary' }),
trandate: loadedRecord.getValue({ fieldId: 'trandate' })
};
if (type === record.Type.PURCHASE_ORDER ||
type === record.Type.VENDOR_BILL) {
return {
...common,
direction: 'payables',
approvalStatus: loadedRecord.getValue({
fieldId: 'approvalstatus'
})
};
}
return {
...common,
direction: 'receivables'
};
};This pattern creates a stable object for later reporting, validation, or integration. The raw NetSuite record remains available when type-specific behavior is necessary.
Before reading a field, confirm that it applies to the current record type and account configuration. Subsidiary, departments, classes, locations, tax fields, and approval fields depend on enabled features and permissions. A field that exists in one account or transaction configuration is not automatically safe to assume in another.
The same rule applies to sublists. Check whether the sublist exists before iterating it:
const getItemCount = (loadedRecord) => {
try {
return loadedRecord.getLineCount({
sublistId: 'item'
});
} catch (error) {
return 0;
}
};A more explicit implementation can use a type capability map:
const CAPABILITIES = Object.freeze({
salesorder: { itemSublist: true, expenseSublist: false },
invoice: { itemSublist: true, expenseSublist: false },
vendorbill: { itemSublist: false, expenseSublist: true }
});Capability-based logic is more reliable than assuming that every transaction supports the same sublists.
Validate before loading to reduce avoidable failures
Validation should happen before `record.load()` whenever the script has enough information to perform it. This includes checking that the ID is present, the type is supported, and the value has the expected format.
const validateLoadRequest = ({ type, id }) => {
if (!type) {
throw new Error('Transaction type is required');
}
if (!id || !String(id).match(/^\d+$/)) {
throw new Error(`Invalid transaction ID: ${id}`);
}
const supportedTypes = new Set([
record.Type.SALES_ORDER,
record.Type.INVOICE,
record.Type.CASH_SALE,
record.Type.CREDIT_MEMO,
record.Type.PURCHASE_ORDER,
record.Type.VENDOR_BILL
]);
if (!supportedTypes.has(type)) {
throw new Error(`Unsupported transaction type: ${type}`);
}
};Validation does not replace permissions or record-existence checks. A record can pass local validation and still fail because it was deleted, made inactive, restricted by subsidiary access, or unavailable to the deployment role.
Use structured error information so failed records can be retried independently. At minimum, capture the record type, internal ID, error name, message, and processing stage.
Search results versus loading every record
A search result already contains many fields, so loading every record is not always necessary. If the script only needs transaction number, date, status, amount, or a joined customer value, add those fields to the search and process the result directly.
Load the full record when the script needs:
A field that is not available or reliable in the search result
Sublist lines
Dynamic sourcing or record manipulation
Record-level validation
Data that requires the record API
A later save operation
This distinction has a direct governance effect. `record.load()` consumes governance units, and transaction records carry a higher governance cost than many non-transaction records. A batch that loads thousands of records without first filtering or projecting the required fields will consume resources unnecessarily.
For transaction reporting and saved search optimization, our NetSuite reporting services cover search design, complex joins, formulas, and reporting scalability.
Handling mixed transaction types in Map/Reduce
Map/Reduce is the strongest fit when the search population is large, records can be processed independently, or failures need to be isolated. A common design uses the search in `getInputData()`, loads one result in `map()`, and reports failures in `summarize()`.
/**
* @NApiVersion 2.1
* @NScriptType MapReduceScript
*/
define(['N/search', 'N/record', 'N/log'], (search, record, log) => {
const getInputData = () => {
return search.load({
id: 'customsearch_mixed_transactions'
});
};
const map = (context) => {
const result = JSON.parse(context.value);
const id = result.id;
const type = result.recordType;
try {
validateLoadRequest({ type, id });
const loaded = record.load({
type,
id,
isDynamic: false
});
const normalized = normalizeTransaction(loaded, type);
context.write({
key: `${type}:${id}`,
value: JSON.stringify(normalized)
});
} catch (error) {
log.error({
title: `Unable to process ${type}:${id}`,
details: {
name: error.name,
message: error.message,
stack: error.stack
}
});
context.write({
key: `${type}:${id}`,
value: JSON.stringify({
status: 'FAILED',
type,
id,
errorName: error.name,
errorMessage: error.message
})
});
}
};
const summarize = (summary) => {
summary.mapSummary.errors.iterator().each((key, error) => {
log.error({
title: `Map error for ${key}`,
details: error
});
return true;
});
};
return {
getInputData,
map,
summarize
};
});The example uses helper functions defined elsewhere in the module. In production, keep validation, normalization, and capability rules in the same deployment or in a shared library that is versioned and tested.
Map/Reduce does not eliminate governance limits. It distributes work across stages, but each record still consumes processing resources. Keep the map stage focused, avoid unrelated searches inside the per-record loop, and use retry-safe writes. If the script saves records, account for record locking and concurrent updates as well as loading costs.
Common mistakes when loading several transaction types
The most common mistake is hardcoding one type for a mixed search:
record.load({
type: record.Type.INVOICE,
id: result.id
});That code is correct only when every result is an invoice. It is not a general mixed-transaction solution.
Another mistake is using the transaction number as though it were a universal identifier. Transaction numbers can vary by type and numbering configuration. Use the internal ID and record type as the technical identity, and use `tranid` only as a display or business reference.
A third mistake is applying the same field and sublist logic to every record. Shared terminology does not guarantee shared schema. Build a common normalized layer and isolate type-specific fields behind explicit rules.
Finally, do not hide every error with a broad catch block that returns only “failed.” A useful exception record should identify the type, ID, deployment, processing stage, and original NetSuite error. That information determines whether the correct response is a retry, permission change, data correction, or code change.
A practical design decision framework
| Requirement | Recommended approach |
|---|---|
| Read a few common fields | Search columns without loading the record |
| Load several known transaction types | Use `result.recordType` and `result.id` |
| Receive type and ID from another process | Validate against an explicit type map |
| Read or update line-level data | Load the record and use capability rules |
| Process a large independent population | Use Map/Reduce |
| Apply UI-style sourcing | Use dynamic mode only for the relevant type |
| Return data to an external system | Normalize the output before serialization |
| Retry individual failures | Use type-plus-ID keys and structured errors |
If the process is part of a broader integration, define ownership for authentication, mapping, duplicate controls, and recovery. Our NetSuite integration platform services support architectures using SuiteScript, REST, SOAP, and middleware according to the process requirements.
Testing a mixed-type loader
Test each supported transaction type separately before testing the mixed search. A script can appear successful when the first several results are invoices and still fail as soon as a credit memo or vendor bill appears.
A useful test matrix includes:
One valid record for every supported type
A missing or invalid internal ID
A deleted record
A record restricted by role or subsidiary
A transaction with no item lines
A transaction using an alternate sublist such as `expense`
A record with optional fields disabled
A search result with an unsupported type
A batch containing both successful and failed records
Also test the deployment context. The script owner, deployment role, execution context, subsidiary permissions, and feature configuration all affect whether `record.load()` and later field operations succeed.
Before moving to production, verify that the script does not save records unnecessarily, that errors remain traceable after deployment, and that the processing model behaves correctly when the search returns zero results or a much larger population than the initial test.
If your loading logic is part of a wider NetSuite customization, contact Versich to discuss the record model, deployment design, and testing approach before implementation.
Conclusion
Loading multiple transaction types in SuiteScript requires more than passing IDs into `record.load()`. NetSuite needs the correct record type for every ID, and each loaded transaction must then be handled according to its supported fields, sublists, permissions, and processing behavior.
The reliable design is to identify the type per search result, validate it against supported records, load with an explicit mode, normalize common data, isolate type-specific rules, and record structured failures. Use search columns instead of full loads when possible, and use Map/Reduce when the population is large or individual records need independent retries. This approach keeps mixed-transaction scripts predictable, testable, and easier to maintain as the NetSuite account evolves.

