How to Use These
- All examples are SuiteScript 2.1.
- Replace placeholder IDs (saved search IDs, folder IDs, custom field IDs).
- Test in sandbox; enable debug logging via script parameters when provided.
1) Map/Reduce — Bulk Update Invoices (set custom form + memo)
Use when: thousands of invoices need a field change safely.
/**
* @NApiVersion 2.1
* @NScriptType MapReduceScript
* Bulk update invoice custom form + memo (governance-safe)
*/
define(['N/search','N/record','N/runtime','N/log'], (search, record, runtime, log) => {
const PARAM_FORM = 'custscript_target_customform'; // integer
const PARAM_MEMO = 'custscript_memo_text'; // string
const PARAM_SEARCH = 'custscript_invoice_search'; // saved search id
const getInputData = () => {
try {
const searchId = runtime.getCurrentScript().getParameter(PARAM_SEARCH);
return search.load({ id: searchId });
} catch (e) {
log.error('getInputData', e); throw e;
}
};
const map = ctx => {
try {
const row = JSON.parse(ctx.value);
ctx.write(row.id, { id: row.id }); // group by record id
} catch (e) { log.error('map', e); }
};
const reduce = ctx => {
const formId = Number(runtime.getCurrentScript().getParameter(PARAM_FORM));
const memoTxt = String(runtime.getCurrentScript().getParameter(PARAM_MEMO) || '');
try {
const invoiceId = ctx.key;
const inv = record.load({ type: record.Type.INVOICE, id: invoiceId });
if (formId) inv.setValue({ fieldId:'customform', value: formId });
if (memoTxt) inv.setValue({ fieldId:'memo', value: memoTxt });
const id = inv.save({ enableSourcing: false, ignoreMandatoryFields: true });
log.audit('Updated Invoice', id);
} catch (e) { log.error(`reduce(${ctx.key})`, e); }
};
const summarize = (sum) => {
log.audit('Summary', { map: sum.mapSummary, reduce: sum.reduceSummary });
};
return { getInputData, map, reduce, summarize };
});
Notes
- Pass parameters at deployment: saved search of target invoices, target custom form ID, memo text.
- MR auto-resumes; no manual yielding needed.
2) User Event — Validate Lines & Prevent Duplicate Items
Use when: you want to block common data entry mistakes.
/**
* @NApiVersion 2.1
* @NScriptType UserEventScript
* Prevent duplicate items & ensure quantity > 0 on Sales Orders
*/
define(['N/error','N/log'], (error, log) => {
const beforeSubmit = (ctx) => {
try {
const rec = ctx.newRecord;
if (rec.type !== 'salesorder') return;
const seen = new Set();
const count = rec.getLineCount({ sublistId:'item' });
for (let i=0; i<count; i++) {
const item = rec.getSublistValue({ sublistId:'item', fieldId:'item', line:i });
const qty = Number(rec.getSublistValue({ sublistId:'item', fieldId:'quantity', line:i }) || 0);
if (seen.has(item)) {
throw error.create({ name:'DUP_ITEM', message:`Duplicate item at line ${i+1}.` });
}
if (qty <= 0) {
throw error.create({ name:'BAD_QTY', message:`Quantity must be > 0 at line ${i+1}.` });
}
seen.add(item);
}
} catch (e) { log.error('beforeSubmit', e); throw e; }
};
return { beforeSubmit };
});
Notes
- Deploy on Sales Order
beforeSubmit
. - Keep validations lightweight to avoid timeouts.
3) Client Script — “Fill Down” Helper for Sublist Fields
Use when: users repeat the same value across many lines.
/**
* @NApiVersion 2.1
* @NScriptType ClientScript
* Button handler to copy current line's Department to all lines
*/
define(['N/currentRecord','N/ui/dialog'], (currentRecord, dialog) => {
const copyDepartment = async () => {
try {
const rec = currentRecord.get();
const sublistId = 'item';
const line = rec.getCurrentSublistIndex({ sublistId });
const dept = rec.getCurrentSublistValue({ sublistId, fieldId:'department' });
if (!dept) return dialog.alert({ title:'Info', message:'No department to copy.' });
const count = rec.getLineCount({ sublistId });
for (let i=0; i<count; i++) {
rec.selectLine({ sublistId, line:i });
rec.setCurrentSublistValue({ sublistId, fieldId:'department', value: dept });
rec.commitLine({ sublistId });
}
await dialog.alert({ title:'Done', message:`Copied to ${count} line(s).` });
} catch (e) { console.log('copyDepartment', e); }
};
return { copyDepartment };
});
Notes
- Add a custom button via UE afterLoad that calls
copyDepartment()
. - Improves UX; zero server calls.
4) Suitelet — Email Statements (Queue Map/Reduce + Status)
Use when: you want a safe, scalable “Send Statements” tool.
/**
* @NApiVersion 2.1
* @NScriptType Suitelet
* Start MR to email statements; show taskId
*/
define(['N/ui/serverWidget','N/task','N/log'], (ui, task, log) => {
const onRequest = (ctx) => {
try {
if (ctx.request.method === 'GET') {
const f = ui.createForm({ title:'Email Statements' });
f.addField({ id:'custpage_cust_search', type:'select', label:'Customer Search', source:'search' });
f.addSubmitButton({ label:'Start' });
return ctx.response.writePage(f);
}
// POST: start MR
const t = task.create({ taskType: task.TaskType.MAP_REDUCE, scriptId:'customscript_email_stmt_mr', deploymentId:'customdeploy_email_stmt_mr' });
const taskId = t.submit();
ctx.response.write(`Started. Task ID: ${taskId}`);
} catch (e) { log.error('Suitelet', e); ctx.response.write(`Error: ${e.message}`); }
};
return { onRequest };
});
Notes
- MR handles email generation + sending; Suitelet stays fast.
- Add a tiny status Suitelet if you want the UI to poll progress.
5) RESTlet — Create or Update Customer (Upsert)
Use when: integrating with an external system simply and securely.
/**
* @NApiVersion 2.1
* @NScriptType Restlet
* Upsert Customer by externalId; returns internalId
*/
define(['N/record','N/search','N/log'], (record, search, log) => {
const post = (body) => {
try {
const extId = String(body.externalId || '').trim();
if (!extId) throw new Error('externalId required');
const existing = findByExternalId(extId);
const rec = existing
? record.load({ type: record.Type.CUSTOMER, id: existing })
: record.create({ type: record.Type.CUSTOMER, isDynamic: true });
if (!existing) rec.setValue({ fieldId:'externalid', value: extId });
if (body.companyName) rec.setValue({ fieldId:'companyname', value: body.companyName });
if (body.email) rec.setValue({ fieldId:'email', value: body.email });
const id = rec.save({ enableSourcing: true, ignoreMandatoryFields: false });
return { ok: true, internalId: id };
} catch (e) { log.error('REST Upsert', e); return { ok:false, message: e.message }; }
};
function findByExternalId(externalId){
const s = search.create({ type:'customer', filters:[['externalidstring','is',externalId]], columns:['internalid'] });
const r = s.run().getRange({ start:0, end:1 })[0];
return r && r.getValue('internalid');
}
return { post };
});
Notes
- Secure with token-based auth or OAuth 2.0.
- Validate inputs; never trust external sources.
6) Scheduled Script — SFTP Inventory Import (JSON → Item)
Use when: nightly feeds update on-hand/price from a 3PL/ERP.
/**
* @NApiVersion 2.1
* @NScriptType ScheduledScript
* Pull JSON from SFTP and update inventory item fields
*/
define(['N/sftp','N/file','N/record','N/runtime','N/log'], (sftp, file, record, runtime, log) => {
const execute = () => {
try {
const conn = sftp.createConnection({
url: 'sftp.partner.com',
username: 'ftp_user',
passwordGuid: 'custsecret_sftp_pwd_guid',
hostKey: 'AAAAB3NzaC1yc2EAAAADAQABAAABAQ...' // exact host key
});
const dir = '/inbound/inventory';
(conn.list({ path: dir }) || []).forEach(f => {
if (!f.name.endsWith('.json')) return;
try {
const txt = conn.download({ directory: dir, filename: f.name }).getContents();
const items = JSON.parse(txt);
items.forEach(updateItemSafe);
archive(txt, f.name);
} catch (e) { log.error(`File ${f.name}`, e); }
});
} catch (e) { log.error('SFTP execute', e); }
};
function updateItemSafe(it){
try {
const rec = record.load({ type:'inventoryitem', id: it.internalId });
if (it.price != null) rec.setValue({ fieldId:'baseprice', value: Number(it.price) });
if (it.qty != null) rec.setValue({ fieldId:'custitem_integration_qty', value: Number(it.qty) });
rec.save({ enableSourcing:false, ignoreMandatoryFields:true });
} catch (e) { /* log and continue */ }
}
function archive(contents, name){
try {
file.create({ name:`processed_${name}`, fileType:file.Type.JSON, contents, folder: 12345 }).save();
} catch (e) {}
}
return { execute };
});
Notes
- Keep per-file batches small; consider MR for very large feeds.
- Use Secrets Manager for credentials.
7) Custom GL Plug-in — Redirect Revenue by Subsidiary
Use when: accounting wants specific revenue accounts per subsidiary/channel.
/**
* @NApiVersion 2.x
* @NScriptType CustomGLPlugin
* Redirect credit lines based on subsidiary → custom account
*/
define(['N/log'], (log) => {
function CustomGL() {}
CustomGL.prototype.generateLines = function(context){
try {
const trx = context.transactionRecord;
const sub = trx.getValue({ fieldId:'subsidiary' });
context.standardLines.iterator().each(line => {
if (line.creditAmount > 0 && sub == 2) {
// neutralize original
line.setCreditAmount(0);
// add custom credit
const l = context.lines.addNewLine();
l.setAccountId(1234); // target revenue account
l.setCreditAmount(line.creditAmount);
l.setMemo('Redirected revenue (Subsidiary 2)');
}
return true;
});
} catch (e) { log.error('generateLines', e); }
};
return CustomGL;
});
Notes
- Balance debits/credits; test on sample invoices.
- Avoid double-posting—always neutralize if replacing lines.
8) Utility Module — Safe Logging, Backoff & Lookup
Use when: you want consistency across scripts.
/**
* @NApiVersion 2.1
* @NModuleScope Public
* Utils: logSafe, backoff, lookup
*/
define(['N/search','N/log'], (search, log) => {
const logSafe = (title, obj) => {
try { log.debug(title, JSON.stringify(obj).slice(0, 1000)); }
catch (_) { log.debug(title, '[unserializable]'); }
};
async function backoff(fn, tries=5, base=300){
for (let i=0;i<tries;i++){
try { return await fn(); }
catch (e){ if (i===tries-1) throw e; await wait(base * Math.pow(2,i)); }
}
}
const wait = (ms) => new Promise(r => setTimeout(r, ms));
const lookupIdByText = (type, field, text) => {
const s = search.create({ type, filters:[[field,'is',text]], columns:['internalid'] });
const r = s.run().getRange({ start:0, end:1 })[0];
return r && r.getValue('internalid');
};
return { logSafe, backoff, lookupIdByText };
});
Notes
- Import this small module in your projects to standardize behaviors.
Deployment & Governance Tips (Quick)
- User Event: keep logic light; offload heavy work to MR.
- Suitelet: return fast; use JSON endpoints + client polling for long tasks.
- MR: group by record/document; write in reduce; summarize outcomes.
- SFTP: verify host key; test with tiny files; handle empty listings.
- RESTlet: sanitize inputs, enforce auth, return compact JSON, avoid heavy DB loops.
- Custom GL: neutralize replaced lines; add rich logging in sandbox.