User Event Scripts
User Event scripts execute automatically when records are loaded, created, edited, or deleted. They run server-side and are perfect for validation, automation, and data transformation.
When to Use User Events
| Use Case | Example |
|---|---|
| Validation | Prevent saving if credit limit exceeded |
| Field defaults | Auto-populate fields on new records |
| Automation | Create related records automatically |
| Calculations | Update totals, compute custom fields |
| Notifications | Send emails on status changes |
| Data sync | Push changes to external systems |
User Event Execution Flow
┌─────────────────────────────────────────────────────────────────────────────┐
│ USER EVENT EXECUTION FLOW │
└─────────────────────────────────────────────────────────────────────────────┘
RECORD LIFECYCLE
┌─────────────────────────────────────────────────────────────────┐
│ │
│ ┌──────────┐ │
│ │ User │ │
│ │ Opens │ │
│ │ Record │ │
│ └────┬─────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────┐ │
│ │ beforeLoad │ │
│ │ ───────────────────────────────────── │ │
│ │ • Add custom buttons │ │
│ │ • Hide/show fields │ │
│ │ • Modify form layout │ │
│ │ • Set field defaults │ │
│ └─────────────────────┬───────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────┐ │
│ │ FORM DISPLAYED │ │
│ │ User edits record │ │
│ └─────────────────────┬───────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────┐ │
│ │ beforeSubmit │ │
│ │ ───────────────────────────────────── │ │
│ │ • Validate data │ │
│ │ • Modify values before save │ │
│ │ • Block save if invalid │ │
│ │ • Transform data │ │
│ └─────────────────────┬───────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────┐ │
│ │ RECORD SAVED │ │
│ │ Database updated │ │
│ └─────────────────────┬───────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────┐ │
│ │ afterSubmit │ │
│ │ ───────────────────────────────────── │ │
│ │ • Create related records │ │
│ │ • Send notifications │ │
│ │ • Sync to external systems │ │
│ │ • Update other records │ │
│ └─────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
Basic User Event Structure
/**
* @NApiVersion 2.1
* @NScriptType UserEventScript
* @NModuleScope SameAccount
*/
define(['N/record', 'N/search', 'N/log', 'N/runtime'],
(record, search, log, runtime) => {
/**
* Executes when record is loaded (view, edit, copy)
* @param {Object} context
* @param {Record} context.newRecord - Record being loaded
* @param {string} context.type - Trigger type (view, edit, create, copy, etc.)
* @param {Form} context.form - UI form object
*/
const beforeLoad = (context) => {
log.debug('beforeLoad', `Type: ${context.type}`);
};
/**
* Executes before record is saved
* @param {Object} context
* @param {Record} context.newRecord - Record being saved
* @param {Record} context.oldRecord - Previous version (edit only)
* @param {string} context.type - Trigger type (create, edit, delete, etc.)
*/
const beforeSubmit = (context) => {
log.debug('beforeSubmit', `Type: ${context.type}`);
};
/**
* Executes after record is saved
* @param {Object} context
* @param {Record} context.newRecord - Saved record
* @param {Record} context.oldRecord - Previous version (edit only)
* @param {string} context.type - Trigger type (create, edit, delete, etc.)
*/
const afterSubmit = (context) => {
log.debug('afterSubmit', `Type: ${context.type}`);
};
return { beforeLoad, beforeSubmit, afterSubmit };
});
Context Types
| Type | When Triggered |
|---|---|
create | New record being created |
edit | Existing record being edited |
view | Record being viewed (beforeLoad only) |
copy | Record being copied |
delete | Record being deleted |
xedit | Inline edit |
approve | Record approval |
cancel | Record cancellation |
pack | Fulfillment packing |
ship | Fulfillment shipping |
beforeLoad Examples
Add Custom Button
const beforeLoad = (context) => {
if (context.type !== context.UserEventType.VIEW) return;
const form = context.form;
// Add custom button
form.addButton({
id: 'custpage_approve',
label: 'Approve Invoice',
functionName: 'approveInvoice'
});
// Add client script for button
form.clientScriptModulePath = './invoice_cs.js';
};
Hide/Show Fields Based on Status
const beforeLoad = (context) => {
const record = context.newRecord;
const form = context.form;
const status = record.getValue({ fieldId: 'status' });
if (status === 'Approved') {
// Make approval fields read-only
const approverField = form.getField({ id: 'custbody_approver' });
if (approverField) {
approverField.updateDisplayType({
displayType: serverWidget.FieldDisplayType.INLINE
});
}
}
};
Set Default Values on Create
const beforeLoad = (context) => {
if (context.type !== context.UserEventType.CREATE) return;
const record = context.newRecord;
const user = runtime.getCurrentUser();
// Set default department from user
record.setValue({
fieldId: 'department',
value: user.department
});
// Set default date to today
record.setValue({
fieldId: 'trandate',
value: new Date()
});
};
beforeSubmit Examples
Validate Before Save
const beforeSubmit = (context) => {
if (context.type === context.UserEventType.DELETE) return;
const record = context.newRecord;
const amount = record.getValue({ fieldId: 'total' });
const creditLimit = record.getValue({ fieldId: 'custbody_credit_limit' });
if (amount > creditLimit) {
throw error.create({
name: 'CREDIT_LIMIT_EXCEEDED',
message: `Order amount ($${amount}) exceeds credit limit ($${creditLimit})`
});
}
};
Auto-Calculate Fields
const beforeSubmit = (context) => {
if (context.type === context.UserEventType.DELETE) return;
const record = context.newRecord;
const lineCount = record.getLineCount({ sublistId: 'item' });
let totalWeight = 0;
let totalQty = 0;
for (let i = 0; i < lineCount; i++) {
const qty = record.getSublistValue({
sublistId: 'item',
fieldId: 'quantity',
line: i
});
const weight = record.getSublistValue({
sublistId: 'item',
fieldId: 'custcol_weight',
line: i
});
totalQty += qty;
totalWeight += (qty * weight);
}
record.setValue({ fieldId: 'custbody_total_qty', value: totalQty });
record.setValue({ fieldId: 'custbody_total_weight', value: totalWeight });
};
Transform Data Before Save
const beforeSubmit = (context) => {
if (context.type === context.UserEventType.DELETE) return;
const record = context.newRecord;
// Uppercase customer name
const name = record.getValue({ fieldId: 'companyname' });
if (name) {
record.setValue({
fieldId: 'companyname',
value: name.toUpperCase()
});
}
// Format phone number
let phone = record.getValue({ fieldId: 'phone' });
if (phone) {
phone = phone.replace(/\D/g, ''); // Remove non-digits
if (phone.length === 10) {
phone = `(${phone.slice(0,3)}) ${phone.slice(3,6)}-${phone.slice(6)}`;
record.setValue({ fieldId: 'phone', value: phone });
}
}
};
afterSubmit Examples
Create Related Record
const afterSubmit = (context) => {
if (context.type !== context.UserEventType.CREATE) return;
const salesOrder = context.newRecord;
const customerId = salesOrder.getValue({ fieldId: 'entity' });
const soId = salesOrder.id;
// Create task for follow-up
const task = record.create({ type: record.Type.TASK });
task.setValue({ fieldId: 'title', value: `Follow up on SO #${soId}` });
task.setValue({ fieldId: 'company', value: customerId });
task.setValue({
fieldId: 'startdate',
value: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000) // 7 days
});
task.setValue({ fieldId: 'priority', value: 'HIGH' });
const taskId = task.save();
log.audit('Task Created', `Task ${taskId} for SO ${soId}`);
};
Send Email Notification
const afterSubmit = (context) => {
if (context.type !== context.UserEventType.CREATE &&
context.type !== context.UserEventType.EDIT) return;
const newRecord = context.newRecord;
const oldRecord = context.oldRecord;
// Check if status changed to Approved
const newStatus = newRecord.getValue({ fieldId: 'status' });
const oldStatus = oldRecord ? oldRecord.getValue({ fieldId: 'status' }) : null;
if (newStatus === 'Approved' && newStatus !== oldStatus) {
const email = require('N/email');
email.send({
author: -5, // System user
recipients: newRecord.getValue({ fieldId: 'custbody_requester' }),
subject: `Your request #${newRecord.getValue({ fieldId: 'tranid' })} has been approved`,
body: `Your request has been approved and is being processed.`
});
log.audit('Email Sent', 'Approval notification sent');
}
};
Update Related Records
const afterSubmit = (context) => {
if (context.type === context.UserEventType.DELETE) return;
const invoice = context.newRecord;
const customerId = invoice.getValue({ fieldId: 'entity' });
const invoiceTotal = invoice.getValue({ fieldId: 'total' });
// Update customer's total purchases
const customerLookup = search.lookupFields({
type: search.Type.CUSTOMER,
id: customerId,
columns: ['custentity_total_purchases']
});
const currentTotal = parseFloat(customerLookup.custentity_total_purchases) || 0;
record.submitFields({
type: record.Type.CUSTOMER,
id: customerId,
values: {
'custentity_total_purchases': currentTotal + invoiceTotal
}
});
log.audit('Customer Updated', `Updated total purchases for customer ${customerId}`);
};
Detecting Field Changes
const beforeSubmit = (context) => {
if (context.type !== context.UserEventType.EDIT) return;
const newRecord = context.newRecord;
const oldRecord = context.oldRecord;
// Check if specific field changed
const newStatus = newRecord.getValue({ fieldId: 'status' });
const oldStatus = oldRecord.getValue({ fieldId: 'status' });
if (newStatus !== oldStatus) {
log.debug('Status Changed', `From ${oldStatus} to ${newStatus}`);
// Set status change timestamp
newRecord.setValue({
fieldId: 'custbody_status_changed',
value: new Date()
});
}
};
Complete Example: Sales Order Validation & Automation
/**
* @NApiVersion 2.1
* @NScriptType UserEventScript
* @NModuleScope SameAccount
*/
define(['N/record', 'N/search', 'N/email', 'N/runtime', 'N/log', 'N/error'],
(record, search, email, runtime, log, error) => {
/**
* Add approval button for managers
*/
const beforeLoad = (context) => {
if (context.type !== context.UserEventType.VIEW) return;
const rec = context.newRecord;
const status = rec.getValue({ fieldId: 'orderstatus' });
const user = runtime.getCurrentUser();
// Only show approve button for pending orders and managers
if (status === 'A' && user.role === 3) { // 3 = Administrator
context.form.addButton({
id: 'custpage_approve',
label: 'Approve Order',
functionName: 'approveOrder'
});
}
};
/**
* Validate order before saving
*/
const beforeSubmit = (context) => {
if (context.type === context.UserEventType.DELETE) return;
const rec = context.newRecord;
// 1. Validate minimum order amount
const total = rec.getValue({ fieldId: 'total' });
if (total < 100) {
throw error.create({
name: 'MIN_ORDER_AMOUNT',
message: 'Minimum order amount is $100'
});
}
// 2. Validate all items have quantity
const lineCount = rec.getLineCount({ sublistId: 'item' });
for (let i = 0; i < lineCount; i++) {
const qty = rec.getSublistValue({
sublistId: 'item',
fieldId: 'quantity',
line: i
});
if (!qty || qty <= 0) {
throw error.create({
name: 'INVALID_QTY',
message: `Line ${i + 1} has invalid quantity`
});
}
}
// 3. Set processing date if not set
if (!rec.getValue({ fieldId: 'custbody_process_date' })) {
rec.setValue({
fieldId: 'custbody_process_date',
value: new Date()
});
}
log.debug('Validation Passed', `Order total: ${total}`);
};
/**
* Post-save automation
*/
const afterSubmit = (context) => {
if (context.type === context.UserEventType.DELETE) return;
const rec = context.newRecord;
const oldRec = context.oldRecord;
const recId = rec.id;
// On create: Create fulfillment task
if (context.type === context.UserEventType.CREATE) {
createFulfillmentTask(rec);
}
// On edit: Check for status change
if (context.type === context.UserEventType.EDIT) {
const newStatus = rec.getValue({ fieldId: 'orderstatus' });
const oldStatus = oldRec.getValue({ fieldId: 'orderstatus' });
if (newStatus !== oldStatus) {
sendStatusNotification(rec, oldStatus, newStatus);
}
}
};
/**
* Create fulfillment task
*/
const createFulfillmentTask = (salesOrder) => {
try {
const task = record.create({ type: record.Type.TASK });
task.setValue({
fieldId: 'title',
value: `Fulfill SO #${salesOrder.getValue({ fieldId: 'tranid' })}`
});
task.setValue({
fieldId: 'company',
value: salesOrder.getValue({ fieldId: 'entity' })
});
task.setValue({
fieldId: 'custevent_related_so',
value: salesOrder.id
});
const taskId = task.save();
log.audit('Task Created', taskId);
} catch (e) {
log.error('Task Creation Failed', e.message);
}
};
/**
* Send status change notification
*/
const sendStatusNotification = (rec, oldStatus, newStatus) => {
try {
const salesRepId = rec.getValue({ fieldId: 'salesrep' });
if (!salesRepId) return;
email.send({
author: runtime.getCurrentUser().id,
recipients: salesRepId,
subject: `Order ${rec.getValue({ fieldId: 'tranid' })} Status Changed`,
body: `Order status changed from ${oldStatus} to ${newStatus}`
});
log.audit('Notification Sent', `To sales rep ${salesRepId}`);
} catch (e) {
log.error('Email Failed', e.message);
}
};
return { beforeLoad, beforeSubmit, afterSubmit };
});
Script Deployment XML
<?xml version="1.0" encoding="UTF-8"?>
<customscript scriptid="customscript_so_validation_ue">
<name>Sales Order Validation UE</name>
<scripttype>USEREVENT</scripttype>
<scriptfile>[/SuiteScripts/UserEvents/so_validation_ue.js]</scriptfile>
<description>Validates and automates sales order processing</description>
<isinactive>F</isinactive>
<notifyowner>T</notifyowner>
<scriptdeployments>
<scriptdeployment scriptid="customdeploy_so_validation_ue">
<status>RELEASED</status>
<recordtype>SALESORDER</recordtype>
<isdeployed>T</isdeployed>
<loglevel>DEBUG</loglevel>
<allroles>T</allroles>
</scriptdeployment>
</scriptdeployments>
</customscript>
Best Practices
| Practice | Description |
|---|---|
| Check context type | Always check context.type before executing logic |
| Use try/catch | Wrap code in error handling, especially afterSubmit |
| Minimize beforeLoad | Keep it fast - affects page load time |
| Use submitFields | For simple updates, avoid loading full record |
| Log everything | Log key actions for debugging |
| Check for recursion | Avoid infinite loops when updating same record type |
Execution Context
// Check how script was triggered
const executionContext = runtime.executionContext;
if (executionContext === runtime.ContextType.USER_INTERFACE) {
// Triggered from UI
} else if (executionContext === runtime.ContextType.WEBSERVICES) {
// Triggered via web service
} else if (executionContext === runtime.ContextType.CSV_IMPORT) {
// Triggered via CSV import
}
Next Steps
- Client Scripts - Client-side validation
- Scheduled Scripts - Background processing
- Invoice Approval Scenario - Complete workflow