NetSuiteFree generatorPDF, Excel, Word exportNetSuite

SuiteScript Map/Reduce Skeleton Generator

Map/reduce is the SuiteScript type for bulk work, because NetSuite splits input into many small invocations and handles queueing and retries. This generator creates a working skeleton with an input query, map, reduce and a summarize stage that reports errors.

Fill in

Generated code
Download:

map_reduce_skeleton.js

/**
 * @NApiVersion 2.1
 * @NScriptType MapReduceScript
 * @name Close Old Sales Orders
 * @author Your Name
 * @version 1.0 (2026-10-20)
 */
define(['N/record', 'N/query', 'N/log'], (record, query, log) => {

  const getInputData = () => {
    const sql = "SELECT id FROM transaction WHERE type = 'SalesOrd' AND status = 'B' AND trandate < TO_DATE('2025-01-01', 'YYYY-MM-DD')";
    return query.runSuiteQL({ query: sql }).asMappedResults();
  };

  const map = (context) => {
    try {
      const row = JSON.parse(context.value);
      // Emit by key. Rows with the same key reach the same reduce call.
      context.write({ key: String(row.id), value: row.id });
    } catch (e) {
      log.error({ title: 'map failed', details: e });
      throw e;
    }
  };

  const reduce = (context) => {
    try {
      const id = context.key;
      record.submitFields({
        type: record.Type.SALES_ORDER,
        id: id,
        values: { memo: 'Processed by Close Old Sales Orders' },
        options: { enableSourcing: false, ignoreMandatoryFields: true }
      });
      context.write({ key: id, value: 'ok' });
    } catch (e) {
      log.error({ title: 'reduce failed for ' + context.key, details: e });
      throw e;
    }
  };

  const summarize = (summary) => {
    let processed = 0;
    let failed = 0;
    summary.output.iterator().each((key, value) => { processed += 1; return true; });
    summary.mapSummary.errors.iterator().each((key, error) => {
      failed += 1;
      log.error({ title: 'Map error ' + key, details: error });
      return true;
    });
    summary.reduceSummary.errors.iterator().each((key, error) => {
      failed += 1;
      log.error({ title: 'Reduce error ' + key, details: error });
      return true;
    });
    if (summary.inputSummary.error) {
      log.error({ title: 'Input error', details: summary.inputSummary.error });
    }
    log.audit({ title: 'Done', details: 'processed=' + processed + ' failed=' + failed + ' usage=' + summary.usage });
  };

  return { getInputData, map, reduce, summarize };
});

Deployment settings to check

Concurrency limit: start low (for example 2 to 5) and raise it after testing.
Yield after minutes: leave the default unless you see long-running map calls.
Buffer size: leave the default.
Status: Testing with a narrow input query until verified.

The example updates the memo of matching records, which is a harmless placeholder. Replace the reduce logic with your real change and test it on a handful of records. Verify the input query returns the intended rows before enabling a wide deployment.

Batch job design review

Optional review of a NetSuite bulk update design, concurrency and error handling.

Optional. The tool and every export work without this. No spam.

How to use the generator

Set a script name, author, record type and the SuiteQL query that selects the rows to process. Copy the generated file to the File Cabinet, create the script record and a deployment, and run it against a small input first. Replace the reduce body with your actual update. The summarize stage logs every map and reduce error with its key, which is what you will read after a run.

How the four stages work

getInputData returns the list of work, from a query, a search or an array. map receives one input row at a time and writes key and value pairs. reduce receives all values for one key, which lets you group related rows into one unit of work, such as all lines for one order. summarize runs once at the end with counts, usage and every error collected from earlier stages.

  • Each map and reduce call has its own governance allowance
  • Values passed between stages must be JSON serializable
  • Rows are processed in parallel, so do not rely on order
  • Throwing in map or reduce records the error and continues

Design guidance

Choose the key in map carefully, since it decides how work groups in reduce. Use the record ID when each row is independent, and a parent ID when child rows must be updated together. Keep concurrency modest to avoid record locking and to stay inside the account concurrency limit shared with integrations. Prefer submitFields for small changes, because it costs far fewer units than loading and saving a transaction.

Common mistakes

Common issues are returning huge objects from getInputData that exceed storage limits, writing values that are not serializable, updating the same record from several reduce calls at once and hitting a lock error, and ignoring summarize so failures go unnoticed. Always test with a small input, check the Map/Reduce Script Status page, and confirm results with a saved search or SuiteQL query.

Frequently Asked Questions

When should I use map/reduce instead of a scheduled script?

Use map/reduce when you process thousands of records, need parallelism or want automatic retry handling. A scheduled script is simpler for small, sequential jobs, but it has one governance allowance for the entire run.

What does the key in map control?

All values written with the same key are delivered to a single reduce call. Choose a key that groups the data that must be handled together, such as a parent record ID.

How do I find errors after a run?

Read the summarize stage logs, which iterate over map and reduce errors, and check Customization > Scripting > Map/Reduce Script Status for stage progress and failures.

Can getInputData use SuiteQL?

Yes. Return query.runSuiteQL(...).asMappedResults() or a paged query, and keep the result size reasonable. You can also return a saved search or an array of objects.

If you run large NetSuite batch jobs, Netray can help design them and add read-only AI checks over the results, with approval on any write-back.