User Event Scripts are one of the most powerful tools in SuiteScript development. They allow you to execute custom logic when records are loaded, created, edited, or deleted. This tutorial covers everything you need to know to build production-ready User Event Scripts.
What Are User Event Scripts?
User Event Scripts run server-side and are triggered by record operations. Unlike Client Scripts that run in the browser, User Event Scripts execute on NetSuite's servers, making them ideal for:
- Data validation before saving
- Auto-populating fields
- Creating related records
- Sending notifications
- Integration triggers
Entry Points Explained
beforeLoad
Executes before the record form is displayed to the user. Use this for:
- Adding custom buttons or fields to forms
- Hiding/showing fields based on conditions
- Pre-populating default values
/**
* @NApiVersion 2.1
* @NScriptType UserEventScript
*/
define(['N/ui/serverWidget'], (serverWidget) => {
const beforeLoad = (context) => {
if (context.type !== context.UserEventType.CREATE) return;
const form = context.form;
// Add a custom field
form.addField({
id: 'custpage_approval_status',
type: serverWidget.FieldType.TEXT,
label: 'Approval Status'
}).defaultValue = 'Pending';
// Add a custom button
form.addButton({
id: 'custpage_calculate',
label: 'Calculate Total',
functionName: 'calculateTotal'
});
};
return { beforeLoad };
});beforeSubmit
Executes after the user clicks Save but before the record is committed to the database. Perfect for:
- Data validation
- Field calculations
- Preventing invalid saves
/**
* @NApiVersion 2.1
* @NScriptType UserEventScript
*/
define(['N/error'], (error) => {
const beforeSubmit = (context) => {
const record = context.newRecord;
// Skip on delete
if (context.type === context.UserEventType.DELETE) return;
const amount = record.getValue({ fieldId: 'amount' });
const creditLimit = record.getValue({ fieldId: 'custbody_credit_limit' });
// Validation example
if (amount > creditLimit) {
throw error.create({
name: 'CREDIT_LIMIT_EXCEEDED',
message: `Order amount ($${amount}) exceeds credit limit ($${creditLimit})`,
notifyOff: false
});
}
// Auto-calculate field
const taxRate = 0.08;
const totalWithTax = amount * (1 + taxRate);
record.setValue({
fieldId: 'custbody_total_with_tax',
value: totalWithTax
});
};
return { beforeSubmit };
});afterSubmit
Executes after the record is saved to the database. Use for:
- Creating related records
- Sending emails/notifications
- Triggering integrations
- Logging/auditing
/**
* @NApiVersion 2.1
* @NScriptType UserEventScript
*/
define(['N/record', 'N/email', 'N/runtime'], (record, email, runtime) => {
const afterSubmit = (context) => {
// Only run on create
if (context.type !== context.UserEventType.CREATE) return;
const newRecord = context.newRecord;
const salesOrderId = newRecord.id;
const customerEmail = newRecord.getValue({ fieldId: 'email' });
// Create a related task
const task = record.create({ type: record.Type.TASK });
task.setValue({ fieldId: 'title', value: `Follow up on SO #${salesOrderId}` });
task.setValue({ fieldId: 'assigned', value: runtime.getCurrentUser().id });
task.setValue({ fieldId: 'transaction', value: salesOrderId });
const taskId = task.save();
// Send confirmation email
email.send({
author: runtime.getCurrentUser().id,
recipients: customerEmail,
subject: `Order Confirmation #${salesOrderId}`,
body: `Thank you for your order. Your order number is ${salesOrderId}.`
});
log.audit('afterSubmit', `Created task ${taskId} for SO ${salesOrderId}`);
};
return { afterSubmit };
});Complete Example: Sales Order Validation Script
Here's a production-ready script that combines all three entry points:
/**
* @NApiVersion 2.1
* @NScriptType UserEventScript
* @NModuleScope SameAccount
*
* Sales Order validation and automation script
*/
define(['N/record', 'N/email', 'N/runtime', 'N/error', 'N/ui/serverWidget', 'N/log'],
(record, email, runtime, error, serverWidget, log) => {
/**
* Add approval workflow fields to form
*/
const beforeLoad = (context) => {
if (context.type === context.UserEventType.VIEW) {
const form = context.form;
const rec = context.newRecord;
const status = rec.getValue({ fieldId: 'custbody_approval_status' });
if (status === 'Pending') {
form.addButton({
id: 'custpage_approve',
label: 'Approve Order',
functionName: 'approveOrder'
});
}
}
};
/**
* Validate order before save
*/
const beforeSubmit = (context) => {
if (context.type === context.UserEventType.DELETE) return;
const rec = context.newRecord;
const oldRec = context.oldRecord;
// Validate minimum order amount
const total = rec.getValue({ fieldId: 'total' });
if (total < 100) {
throw error.create({
name: 'MIN_ORDER_ERROR',
message: 'Minimum order amount is $100'
});
}
// Check inventory availability
const lineCount = rec.getLineCount({ sublistId: 'item' });
for (let i = 0; i < lineCount; i++) {
const qty = rec.getSublistValue({
sublistId: 'item',
fieldId: 'quantity',
line: i
});
const available = rec.getSublistValue({
sublistId: 'item',
fieldId: 'quantityavailable',
line: i
});
if (qty > available) {
const itemName = rec.getSublistText({
sublistId: 'item',
fieldId: 'item',
line: i
});
throw error.create({
name: 'INVENTORY_ERROR',
message: `Insufficient inventory for ${itemName}. Available: ${available}, Requested: ${qty}`
});
}
}
// Set approval status for large orders
if (total > 10000 && context.type === context.UserEventType.CREATE) {
rec.setValue({
fieldId: 'custbody_approval_status',
value: 'Pending'
});
}
};
/**
* Post-save automation
*/
const afterSubmit = (context) => {
if (context.type === context.UserEventType.DELETE) return;
const rec = context.newRecord;
const orderId = rec.id;
try {
// Log the transaction
log.audit('Sales Order Processed', {
orderId: orderId,
total: rec.getValue({ fieldId: 'total' }),
customer: rec.getValue({ fieldId: 'entity' }),
type: context.type
});
// Send internal notification for large orders
const total = rec.getValue({ fieldId: 'total' });
if (total > 5000 && context.type === context.UserEventType.CREATE) {
email.send({
author: runtime.getCurrentUser().id,
recipients: 'sales-team@company.com',
subject: `Large Order Alert: SO #${orderId}`,
body: `A new order of $${total} has been created. Please review.`
});
}
} catch (e) {
log.error('afterSubmit Error', e.message);
// Don't throw - record is already saved
}
};
return { beforeLoad, beforeSubmit, afterSubmit };
});Deployment Configuration
Create a Script Deployment record with these settings:
| Setting | Value |
|---|---|
| Script | Your User Event Script |
| Applies To | Sales Order (or your record type) |
| Status | Released |
| Execute As Role | Administrator (or appropriate role) |
| Log Level | Debug (for development) |
Best Practices
- Always check context.type - Don't run logic on delete if you're reading fields
- Use try-catch in afterSubmit - The record is already saved, errors here shouldn't affect the user
- Minimize governance usage - User Events have limited governance units
- Test with all user roles - Scripts may behave differently based on permissions
- Log extensively during development - Use
log.debug()andlog.audit()
Common Pitfalls
- Infinite loops: Don't trigger saves that re-trigger the same script
- Missing null checks: Always validate field values exist before using them
- Governance limits: User Events share governance with the user's transaction
Next Steps
Now that you understand User Event Scripts, explore:
- Creating Custom Suitelets for custom UI pages
- Scheduled Scripts for batch processing
- Map/Reduce Scripts for high-volume operations
Need help implementing User Event Scripts in your NetSuite account? Contact our development team for a consultation.