Skip to main content

Map/Reduce Script Development

Map/Reduce scripts process large volumes of data by breaking work into smaller chunks that run in parallel, then combining results.


When to Use Map/Reduce

Use CaseExample
Mass updatesUpdate all customer records
Data migrationImport thousands of records
Complex calculationsRecalculate pricing across catalog
Report generationAggregate data from many sources
Batch processingProcess large file imports

Map/Reduce vs Scheduled Script

CriteriaMap/ReduceScheduled Script
Data volume10,000+ records< 10,000 records
ParallelismBuilt-inManual
GovernancePer stage10,000 units total
ComplexityHigherLower
ResumabilityAutomaticManual

Map/Reduce Execution Flow

┌─────────────────────────────────────────────────────────────────────────────┐
│ MAP/REDUCE EXECUTION FLOW │
└─────────────────────────────────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────────────────────┐
│ 1. getInputData() │
│ ────────────────────────────────────────────────────────────────│
│ • Runs ONCE at start │
│ • Returns data to process │
│ • Can return: Search, Array, or Object │
│ • Governance: 10,000 units │
└──────────────────────────────┬───────────────────────────────────┘

Returns search or array of items


┌──────────────────────────────────────────────────────────────────┐
│ 2. map(context) │
│ ────────────────────────────────────────────────────────────────│
│ • Runs for EACH item from getInputData │
│ • Runs in PARALLEL (multiple at once) │
│ • Transform or filter data │
│ • Write key-value pairs for reduce │
│ • Governance: 1,000 units per invocation │
└──────────────────────────────┬───────────────────────────────────┘

┌───────────────────────────┼───────────────────────────┐
│ │ │
▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐
│ map(1) │ │ map(2) │ │ map(3) │
│ Item 1 │ │ Item 2 │ │ Item 3 │
└────┬────┘ └────┬────┘ └────┬────┘
│ │ │
│ context.write() │ context.write() │ context.write()
│ key: "A" │ key: "A" │ key: "B"
│ value: data1 │ value: data2 │ value: data3
│ │ │
└──────────────────────────┼──────────────────────────┘

Keys grouped together


┌──────────────────────────────────────────────────────────────────┐
│ 3. reduce(context) │
│ ────────────────────────────────────────────────────────────────│
│ • Runs for EACH UNIQUE KEY │
│ • Receives all values for that key │
│ • Aggregate, summarize, or process │
│ • Write results for summarize │
│ • Governance: 5,000 units per invocation │
└──────────────────────────────┬───────────────────────────────────┘

┌───────────────────────────┴───────────────────────────┐
│ │
▼ ▼
┌───────────────────────────┐ ┌───────────────────────────┐
│ reduce(key="A") │ │ reduce(key="B") │
│ values: [data1, data2] │ │ values: [data3] │
└─────────────┬─────────────┘ └─────────────┬─────────────┘
│ │
│ context.write() │ context.write()
│ key: "A" │ key: "B"
│ value: result_A │ value: result_B
│ │
└─────────────────────┬─────────────────────┘


┌──────────────────────────────────────────────────────────────────┐
│ 4. summarize(context) │
│ ────────────────────────────────────────────────────────────────│
│ • Runs ONCE at end │
│ • Access all outputs from reduce │
│ • Send summary email │
│ • Log completion │
│ • Governance: 10,000 units │
└──────────────────────────────────────────────────────────────────┘

Stage Data Flow

┌─────────────────────────────────────────────────────────────────────────────┐
│ DATA FLOW BETWEEN STAGES │
└─────────────────────────────────────────────────────────────────────────────┘

getInputData()

│ Returns: Search OR Array OR Object

│ Example Search Return:
│ ┌────────────────────────────────────────────┐
│ │ search.create({ │
│ │ type: 'salesorder', │
│ │ filters: [...], │
│ │ columns: [...] │
│ │ }) │
│ └────────────────────────────────────────────┘


map(context)

│ context.key = Internal ID (from search) or index (from array)
│ context.value = Search result or array element (as JSON string)

│ Parse the value:
│ ┌────────────────────────────────────────────┐
│ │ const data = JSON.parse(context.value); │
│ └────────────────────────────────────────────┘

│ Write to reduce:
│ ┌────────────────────────────────────────────┐
│ │ context.write({ │
│ │ key: 'category_A', ◄── Group key │
│ │ value: processedData ◄── Any data │
│ │ }); │
│ └────────────────────────────────────────────┘


reduce(context)

│ context.key = The key you wrote in map
│ context.values = Array of ALL values for this key

│ ┌────────────────────────────────────────────┐
│ │ context.values.forEach(value => { │
│ │ const data = JSON.parse(value); │
│ │ // Process... │
│ │ }); │
│ └────────────────────────────────────────────┘

│ Write to summarize:
│ ┌────────────────────────────────────────────┐
│ │ context.write({ │
│ │ key: context.key, │
│ │ value: aggregatedResult │
│ │ }); │
│ └────────────────────────────────────────────┘


summarize(context)

│ Access outputs from reduce:
│ ┌────────────────────────────────────────────┐
│ │ context.output.iterator().each( │
│ │ (key, value) => { │
│ │ // Process each reduce output │
│ │ return true; │
│ │ } │
│ │ ); │
│ └────────────────────────────────────────────┘


COMPLETE

Basic Map/Reduce Structure

/**
* @NApiVersion 2.1
* @NScriptType MapReduceScript
* @NModuleScope SameAccount
*/
define(['N/search', 'N/record', 'N/runtime', 'N/log'],
(search, record, runtime, log) => {

/**
* Get input data to process
* @returns {search.Search|Array|Object}
*/
const getInputData = () => {
log.audit('Stage', 'getInputData started');

// Return a search - each result becomes a map() call
return search.create({
type: search.Type.SALES_ORDER,
filters: [
['mainline', 'is', 'T'],
'AND',
['status', 'anyof', 'SalesOrd:B']
],
columns: ['tranid', 'entity', 'total']
});
};

/**
* Process each input record
* @param {Object} context
* @param {string} context.key - Record ID
* @param {string} context.value - Record data (JSON string)
*/
const map = (context) => {
const searchResult = JSON.parse(context.value);

log.debug('Map', `Processing order ID: ${context.key}`);

// Do processing
const processed = {
id: context.key,
tranId: searchResult.values.tranid,
amount: parseFloat(searchResult.values.total) || 0
};

// Group by customer
const customerId = searchResult.values.entity.value;

context.write({
key: customerId,
value: processed
});
};

/**
* Process grouped data
* @param {Object} context
* @param {string} context.key - Group key
* @param {Array} context.values - All values for this key
*/
const reduce = (context) => {
log.debug('Reduce', `Processing customer: ${context.key}`);

let totalAmount = 0;
let orderCount = 0;

context.values.forEach(value => {
const data = JSON.parse(value);
totalAmount += data.amount;
orderCount++;
});

// Write summary for this customer
context.write({
key: context.key,
value: JSON.stringify({
customerId: context.key,
orderCount: orderCount,
totalAmount: totalAmount
})
});
};

/**
* Final summary
* @param {Object} context
* @param {Object} context.inputSummary
* @param {Object} context.mapSummary
* @param {Object} context.reduceSummary
* @param {Iterator} context.output
*/
const summarize = (context) => {
log.audit('Stage', 'summarize started');

// Check for errors
handleErrors(context);

// Process outputs from reduce
let totalCustomers = 0;
let grandTotal = 0;

context.output.iterator().each((key, value) => {
const data = JSON.parse(value);
totalCustomers++;
grandTotal += data.totalAmount;

log.debug('Customer Summary', data);
return true;
});

log.audit('Complete', `Processed ${totalCustomers} customers, total: $${grandTotal}`);
};

/**
* Handle errors from all stages
*/
const handleErrors = (context) => {
// Input stage errors
if (context.inputSummary.error) {
log.error('Input Error', context.inputSummary.error);
}

// Map stage errors
context.mapSummary.errors.iterator().each((key, error) => {
log.error('Map Error', `Key: ${key}, Error: ${error}`);
return true;
});

// Reduce stage errors
context.reduceSummary.errors.iterator().each((key, error) => {
log.error('Reduce Error', `Key: ${key}, Error: ${error}`);
return true;
});
};

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

Return Types for getInputData

const getInputData = () => {
// Each search result becomes one map() invocation
return search.create({
type: search.Type.CUSTOMER,
filters: [['isinactive', 'is', 'F']],
columns: ['companyname', 'email', 'salesrep']
});
};

Return an Array

const getInputData = () => {
// Each array element becomes one map() invocation
return [
{ id: 1, name: 'Item A', price: 100 },
{ id: 2, name: 'Item B', price: 200 },
{ id: 3, name: 'Item C', price: 300 }
];
};

Return an Object

const getInputData = () => {
// Each key-value pair becomes one map() invocation
// context.key = the object key
// context.value = the value
return {
'customer_1': { name: 'Acme Corp', amount: 1000 },
'customer_2': { name: 'XYZ Inc', amount: 2000 },
'customer_3': { name: 'ABC Ltd', amount: 3000 }
};
};

Return Search ID

const getInputData = () => {
const script = runtime.getCurrentScript();
const searchId = script.getParameter({
name: 'custscript_search_id'
});

// Load existing saved search
return search.load({ id: searchId });
};

Complete Example: Mass Price Update

/**
* @NApiVersion 2.1
* @NScriptType MapReduceScript
* @NModuleScope SameAccount
* @description Updates item prices based on cost plus markup
*/
define(['N/search', 'N/record', 'N/runtime', 'N/email', 'N/log'],
(search, record, runtime, email, log) => {

/**
* Get all inventory items to update
*/
const getInputData = () => {
log.audit('Price Update', 'Starting getInputData');

const script = runtime.getCurrentScript();
const markupPercent = script.getParameter({
name: 'custscript_markup_percent'
}) || 30;

log.debug('Parameters', `Markup: ${markupPercent}%`);

return search.create({
type: search.Type.INVENTORY_ITEM,
filters: [
['isinactive', 'is', 'F'],
'AND',
['type', 'anyof', 'InvtPart']
],
columns: [
'itemid',
'displayname',
'cost',
'baseprice',
'custitem_category'
]
});
};

/**
* Process each item - calculate new price
*/
const map = (context) => {
try {
const searchResult = JSON.parse(context.value);
const itemId = context.key;
const values = searchResult.values;

const cost = parseFloat(values.cost) || 0;
const currentPrice = parseFloat(values.baseprice) || 0;
const category = values.custitem_category?.value || 'uncategorized';

// Get markup from parameters
const script = runtime.getCurrentScript();
const markupPercent = script.getParameter({
name: 'custscript_markup_percent'
}) || 30;

// Calculate new price
const newPrice = cost * (1 + markupPercent / 100);

// Only update if price changed significantly (>1%)
const priceChange = Math.abs(newPrice - currentPrice) / (currentPrice || 1);

if (priceChange > 0.01 && cost > 0) {
context.write({
key: category,
value: JSON.stringify({
itemId: itemId,
itemName: values.itemid,
cost: cost,
oldPrice: currentPrice,
newPrice: newPrice,
change: ((newPrice - currentPrice) / (currentPrice || 1) * 100).toFixed(2)
})
});
}

} catch (e) {
log.error('Map Error', `Item ${context.key}: ${e.message}`);
}
};

/**
* Update items by category
*/
const reduce = (context) => {
const category = context.key;
let updatedCount = 0;
let errorCount = 0;
const updates = [];

log.debug('Reduce', `Processing category: ${category}`);

context.values.forEach(value => {
try {
const item = JSON.parse(value);

// Update the item
record.submitFields({
type: record.Type.INVENTORY_ITEM,
id: item.itemId,
values: {
'baseprice': item.newPrice
}
});

updates.push({
itemName: item.itemName,
oldPrice: item.oldPrice,
newPrice: item.newPrice,
change: item.change
});

updatedCount++;

} catch (e) {
log.error('Update Error', `Item ${item?.itemId}: ${e.message}`);
errorCount++;
}
});

// Write summary for this category
context.write({
key: category,
value: JSON.stringify({
category: category,
updatedCount: updatedCount,
errorCount: errorCount,
updates: updates
})
});
};

/**
* Send summary email
*/
const summarize = (context) => {
log.audit('Price Update', 'Summarize started');

// Handle errors
logErrors(context);

// Gather results
const results = [];
let totalUpdated = 0;
let totalErrors = 0;

context.output.iterator().each((key, value) => {
const data = JSON.parse(value);
results.push(data);
totalUpdated += data.updatedCount;
totalErrors += data.errorCount;
return true;
});

// Build email body
const emailBody = buildEmailBody(results, totalUpdated, totalErrors);

// Send notification
const script = runtime.getCurrentScript();
const recipientId = script.getParameter({
name: 'custscript_notify_user'
});

if (recipientId) {
email.send({
author: runtime.getCurrentUser().id,
recipients: [recipientId],
subject: `Price Update Complete - ${totalUpdated} Items Updated`,
body: emailBody
});

log.audit('Email Sent', `Notification sent to ${recipientId}`);
}

// Log summary
log.audit('Price Update Complete', {
totalUpdated: totalUpdated,
totalErrors: totalErrors,
categories: results.length
});
};

/**
* Log all errors from each stage
*/
const logErrors = (context) => {
// Input errors
if (context.inputSummary.error) {
log.error('Input Stage Error', context.inputSummary.error);
}

// Map errors
context.mapSummary.errors.iterator().each((key, error) => {
log.error('Map Stage Error', { key: key, error: error });
return true;
});

// Reduce errors
context.reduceSummary.errors.iterator().each((key, error) => {
log.error('Reduce Stage Error', { key: key, error: error });
return true;
});
};

/**
* Build HTML email body
*/
const buildEmailBody = (results, totalUpdated, totalErrors) => {
let html = `
<html>
<head>
<style>
body { font-family: Arial, sans-serif; }
h2 { color: #333; }
table { border-collapse: collapse; width: 100%; margin: 20px 0; }
th { background-color: #4CAF50; color: white; padding: 10px; }
td { padding: 8px; border: 1px solid #ddd; }
.summary { background: #f5f5f5; padding: 15px; border-radius: 5px; }
.error { color: #d32f2f; }
.success { color: #388e3c; }
</style>
</head>
<body>
<h2>Price Update Summary</h2>

<div class="summary">
<p><strong>Total Items Updated:</strong> <span class="success">${totalUpdated}</span></p>
<p><strong>Total Errors:</strong> <span class="error">${totalErrors}</span></p>
<p><strong>Categories Processed:</strong> ${results.length}</p>
</div>

<h3>Updates by Category</h3>
<table>
<tr>
<th>Category</th>
<th>Updated</th>
<th>Errors</th>
</tr>
`;

results.forEach(result => {
html += `
<tr>
<td>${result.category}</td>
<td>${result.updatedCount}</td>
<td class="error">${result.errorCount}</td>
</tr>
`;
});

html += `
</table>

<h3>Detailed Changes</h3>
`;

results.forEach(result => {
if (result.updates.length > 0) {
html += `
<h4>${result.category}</h4>
<table>
<tr>
<th>Item</th>
<th>Old Price</th>
<th>New Price</th>
<th>Change</th>
</tr>
`;

result.updates.slice(0, 20).forEach(update => {
html += `
<tr>
<td>${update.itemName}</td>
<td>$${parseFloat(update.oldPrice).toFixed(2)}</td>
<td>$${parseFloat(update.newPrice).toFixed(2)}</td>
<td>${update.change}%</td>
</tr>
`;
});

if (result.updates.length > 20) {
html += `<tr><td colspan="4">... and ${result.updates.length - 20} more</td></tr>`;
}

html += '</table>';
}
});

html += `
<p style="color: #666; font-size: 12px; margin-top: 30px;">
This is an automated message from NetSuite.
</p>
</body>
</html>
`;

return html;
};

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

Script Deployment XML

<?xml version="1.0" encoding="UTF-8"?>
<mapreducescript scriptid="customscript_price_update_mr">
<name>Mass Price Update</name>
<scriptfile>[/SuiteScripts/MapReduce/price_update_mr.js]</scriptfile>
<description>Updates item prices based on cost plus markup</description>
<isinactive>F</isinactive>
<notifyowner>T</notifyowner>

<scriptcustomfields>
<scriptcustomfield scriptid="custscript_markup_percent">
<label>Markup Percentage</label>
<fieldtype>PERCENT</fieldtype>
<defaultvalue>30</defaultvalue>
<ismandatory>T</ismandatory>
</scriptcustomfield>

<scriptcustomfield scriptid="custscript_notify_user">
<label>Notification Recipient</label>
<fieldtype>SELECT</fieldtype>
<selectrecordtype>-4</selectrecordtype>
</scriptcustomfield>

<scriptcustomfield scriptid="custscript_search_id">
<label>Item Search ID</label>
<fieldtype>SELECT</fieldtype>
<selectrecordtype>-119</selectrecordtype>
<description>Optional: Use specific saved search</description>
</scriptcustomfield>
</scriptcustomfields>

<scriptdeployments>
<scriptdeployment scriptid="customdeploy_price_update_mr">
<status>RELEASED</status>
<title>Price Update - Manual Run</title>
<isdeployed>T</isdeployed>
<loglevel>DEBUG</loglevel>
<allroles>F</allroles>
<audslctrole>
<role>[Administrator]</role>
</audslctrole>
</scriptdeployment>
</scriptdeployments>
</mapreducescript>

Skipping Stages

You don't need all four stages. Common patterns:

Map Only (No Grouping Needed)

const getInputData = () => {
return search.create({ /* ... */ });
};

const map = (context) => {
// Process each record directly
const data = JSON.parse(context.value);
updateRecord(data);

// Write directly to output (skip reduce)
context.write({
key: context.key,
value: 'processed'
});
};

// No reduce function

const summarize = (context) => {
let count = 0;
context.output.iterator().each(() => {
count++;
return true;
});
log.audit('Complete', `Processed ${count} records`);
};

return { getInputData, map, summarize };

Map and Reduce (Most Common)

return {
getInputData,
map,
reduce,
summarize
};

Governance Per Stage

┌─────────────────────────────────────────────────────────────────────────────┐
│ GOVERNANCE LIMITS BY STAGE │
└─────────────────────────────────────────────────────────────────────────────┘

┌────────────────────────┬─────────────────┬──────────────────────────────────┐
│ Stage │ Units/Run │ Notes │
├────────────────────────┼─────────────────┼──────────────────────────────────┤
│ getInputData │ 10,000 │ Runs once, defines all work │
├────────────────────────┼─────────────────┼──────────────────────────────────┤
│ map │ 1,000 │ Per invocation (parallel) │
│ │ │ Auto-retries on failure │
├────────────────────────┼─────────────────┼──────────────────────────────────┤
│ reduce │ 5,000 │ Per unique key │
│ │ │ More units for aggregation │
├────────────────────────┼─────────────────┼──────────────────────────────────┤
│ summarize │ 10,000 │ Runs once at end │
│ │ │ For final reporting │
└────────────────────────┴─────────────────┴──────────────────────────────────┘

┌──────────────────────────────────────────────────────────────────┐
│ TOTAL SCRIPT EXECUTION │
│ ────────────────────────────────────────────────────────────────│
│ No total limit - stages have individual limits │
│ Script runs until all data processed │
│ Automatic retry on transient failures │
└──────────────────────────────────────────────────────────────────┘

Error Handling

In Map Stage

const map = (context) => {
try {
const data = JSON.parse(context.value);
processData(data);

context.write({
key: 'success',
value: context.key
});

} catch (e) {
// Log error - script continues with next item
log.error('Map Error', {
key: context.key,
error: e.message
});

// Optionally write to error bucket
context.write({
key: 'error',
value: JSON.stringify({
id: context.key,
error: e.message
})
});
}
};

In Summarize Stage

const summarize = (context) => {
// Check input stage
if (context.inputSummary.error) {
log.error('Input Failed', context.inputSummary.error);
sendErrorNotification('getInputData failed: ' + context.inputSummary.error);
return;
}

// Check map stage
let mapErrors = 0;
context.mapSummary.errors.iterator().each((key, error) => {
mapErrors++;
log.error('Map Error', `${key}: ${error}`);
return true;
});

// Check reduce stage
let reduceErrors = 0;
context.reduceSummary.errors.iterator().each((key, error) => {
reduceErrors++;
log.error('Reduce Error', `${key}: ${error}`);
return true;
});

if (mapErrors > 0 || reduceErrors > 0) {
sendErrorNotification(`Errors: Map=${mapErrors}, Reduce=${reduceErrors}`);
}
};

Triggering Map/Reduce from Code

const task = require('N/task');

const triggerMapReduce = () => {
const mrTask = task.create({
taskType: task.TaskType.MAP_REDUCE,
scriptId: 'customscript_price_update_mr',
deploymentId: 'customdeploy_price_update_mr',
params: {
'custscript_markup_percent': 25
}
});

const taskId = mrTask.submit();

log.audit('Map/Reduce Started', `Task ID: ${taskId}`);

return taskId;
};

// Check status
const checkStatus = (taskId) => {
const taskStatus = task.checkStatus({
taskId: taskId
});

log.debug('Task Status', {
status: taskStatus.status,
stage: taskStatus.stage,
percentComplete: taskStatus.getPercentageCompleted()
});

return taskStatus;
};

Best Practices

PracticeDescription
Keep map lightweightDo heavy processing in reduce
Use appropriate keysGroup logically for reduce
Handle errors gracefullyLog errors, continue processing
Check governanceMonitor in summarize stage
Parse JSON carefullyAlways wrap in try/catch
Limit reduce workAvoid too many values per key

Common Patterns

Progress Tracking

let processedInMap = 0;

const map = (context) => {
processedInMap++;

if (processedInMap % 100 === 0) {
log.audit('Progress', `Processed ${processedInMap} in map`);
}

// Process...
};

Conditional Processing

const map = (context) => {
const data = JSON.parse(context.value);

// Skip based on condition
if (data.values.isinactive === 'T') {
return; // Don't write - skip this record
}

// Process active records
context.write({
key: data.values.category,
value: data
});
};

Next Steps