Skip to content

Latest commit

 

History

2 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 

Repository files navigation

SuiteScript 2.1 Patterns

Production patterns for NetSuite SuiteScript 2.1: a Map/Reduce template whose summarize stage actually reports failures, a governance guard for bulk operations, and field notes on the async entity deduplication engine.

These are the patterns that survive contact with real data volumes. Each one exists because its absence caused a specific, expensive failure.


Why do Map/Reduce scripts silently skip records?

Because errors thrown in map() and reduce() do not fail the script.

NetSuite catches them, records them against the key, and hands them to summarize(). If you do not drain mapSummary.errors and reduceSummary.errors, those errors disappear. The deployment screen shows a completed run. The logs show a clean finish. And some fraction of your records were never processed.

This is the most common Map/Reduce defect in production, and it is invisible by construction — a script that processes 80% of its input looks exactly like a script that processes 100%.

There are three separate error channels, and all three must be drained:

const summarize = (summary) => {
    const failures = [];

    // 1. getInputData failed outright
    if (summary.inputSummary.error) {
        log.error({ title: 'getInputData failed', details: summary.inputSummary.error });
        failures.push({ stage: 'input', error: summary.inputSummary.error });
    }

    // 2. per-key failures in map
    summary.mapSummary.errors.each((key, error) => {
        log.error({ title: `map error on key ${key}`, details: error });
        failures.push({ stage: 'map', key, error });
        return true;
    });

    // 3. per-key failures in reduce
    summary.reduceSummary.errors.each((key, error) => {
        log.error({ title: `reduce error on key ${key}`, details: error });
        failures.push({ stage: 'reduce', key, error });
        return true;
    });

    if (failures.length > 0) {
        throw new Error(`Completed with ${failures.length} failed record(s).`);
    }
};

Two details that bite people:

.errors is an iterator, not an array. It exposes .each(). Calling .forEach() or .map() on it is a silent no-op, which reintroduces exactly the bug you were trying to fix.

The callback must return true to continue. Returning nothing — or anything falsy — stops iteration after the first error, so you see one failure and assume it was the only one.

Full template: src/map-reduce-template.js


How do I stop a bulk script dying halfway through?

Check the governance budget before each operation, and keep a reserve.

NetSuite terminates a script that exceeds its limit. It does not roll back the work already done. A bulk update that dies at record 4,000 of 10,000 leaves 4,000 records changed, 6,000 untouched, and no record of where the boundary fell — so a naive re-run double-applies the first 4,000.

const guard = require('./governance-guard');

// Need budget for the save AND enough left over to exit cleanly
guard.assertAffordable(guard.COST.RECORD_SAVE, 1, 'PO update');

The reserve is the part people leave out. You need units in hand to log the outcome, write a checkpoint, and reschedule. Spending down to zero means dying before you can record where you stopped.

Costs that dominate real scripts:

Operation Units
record.load (entity) 5
record.save (entity) 10
record.submitFields 10
record.load (transaction) 10
record.save (transaction) 20
query.runSuiteQL 10
https.request 10

Budgets by script type: user event 1,000 · scheduled 10,000 · Map/Reduce map 1,000 per key, reduce 5,000 per key.

Full module: src/governance-guard.js


Why did my entity merge keep the wrong record?

Because in a bulk run, the async deduplication engine does not reliably honour masterRecordId. Groups come back merged into a master that is not the id you supplied. The specific failure observed in production: a Lead record surviving instead of the Customer that was explicitly nominated.

The master is the record that survives — everything else in the group is absorbed into it and stops existing. So when the wrong record wins, the surviving entity carries the wrong stage, and every downstream integration holding the Customer's internal id now points at an absorbed record. No error is raised. The task reports completion.

Setting masterSelectionMode to SELECT_BY_ID is necessary — supply masterRecordId under any other mode and it is accepted then disregarded by design — but it is not sufficient. The behaviour above was observed with SELECT_BY_ID set.

Treat master selection as advisory in a bulk run, and verify survivors afterwards. You cannot reconstruct the mapping after the fact, because the absorbed records are gone — so capture the intended master id -> absorbed ids mapping before you submit.

Full notes, the four-step safe bulk-merge procedure, and the two verification queries: docs/deduplication-notes.md


How do I use these?

Both src/ files are AMD modules in SuiteScript 2.1 format, ready to drop into the File Cabinet under SuiteScripts/.

map-reduce-template.js is a starting point — replace getInputData's query and the reduce body with your own logic, and keep the summarize stage as-is.

governance-guard.js is a library module. Require it from any script type; it has no entry point of its own.

Both pass node --check.


Related

License

MIT — see LICENSE.

About

Production SuiteScript 2.1 patterns for NetSuite: a Map/Reduce template whose summarize stage actually reports failures, a governance guard for bulk operations, and field notes on the async entity deduplication engine.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages