SuiteScript 2.1 Scheduled Script Generator for NetSuite
This generator writes a SuiteScript 2.1 scheduled script that works through a saved search in internal ID order, applies one action per record, tracks remaining governance and resubmits itself with the last processed ID. It is for developers who need a batch job that survives the 10,000-unit limit without skipping or repeating records.
Fill in
Script ID or internal ID. Use Main Line = true so each transaction appears once.
submitFields sets a checkbox, closeLines closes every item line, logOnly is a dry run.
Must cover the most expensive single iteration plus the reschedule call.
Create it on the script record as an Integer Number parameter.
Scheduled script (upload to the File Cabinet, create a Script record)
/**
* @NApiVersion 2.1
* @NScriptType ScheduledScript
* @NModuleScope SameAccount
*/
// Mark processed sales orders
define(['N/search', 'N/record', 'N/runtime', 'N/task', 'N/log'], (search, record, runtime, task, log) => {
const SEARCH_ID = 'customsearch_open_so_to_close';
const ACTION = 'submitFields'; // submitFields | closeLines | logOnly
const FIELD_ID = 'custbody_processed';
const UNITS_FLOOR = 200;
const PARAM_ID = 'custscript_last_id';
const RESCHEDULE = 'Yes' === 'Yes';
const BATCH = 1000;
const processResult = (result) => {
if (ACTION === 'submitFields') {
record.submitFields({
type: result.recordType,
id: result.id,
values: { [FIELD_ID]: true },
options: { enableSourcing: false, ignoreMandatoryFields: true }
});
} else if (ACTION === 'closeLines') {
const rec = record.load({ type: result.recordType, id: result.id });
const lineCount = rec.getLineCount({ sublistId: 'item' });
for (let i = 0; i < lineCount; i++) {
rec.setSublistValue({ sublistId: 'item', fieldId: 'isclosed', line: i, value: true });
}
rec.save({ ignoreMandatoryFields: true });
} else {
log.debug({ title: 'Would process', details: result.recordType + ' ' + result.id });
}
};
const reschedule = (lastId) => {
const scheduledTask = task.create({
taskType: task.TaskType.SCHEDULED_SCRIPT,
scriptId: runtime.getCurrentScript().id,
params: { [PARAM_ID]: lastId }
});
const taskId = scheduledTask.submit();
log.audit({ title: 'Rescheduled', details: 'Task ' + taskId + ', resume after internal id ' + lastId });
};
const execute = (context) => {
const script = runtime.getCurrentScript();
let lastId = Number(script.getParameter({ name: PARAM_ID })) || 0;
log.audit({ title: 'Start', details: 'type=' + context.type + ', after internal id ' + lastId });
const savedSearch = search.load({ id: SEARCH_ID });
const baseFilters = savedSearch.filters.slice();
savedSearch.columns = [search.createColumn({ name: 'internalid', sort: search.Sort.ASC })];
let processed = 0;
while (true) {
savedSearch.filters = baseFilters.concat([
search.createFilter({ name: 'internalidnumber', operator: search.Operator.GREATERTHAN, values: [lastId] })
]);
const results = savedSearch.run().getRange({ start: 0, end: BATCH });
if (results.length === 0) break;
for (const result of results) {
if (script.getRemainingUsage() < UNITS_FLOOR) {
log.audit({ title: 'Usage floor reached', details: processed + ' processed, last id ' + lastId });
if (RESCHEDULE) reschedule(lastId);
return;
}
try {
processResult(result);
} catch (e) {
log.error({ title: 'Failed ' + result.recordType + ' ' + result.id, details: e.name + ': ' + e.message });
}
lastId = Number(result.id);
processed++;
}
if (results.length < BATCH) break;
}
log.audit({ title: 'Done', details: processed + ' records processed, remaining units ' + script.getRemainingUsage() });
};
return { execute };
});Verify costs against the governance table for your release: scheduled scripts get 10,000 units; search.load 5, ResultSet.getRange 10, record.submitFields 10 for transactions (5 for other standard records, 2 for custom records), record.load 10 and Record.save 20 for transactions, ScheduledScriptTask.submit 20. The reschedule call omits deploymentId so NetSuite picks an available deployment; keep a spare deployment with status Not Scheduled so one is free while the current run is still executing.
Get your batch script reviewed
Send us the script and the record volumes and we will check governance, restart safety and whether map/reduce fits better.
Optional. The tool and every export work without this. No spam.
How to use it
Build the saved search first, with the criteria that select records to process and Main Line set to true for transaction searches. Choose an action, set the field and parameter IDs, and copy the code into a .js file. Upload it to the File Cabinet (SuiteScripts folder), create a Script record from it, and add an Integer Number parameter with the ID you entered. Create a spare deployment with status Not Scheduled so the script can resubmit itself, then run one with Save and Execute, or schedule it. Start with logOnly to confirm the record count before writing anything.
- The deploying role needs the SuiteScript permission plus edit rights on the target records.
- Execution logs are on the deployment record and in Script Execution Logs.
- Set log level to Audit in production to keep logs readable.
How the restart logic works
The script replaces the saved search columns with internal ID sorted ascending and adds a filter internalidnumber greater than the last processed ID. It then reads batches of up to 1000 with getRange from offset 0 every time. Because each batch starts after the last ID rather than at a page offset, records that drop out of the search after being updated (for example when the search excludes custbody_processed = true) cannot shift later pages and cause skips. Before each record it checks runtime.getCurrentScript().getRemainingUsage(); below the floor it submits a new scheduled task with the last ID as parameter and returns. context.type tells you whether the run was SCHEDULED or ON_DEMAND.
Worked governance example
With submitFields on sales orders, each record costs 10 units, plus 10 per batch of 1000 results and 5 for the initial search.load. A run starting with 10,000 units and a 200-unit floor processes about 978 orders before it reschedules (10,000 minus 5, minus 10 for the batch, minus the 200 floor, divided by 10). The closeLines action costs 30 per order (load 10, save 20), so about 326 orders per run. Logging is free in units but slow. If you need thousands of records per hour or parallel processing, a map/reduce script is the better fit; a scheduled script runs on one processor at a time.
- submitFields on a custom record costs 2 units.
- Keep at least 20 units of floor for the reschedule submit.
- Time limits apply as well as units: very slow saves can end a run early.
Common mistakes
Paging with runPaged or getRange offsets while the loop changes the records the search returns, which silently skips records. Leaving Main Line off, so each line of a transaction appears as a separate result and the record is updated many times. Hard-coding deploymentId to the running deployment, which fails because it is already executing. Forgetting to create the script parameter, so the restart point is always zero. Setting the floor too low for closeLines, where one iteration costs 30 units. Not wrapping each record in try/catch, so one locked or invalid record stops the whole run.
Frequently Asked Questions
How do I load a saved search in SuiteScript 2.1?
Use search.load({ id: 'customsearch_id' }), which costs 5 units, then run it with search.run().getRange({ start, end }) for up to 1000 results at a time, or runPaged({ pageSize }) and fetch each page. You can add filters with search.createFilter and replace columns before running.
How many governance units does a scheduled script have?
A scheduled script gets 10,000 units per execution. Check what is left with runtime.getCurrentScript().getRemainingUsage() and resubmit the script with task.create of type SCHEDULED_SCRIPT before you run out, passing the restart point as a script parameter. Submitting the task costs 20 units.
What permission is needed to schedule a SuiteScript?
The person creating the script record and deployment needs the SuiteScript permission, typically on a developer or administrator role. The deployment's Execute As Role decides what the script can read and change, and the records touched need matching permissions. Scheduling is set on the deployment's Schedule subtab.
When should I use map/reduce instead of a scheduled script?
Use map/reduce when the volume is large, the work per record is independent, and you want NetSuite to handle governance yielding and parallel processing. Keep a scheduled script for ordered, sequential work, small daily jobs, or when you need simple control over the restart point, as in this generator.
If you want an assistant that finds the records needing attention and drafts the batch job for review on your own infrastructure, we can build that with you.
Related Tools
SuiteScript 2.1 Client Script Generator for NetSuite
Generate a working client script for a transaction or customer form: field checks, line validation with a dialog, sublist totals and a save guard, exporting only the entry points you pick.
Developer ToolsSuiteScript 2.1 Suitelet Form Generator for NetSuite
Describe the filter fields and a SuiteQL query and get a working Suitelet that renders a NetSuite-styled form, runs the query on submit and lists the results.
NetSuiteSuiteScript Map/Reduce Skeleton Generator
A SuiteScript 2.1 map/reduce template with a SuiteQL input stage, map and reduce handlers and a summarize stage that reports every error.
NetSuiteSuiteScript Scheduled Script Run Count Calculator
Estimate total governance units, records per run, number of reschedules and total runtime for a scheduled script before you write it.
Developer ToolsNetSuite SuiteScript Governance Units by Script Type
A lookup of governance unit limits for each SuiteScript script type and the design guidance that follows from them.
More Developer Tools tools
See all- NetSuite SuiteScript 2.1 RESTlet Skeleton Generator
- NetSuite API Retry Backoff Wait Time Calculator
- NetSuite OAuth 2.0 Access Token Refresh Calculator
- NetSuite REST Web Services Headers and Query Parameters Reference
- SuiteScript 1.0 to 2.1 API Mapping Reference for NetSuite
- NetSuite REST API SuiteQL Request Generator
- NetSuite REST API Sales Order and Journal Entry Request Generator
- NetSuite Token-Based Auth OAuth 1.0 Signature Code Generator
- NetSuite OAuth 2.0 Client Credentials (M2M) JWT Code Generator
Keep exploring
Free tools
- NetSuite Workflow Action Script Generator (SuiteScript 2.1)Generate a working workflow action script that reads a threshold parameter, looks up an approver and returns a value your workflow can store in a field.
- SuiteScript 2.1 Client Script Generator for NetSuiteGenerate a working client script for a transaction or customer form: field checks, line validation with a dialog, sublist totals and a save guard, exporting only the entry points you pick.
- NetSuite SuiteScript 2.1 RESTlet Skeleton GeneratorFill in a script name, record type and fields to get a SuiteScript 2.1 RESTlet with GET lookup, POST create, logging and error handling.
ERP fixes and how-tos
- Fix NetSuite SSS_USAGE_LIMIT_EXCEEDED ErrorSSS_USAGE_LIMIT_EXCEEDED means your SuiteScript ran out of governance units. Learn the cause, how to check usage, and fixes using submitFields and Map/Reduce.
- Avoiding governance limit errors in NetSuite Map/Reduce scriptsUnderstand how governance is allocated per key in NetSuite Map/Reduce scripts, why SSS_USAGE_LIMIT_EXCEEDED still happens, and how to design getInputData, map, reduce and summarize stages that do not time out.
- How NetSuite scheduled script queueing actually worksUnderstand why NetSuite Scheduled Scripts sit in Queued or Pending status, how account-level concurrency limits and deployment priority affect start time, and how to design self-rescheduling scripts that do not compound the queue.
Integrations
- How to integrate Microsoft Teams with Oracle NetSuiteConnect Oracle NetSuite to Microsoft Teams: approval cards, alerts, bots and Entra ID sign-in using SuiteScript, RESTlets and Power Automate or Bot Framework.
- How to add WhatsApp to Oracle NetSuiteHow to connect WhatsApp Business to Oracle NetSuite: SuiteScript triggers, RESTlets, template messages, opt-in rules and two-way replies that post back to records.