VERSICH

NetSuite Load Item Record by ID Without Type Errors

netsuite load item record by id without type errors

When a SuiteScript automation needs item data, loading the item record by its internal ID is one of the most direct approaches. However, NetSuite requires more than an ID alone. The script must provide the correct record type, use the internal ID rather than the displayed item name or SKU, and account for standard versus dynamic record behavior.

To load an item record by ID in NetSuite, use `record.load()` with the item’s record type and internal ID. In SuiteScript 2.x, the standard pattern is `record.load({ type: record.Type.INVENTORY_ITEM, id: itemId, isDynamic: false })`. The `type` value must match the actual item subtype, such as inventory item, non-inventory item, service item, or kit/package, and `id` must be the NetSuite internal ID. Once loaded, read fields with `getValue()` or `getText()`, and use sublist methods when you need line-level data.

This narrow implementation detail matters because an item’s visible identifier, often called the Item Name/Number or SKU, is not necessarily the internal ID used by SuiteScript. A valid-looking value can therefore produce a record-not-found error or load the wrong type when the script assumes every item is an inventory item.

What NetSuite record.load actually does

`record.load()` retrieves an existing NetSuite record and returns a `record.Record` object that SuiteScript can inspect or modify. Loading a record does not automatically save changes. If a script changes field values, it must call `save()` separately for those changes to persist.

For item automation, the returned record gives the script access to body fields such as:

  • Item Name/Number

  • Display Name

  • Description

  • Subsidiary

  • Base Price

  • Stock units

  • Purchase and sales descriptions

  • Inventory and purchasing settings

The available fields depend on the item subtype and account configuration. A serialized inventory item, for example, has different operational requirements from a non-inventory item. Custom fields, multiple units of measure, matrix items, bins, lots, and locations also affect what the script can read.

A record loaded through SuiteScript is not the same thing as a row returned by SuiteQL. SuiteQL is designed for querying data sources, while `record.load()` returns a record object with record APIs and sublist methods. For broader query-driven retrieval, our practical SuiteQL guide for NetSuite data explains the distinction. Use `record.load()` when your logic needs the record structure or record-level operations, not simply a result set.

Which ID should you use to load an item in NetSuite?

Use the item’s internal ID in `record.load()`, not its SKU, Item Name/Number, UPC, vendor code, or external ID.

NetSuite commonly exposes several identifiers for an item:

IdentifierTypical purposeSuitable for `record.load()`?
Internal IDNetSuite’s unique database identifierYes
Item Name/NumberHuman-readable item reference or SKUNo, not directly
UPC or barcodeScanning and product identificationNo
Vendor codeSupplier-facing referenceNo
External IDIntegration and cross-system mappingNot directly

The internal ID might appear in a URL, a saved search result, a Suitelet parameter, a transaction line, or a lookup result. When an integration receives a SKU, the script must first resolve that SKU to the corresponding internal ID. Passing the SKU directly into `record.load()` creates a common failure point.

A robust integration treats identifiers explicitly. If an external system sends `ABC-100`, the script should not assume that `ABC-100` is the NetSuite internal ID. It should search for the item by the appropriate field, validate that exactly one item matches, and then load the resulting internal ID.

How to load an item record by ID with SuiteScript 2.x

The following example uses SuiteScript 2.1 and loads an inventory item in standard mode:

/**
 * @NApiVersion 2.1
 * @NScriptType UserEventScript
 */
define(['N/record'], (record) => {
    const loadInventoryItem = (itemId) => {
        const itemRecord = record.load({
            type: record.Type.INVENTORY_ITEM,
            id: itemId,
            isDynamic: false
        });

        return {
            internalId: itemRecord.id,
            itemName: itemRecord.getValue({
                fieldId: 'itemid'
            }),
            displayName: itemRecord.getValue({
                fieldId: 'displayname'
            }),
            description: itemRecord.getValue({
                fieldId: 'salesdescription'
            })
        };
    };

    return {
        beforeLoad: (context) => {
            if (context.type === context.UserEventType.DELETE) {
                return;
            }

            const itemId = context.newRecord.id;
            if (!itemId) {
                return;
            }

            const itemData = loadInventoryItem(itemId);
            log.debug({
                title: 'Loaded item',
                details: itemData
            });
        }
    };
});

The important parts are the `N/record` module, the record type, the internal ID, and the explicit `isDynamic` setting. The script checks for an ID before loading because a new record does not have a database ID yet. That check is especially important in `beforeLoad` and `beforeSubmit` contexts.

If the script already runs on the item record, loading the same record again is not always necessary. In a User Event script, `context.newRecord` already provides the current record context. A second load consumes governance and creates another database read. Load the record again only when the script needs a separate persisted state, a different record mode, or fields that are not reliably available from the event context.

Choosing the correct item record type

The item type is not interchangeable. NetSuite’s record API expects the type that corresponds to the actual item record. Common constants include:

NetSuite item categorySuiteScript record constant
Inventory item`record.Type.INVENTORY_ITEM`
Non-inventory item`record.Type.NON_INVENTORY_ITEM`
Service item`record.Type.SERVICE_ITEM`
Other charge item`record.Type.OTHER_CHARGE`
Kit/package`record.Type.KIT`
Assembly item`record.Type.ASSEMBLY_ITEM`
Lot-numbered inventory item`record.Type.LOT_NUMBERED_INVENTORY_ITEM`
Serialized inventory item`record.Type.SERIALIZED_INVENTORY_ITEM`

The exact constant depends on the record type supported by the SuiteScript version and the account’s enabled features. Before coding, confirm the record type in the SuiteScript Records Browser, NetSuite Records Catalog, or the item record’s metadata. Our article on using the SuiteScript Records Browser inside NetSuite covers the documentation workflow, while this article focuses specifically on loading an item by ID and handling runtime behavior.

A frequent mistake is using `record.Type.INVENTORY_ITEM` for every stock-related item. A serialized inventory item or lot-numbered inventory item can require its own type. When the type is wrong, a valid internal ID still fails because NetSuite is being asked to load the ID from the wrong record family.

Standard mode versus dynamic mode

For most read-only item lookups, standard mode is the better default:

const itemRecord = record.load({
    type: record.Type.INVENTORY_ITEM,
    id: itemId,
    isDynamic: false
});

In standard mode, field and sublist operations are not modeled as interactive UI edits. The script can read values directly and make changes without following the same sourcing sequence a user experiences on the form.

Dynamic mode is appropriate when the script needs UI-like behavior, such as selecting a sublist line, setting a value, sourcing dependent fields, and committing the line:

const itemRecord = record.load({
    type: record.Type.INVENTORY_ITEM,
    id: itemId,
    isDynamic: true
});

const locationCount = itemRecord.getLineCount({
    sublistId: 'locations'
});

for (let line = 0; line < locationCount; line += 1) {
    itemRecord.selectLine({
        sublistId: 'locations',
        line
    });

    const locationId = itemRecord.getCurrentSublistValue({
        sublistId: 'locations',
        fieldId: 'location'
    });

    log.debug({
        title: 'Item location',
        details: locationId
    });

    itemRecord.commitLine({
        sublistId: 'locations'
    });
}

Dynamic mode adds complexity because the script must select and commit lines. It also changes how sourcing and validation behave. For a simple field read, dynamic mode adds no benefit. We recommend standard mode unless the script’s logic genuinely depends on dynamic sublist interaction.

Reading item fields safely after loading

Use `getValue()` for the underlying field value and `getText()` when the script needs the display text for a select or record-reference field.

const itemType = itemRecord.getValue({
    fieldId: 'type'
});

const subsidiaryText = itemRecord.getText({
    fieldId: 'subsidiary'
});

const purchaseUnit = itemRecord.getText({
    fieldId: 'purchaseunit'
});

A field’s script ID is not always obvious from its screen label. The visible label “Item Name/Number” maps commonly to `itemid`, but custom forms, account features, and record types introduce exceptions. Confirm field IDs in the Records Browser or through a saved search before treating them as universal.

Item records also contain sublists that require different APIs. For example, location data is not read with `getValue()`. Use `getLineCount()` and `getSublistValue()` in standard mode:

const locationCount = itemRecord.getLineCount({
    sublistId: 'locations'
});

for (let line = 0; line < locationCount; line += 1) {
    const locationId = itemRecord.getSublistValue({
        sublistId: 'locations',
        fieldId: 'location',
        line
    });

    const quantityAvailable = itemRecord.getSublistValue({
        sublistId: 'locations',
        fieldId: 'quantityavailable',
        line
    });

    log.debug({
        title: `Location line ${line}`,
        details: {
            locationId,
            quantityAvailable
        }
    });
}

The `locations` sublist and its fields depend on item configuration and enabled inventory features. A script should therefore handle zero lines and missing optional fields rather than assuming every item has location rows.

What causes “record does not exist” when loading an item?

The most common cause is using the wrong identifier. If the script passes an Item Name/Number where NetSuite expects an internal ID, `record.load()` cannot find the record.

Other causes include:

  • The item ID is empty, undefined, or a string containing unexpected characters.

  • The record type does not match the item subtype.

  • The item belongs to a context where the executing role lacks permission.

  • The item was deleted, inactivated, or removed between lookup and load.

  • A value came from an external system and was incorrectly treated as a NetSuite internal ID.

  • The script is running in a restricted subsidiary or location context.

  • The deployment is using a role that cannot view the required item record.

A useful diagnostic pattern is to log the record type and ID separately without exposing unnecessary item data:

log.debug({
    title: 'Item load inputs',
    details: {
        type: record.Type.INVENTORY_ITEM,
        id: itemId
    }
});

Do not conceal an identifier conversion problem by repeatedly retrying the load. Resolve the identifier first, validate the expected item type, and then perform one controlled load.

How to resolve an SKU to an internal ID first

When an integration starts with an SKU, search for the item before calling `record.load()`. A `search.create()` query can return the internal ID and item type:

define(['N/search', 'N/record'], (search, record) => {
    const findItemByName = (itemName) => {
        const result = search.create({
            type: search.Type.ITEM,
            filters: [
                ['itemid', 'is', itemName]
            ],
            columns: [
                'internalid',
                'itemid',
                'type'
            ]
        }).run().getRange({
            start: 0,
            end: 1
        })[0];

        if (!result) {
            throw new Error(`No NetSuite item matched: ${itemName}`);
        }

        return {
            id: result.getValue({ name: 'internalid' }),
            itemName: result.getValue({ name: 'itemid' }),
            type: result.getValue({ name: 'type' })
        };
    };

    return { findItemByName };
});

The returned `type` value may not map directly to the constant required by `record.load()`. For a production integration, create a deliberate mapping layer and test every item subtype the integration is allowed to process. Do not silently default all search results to inventory items.

Exact matching also matters. A partial match can return a related SKU, matrix parent, or similarly named item. If item names are not unique in the relevant account configuration, add additional filters such as subsidiary, item type, or external ID.

Governance and performance considerations

Record loads consume script governance units. The exact cost depends on the record category and NetSuite’s current governance rules, so review the official SuiteScript documentation for the deployed account and version. The practical rule remains stable: do not load the same item repeatedly inside a large loop when one load or one search can provide the required data.

For batch processing, choose the retrieval method based on the work:

  • Use `record.load()` when you need record fields, sublists, or record APIs.

  • Use a saved search or `N/search` when you need a small set of fields across many items.

  • Use SuiteQL when you need structured, multi-record querying and the required data source supports it.

  • Use `N/record.submitFields()` for targeted body-field updates that do not require loading the entire record.

  • Cache repeated item IDs during one script execution.

Loading an item to read one body field is inefficient when the same field could come from a search result. Conversely, using SuiteQL for a process that must edit sublists leads to unnecessary complexity because query results are not editable record objects.

A reliable implementation pattern

A dependable item loader separates input validation, identifier resolution, record loading, and field extraction. That separation makes failures easier to diagnose and prevents an integration from mixing SKU values with internal IDs.

The loader should:

  1. Confirm that an ID exists and is in the expected format.

  2. Use the correct item record type.

  3. Load in standard mode unless dynamic behavior is required.

  4. Read only the fields and sublists needed by the business process.

  5. Handle permission and missing-record errors explicitly.

  6. Avoid saving unless the script intentionally changes the item.

This pattern is particularly important when item data moves between NetSuite and fulfillment, warehouse, ecommerce, or shipping systems. Consistent item identifiers are necessary for SKU mapping, inventory availability, unit-of-measure handling, and fulfillment accuracy. If the integration begins with an external item code, document the conversion from external identifier to NetSuite internal ID as part of the interface contract.

If item loading is part of a broader integration or script redesign, contact Versich to discuss your NetSuite requirements. A technical review should examine record types, permissions, governance usage, identifier mapping, and error handling together rather than treating `record.load()` as an isolated code change.

Conclusion

Loading an item record by ID in NetSuite is straightforward when the script uses the internal ID, the correct item subtype, and an appropriate record mode. The safest implementation validates the input, resolves external identifiers before loading, reads fields through the correct APIs, and avoids unnecessary record loads in loops.

For simple field retrieval, searches or SuiteQL provide a more efficient option. For sublists, record-level logic, and controlled updates, `record.load()` remains the right tool. Treating item type and identifier mapping as explicit parts of the design prevents the most common errors and creates a more reliable SuiteScript integration.

Frequently Asked Questions

How do I load an item record by ID in NetSuite?

Use SuiteScript’s `record.load()` method with the correct item record type and the item’s internal ID. For example, `record.load({ type: record.Type.INVENTORY_ITEM, id: itemId, isDynamic: false })` loads an inventory item in standard mode.

Can I use the SKU or Item Name/Number with `record.load()`?

No. `record.load()` expects the NetSuite internal ID, not the SKU, Item Name/Number, UPC, or vendor code. Search for the item first, retrieve its internal ID, and then pass that ID to `record.load()`.

Is `record.load()` required to read an item in NetSuite?

No. `record.load()` is required only when you need a record object, sublists, or record-level operations. Use `N/search` or SuiteQL when you only need selected fields from one or many items.

Why does NetSuite say the item record does not exist?

The script is usually passing the wrong ID or the wrong item record type. Check that the value is the internal ID, confirm the item subtype, and verify that the executing role has permission to view the record.

Should I load an item in dynamic mode or standard mode?

Use standard mode for most item reads and straightforward updates. Use dynamic mode when the script needs UI-like sublist behavior, including selecting lines, sourcing dependent values, and committing lines.

How much does loading an item record cost in NetSuite governance?

A record load consumes governance units, and the exact amount depends on the record category and current SuiteScript governance rules. Reduce usage by loading each item only when necessary, caching repeated IDs, and using searches or SuiteQL for field-only retrieval.