🧭 What is a User Event Script?
A server-side script that runs at key record lifecycle moments:
- beforeLoad – when the form opens (good for UI tweaks, default values, adding buttons)
- beforeSubmit – just before the record is saved (hard validation, data normalization)
- afterSubmit – after save/commit (create related records, send emails, queue tasks)
Typical targets: Transactions, Entities, Items, and Custom Records.
⚙️ Quick Template
/**
* @NApiVersion 2.1
* @NScriptType UserEventScript
*/
define(['N/record','N/search','N/runtime','N/ui/serverWidget','N/log','N/task'],
(record, search, runtime, ui, log, task) => {
const beforeLoad = (ctx) => {
const { type, form, newRecord } = ctx;
if (type === ctx.UserEventType.VIEW) {
form.addButton({
id: 'custpage_btn_do_something',
label: 'Do Something',
functionName: "alert('Hello from beforeLoad!')"
});
// Set a default field value on create mode
if (runtime.executionContext === runtime.ContextType.USER_INTERFACE && ctx.type === ctx.UserEventType.CREATE) {
newRecord.setValue({ fieldId: 'memo', value: 'Created via UI' });
}
}
};
const beforeSubmit = (ctx) => {
const rec = ctx.newRecord;
// Strict validation: prevent save with missing customer
if (rec.type === 'salesorder' && !rec.getValue('entity')) {
throw new Error('Customer is required before saving the Sales Order.');
}
// Normalize: ensure memo always has prefix
const memo = rec.getValue('memo') || '';
if (!memo.startsWith('[SO] ')) {
rec.setValue({ fieldId: 'memo', value: `[SO] ${memo}` });
}
};
const afterSubmit = (ctx) => {
const rec = ctx.newRecord;
try {
// Post-commit: flag record for async processing if total is large
const total = rec.getValue('total') || 0;
if (total > 10000) {
task.create({
taskType: task.TaskType.MAP_REDUCE,
scriptId: 'customscript_mr_high_value_handler',
deploymentId: 'customdeploy_mr_high_value_handler',
params: { custscript_source_tran_id: rec.id }
}).submit();
}
} catch (e) {
log.error('afterSubmit error', e);
}
};
return { beforeLoad, beforeSubmit, afterSubmit };
});
🔍 Common Patterns & Mini Examples
1) Add & Wire Custom Buttons (beforeLoad)
form.clientScriptModulePath = './CS_custom_actions.js';
form.addButton({
id: 'custpage_btn_refresh_rates',
label: 'Refresh FX Rates',
functionName: "refreshRates()"
});
- Tip: Set
form.clientScriptModulePathto attach a Client Script that implementsrefreshRates().
2) Strict Validation (beforeSubmit)
const terms = rec.getValue('terms');
if (!terms) {
throw new Error('Terms are required before saving.');
}
- Why here? Only
beforeSubmitguarantees block-before-save.
3) Derived/Normalized Data (beforeSubmit)
const externalId = rec.getValue('externalid') || '';
rec.setValue({
fieldId: 'externalid',
value: externalId.trim().toUpperCase()
});
4) Create/Update Related Records (afterSubmit)
if (ctx.type !== ctx.UserEventType.DELETE) {
record.create({ type: 'customrecord_audit_log', isDynamic: true })
.setValue({ fieldId: 'custrecord_source', value: rec.id })
.setValue({ fieldId: 'custrecord_message', value: 'SO posted to audit log' })
.save();
}
5) Sublist Handling (Line-Level)
const lineCount = rec.getLineCount({ sublistId: 'item' });
let totalQty = 0;
for (let i = 0; i < lineCount; i++) {
totalQty += Number(rec.getSublistValue({ sublistId: 'item', fieldId: 'quantity', line: i })) || 0;
}
if (totalQty > 500) {
throw new Error('Total item quantity exceeds the limit (500).');
}
6) Conditional Behavior by Execution Context
if (runtime.executionContext === runtime.ContextType.WEBSERVICES) {
// Relax certain checks for integrations, or log differently
}
🧠 Governance Tips
- Prefer
search.lookupFields()orrecord.submitFields()over full loads/saves where possible. - If heavy work is needed, set a flag & hand off to Scheduled/MapReduce in
afterSubmit. - Avoid long loops in
beforeSubmit— keep the save path fast.
🧪 Testing Matrix (use this before “Released”)
- Create (UI) / Edit (UI) / View (UI)
- CSV import (does code run as expected?)
- Web Services or REST integrations (executionContext checks)
- Copy/Transform (e.g., Estimate → SO)
- Permissions (do restricted roles trigger unexpected errors?)
🧱 Deployment Checklist
- File Cabinet: upload
.jsinto your scripts folder - Script Record: Customization → Scripting → Scripts → New → User Event
- Add Deployments: choose record types, set Status = Released
- Logging: start with
AUDIT→ lower once stable - Versioning: add header tags like
@version 1.2.0, “last modified”
🚧 Common Errors & Fixes
| Error | Likely Cause | Fix |
|---|---|---|
| “You do not have permission…” | Script deployment audience too narrow | Adjust deployment audience/role |
| “Record has been changed” | Competing updates post-save | Move to afterSubmit or use submitFields carefully |
| Timeouts / governance | Heavy work in beforeSubmit | Defer to MR/SS in afterSubmit |
| Duplicate side effects | Multiple UEs on same record | Consolidate or guard with a custom flag field |
📚 Related Pages
- 👉 Client Script Basic Tutorial
- 👉 SuiteFlow Basics & Approval Workflows
- 👉 SuiteScript Security & Governance Best Practices
🧭 Summary
Use User Events when you must control save-time integrity and perform reliable post-commit operations. Keep beforeSubmit lean, apply strict validations there, and push heavy jobs to async tasks via afterSubmit. Pair with Client Scripts for the best UX and with Workflows for visual orchestration.
Leave a Reply