Skip to main content

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 CaseExample
ValidationPrevent saving if credit limit exceeded
Field defaultsAuto-populate fields on new records
AutomationCreate related records automatically
CalculationsUpdate totals, compute custom fields
NotificationsSend emails on status changes
Data syncPush 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

TypeWhen Triggered
createNew record being created
editExisting record being edited
viewRecord being viewed (beforeLoad only)
copyRecord being copied
deleteRecord being deleted
xeditInline edit
approveRecord approval
cancelRecord cancellation
packFulfillment packing
shipFulfillment 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

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');
}
};
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

PracticeDescription
Check context typeAlways check context.type before executing logic
Use try/catchWrap code in error handling, especially afterSubmit
Minimize beforeLoadKeep it fast - affects page load time
Use submitFieldsFor simple updates, avoid loading full record
Log everythingLog key actions for debugging
Check for recursionAvoid 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