🔹 Introduction
Map/Reduce scripts are built for handling large volumes of records in NetSuite. They divide work into four stages and automatically handle governance (pause and resume if limits are exceeded).
They’re ideal for:
- Bulk updates
- Data transformation
- Aggregations (grouping data, totals, summaries)
- Import/export processes
🔹 The 4 Stages of Map/Reduce
- getInputData → Define where the data comes from.
- map → Process each record individually.
- reduce → Combine or group results by key.
- summarize → Clean-up, log results, handle errors, send notifications.
🔹 Example 1: Simple Template
/**
*@NApiVersion 2.1
*@NScriptType MapReduceScript
*/
define(['N/search'], (search) => {
// Step 1: Provide input data (customers from a search)
const getInputData = () => {
try {
return search.create({
type: search.Type.CUSTOMER,
filters: [['isinactive', 'is', 'F']],
columns: ['internalid', 'entityid', 'email']
});
} catch (e) {
log.error('Error in getInputData', e.message);
}
};
// Step 2: Runs once per input (each customer)
const map = (context) => {
try {
const result = JSON.parse(context.value);
log.debug('Map Stage', `Customer ID: ${result.id}, Name: ${result.values.entityid}`);
// Pass the email forward to the reduce stage
context.write({
key: result.id,
value: result.values.email
});
} catch (e) {
log.error('Error in map', e.message);
}
};
// Step 3: Runs for each unique key from map stage
const reduce = (context) => {
try {
log.debug('Reduce Stage', `Key: ${context.key}, Values: ${context.values}`);
} catch (e) {
log.error('Error in reduce', e.message);
}
};
// Step 4: Runs once at the end – summarize results, log usage, handle errors
const summarize = (summary) => {
try {
log.debug('Summary', `Usage consumed: ${summary.usage}`);
if (summary.inputSummary.error) log.error('Input Error', summary.inputSummary.error);
summary.mapSummary.errors.iterator().each((key, error) => {
log.error('Map Error', `Key: ${key}, Error: ${error}`);
return true;
});
summary.reduceSummary.errors.iterator().each((key, error) => {
log.error('Reduce Error', `Key: ${key}, Error: ${error}`);
return true;
});
} catch (e) {
log.error('Error in summarize', e.message);
}
};
return { getInputData, map, reduce, summarize };
});
🔹 Example 2: Mass Update Email Domain
/**
*@NApiVersion 2.1
*@NScriptType MapReduceScript
*/
define(['N/search', 'N/record'], (search, record) => {
// Step 1: Search for customers with old test.com emails
const getInputData = () => {
try {
return search.create({
type: search.Type.CUSTOMER,
filters: [['email', 'contains', '@test.com']],
columns: ['internalid', 'email']
});
} catch (e) {
log.error('Error in getInputData', e.message);
}
};
// Step 2: Send each customer record forward to reduce
const map = (context) => {
try {
const result = JSON.parse(context.value);
context.write({
key: result.id,
value: result.values.email
});
} catch (e) {
log.error('Error in map', e.message);
}
};
// Step 3: Update each customer’s email in reduce stage
const reduce = (context) => {
try {
const custId = context.key;
let email = context.values[0];
const newEmail = email.replace('@test.com', '@example.com');
// More efficient than load+save
record.submitFields({
type: record.Type.CUSTOMER,
id: custId,
values: { email: newEmail }
});
log.debug('Updated', `Customer ${custId} email changed to ${newEmail}`);
} catch (e) {
log.error('Error in reduce', e.message);
}
};
// Step 4: Summarize results
const summarize = (summary) => {
try {
log.debug('Summary', `Total usage: ${summary.usage}, Yields: ${summary.yields}`);
} catch (e) {
log.error('Error in summarize', e.message);
}
};
return { getInputData, map, reduce, summarize };
});
🔹 Example 3: Count Sales Orders by Customer
/**
*@NApiVersion 2.1
*@NScriptType MapReduceScript
*/
define(['N/search'], (search) => {
// Step 1: Search for Sales Orders (mainline only)
const getInputData = () => {
try {
return search.create({
type: search.Type.SALES_ORDER,
filters: [['mainline', 'is', 'T']],
columns: ['entity']
});
} catch (e) {
log.error('Error in getInputData', e.message);
}
};
// Step 2: For each order, write out the customer ID with value = 1
const map = (context) => {
try {
const result = JSON.parse(context.value);
const customerId = result.values.entity.value;
context.write({
key: customerId,
value: 1
});
} catch (e) {
log.error('Error in map', e.message);
}
};
// Step 3: Reduce groups orders by customer and count them
const reduce = (context) => {
try {
let totalOrders = 0;
context.values.forEach(() => {
totalOrders++;
});
log.debug('Orders by Customer', `Customer ${context.key} has ${totalOrders} orders.`);
} catch (e) {
log.error('Error in reduce', e.message);
}
};
// Step 4: Final summary logging
const summarize = (summary) => {
try {
log.debug('Summary', `Script completed. Usage: ${summary.usage}`);
} catch (e) {
log.error('Error in summarize', e.message);
}
};
return { getInputData, map, reduce, summarize };
});
🔹 Best Practices
- Keep
getInputData
lightweight — use searches, not huge in-memory arrays. - Use map for record-level processing, reduce for grouping/aggregation.
- Use submitFields() for fast updates, avoid full
load + save
when possible. - Handle errors with try–catch and log them in summarize.
- Don’t worry about governance — NetSuite auto-yields and resumes.
✅ Key Takeaway
Map/Reduce scripts are the workhorses for scaling in NetSuite. With the four stages — getInputData, map, reduce, summarize — you can safely process thousands of records without hitting limits.