In the first post of this series we looked at User Event and Client scripts, the SuiteScript types that fire when a person opens or saves a record. But a huge amount of real business work has nobody sitting in front of a screen: nightly reconciliations, month-end fee calculations, syncing thousands of items to a marketplace. For that work NetSuite gives you background script types that run on a schedule and can chew through enormous volumes of data without hitting governance limits. In this post we cover the Scheduled script and its more powerful sibling, the Map/Reduce script, with a code example and a real-world scenario for each.
Why Background Scripts Exist: Governance
Every SuiteScript execution is limited by a governance system that assigns a “usage unit” cost to each API call. A record load might cost 10 units, a save 20, and so on. Real-time scripts have modest budgets because they must return quickly. Background scripts get a much larger budget, and Map/Reduce scripts go further by automatically splitting work into stages that each start with a fresh allocation. Understanding governance is the difference between a job that finishes cleanly and one that dies halfway through the second thousand records.
Scheduled Scripts: Run on a Timer
A Scheduled script has a single entry point, execute, that NetSuite runs on a schedule you define or on demand. It is the simplest way to automate a recurring job. The classic pattern is to run a saved search, loop over the results, and update each record. For moderate volumes this is perfect. For very large volumes you should watch the governance meter with N/runtime and reschedule the script before you run out of units.
/**
* @NApiVersion 2.1
* @NScriptType ScheduledScript
*/
define(['N/search', 'N/record', 'N/runtime', 'N/log'],
(search, record, runtime, log) => {
const execute = (context) => {
const overdue = search.load({ id: 'customsearch_overdue_invoices' });
overdue.run().each((result) => {
const invId = result.id;
record.submitFields({
type: record.Type.INVOICE,
id: invId,
values: { custbody_reminder_sent: true }
});
// Stop early if we are running low on governance
if (runtime.getCurrentScript().getRemainingUsage() < 100) {
log.audit('Rescheduling', 'Low on units, will resume');
return false; // break out of the loop
}
return true;
});
};
return { execute };
});
Map/Reduce Scripts: Built for Scale
When you need to process tens of thousands of records reliably, the Map/Reduce script is the right tool. It breaks a job into four stages that NetSuite orchestrates for you. getInputData returns the full set of work, usually a search. map receives each result individually and can do the per-record work or hand keyed data to the next stage. reduce receives all values grouped by key, which is ideal for aggregation. Finally summarize runs once at the end so you can log totals and catch errors. Because each map and reduce invocation gets its own governance budget, the framework parallelizes and yields automatically, so you rarely worry about hitting a wall.
/**
* @NApiVersion 2.1
* @NScriptType MapReduceScript
*/
define(['N/search', 'N/record', 'N/log'],
(search, record, log) => {
const getInputData = () => {
return search.create({
type: 'salesorder',
filters: [['status', 'anyof', 'SalesOrd:B']], // Pending Fulfillment
columns: ['internalid']
});
};
const map = (context) => {
const result = JSON.parse(context.value);
const soId = result.id;
const warehouse = result.values.location;
// Key by warehouse so reduce receives all orders for one location
context.write({ key: warehouse, value: soId });
};
const reduce = (context) => {
context.values.forEach((soId) => {
record.submitFields({
type: record.Type.SALES_ORDER,
id: soId,
values: { custbody_batch_flagged: true }
});
});
};
const summarize = (summary) => {
log.audit('Finished', 'Usage: ' + summary.usage +
', Yields: ' + summary.yields);
summary.mapSummary.errors.iterator().each((key, err) => {
log.error('Map error on ' + key, err);
return true;
});
};
return { getInputData, map, reduce, summarize };
});
A Real-World Example: Month-End Fee Accrual for 40,000 Transactions
Consider a payments company that must calculate and post a small processing fee against every transaction from the previous month, roughly 40,000 records. A Scheduled script would run the risk of exhausting its governance budget long before it finished, forcing awkward reschedule logic. Instead the team builds a Map/Reduce script. In getInputData a saved search returns all of last month’s transactions. Each map call computes the fee for one transaction and writes the amount keyed by customer. Each reduce call then receives every fee for a single customer, sums them, and creates one consolidated journal entry per customer instead of 40,000 tiny ones. When the run finishes, summarize logs the total usage consumed and surfaces any records that errored so finance can review them. What would have been a fragile overnight batch becomes a self-managing job that scales with volume, recovers gracefully, and produces clean, auditable output.
Choosing Between Scheduled and Map/Reduce
Reach for a Scheduled script when the job is simple, the volume is predictable and modest, and you want the least ceremony. Reach for a Map/Reduce script when volumes are large or unpredictable, when you need aggregation across records, or when reliability and automatic governance handling matter more than simplicity. Both run in the background on a schedule you control through the deployment record, and both are essential parts of a mature NetSuite customization strategy. Together with the User Event and Client scripts from the previous post, they give you a complete toolkit for automating NetSuite from the moment a user touches a record all the way through the largest overnight batch.
Discover more from The NetSuite Pro
Subscribe to get the latest posts sent to your email.
Leave a Reply