NewNetSuite 2025.2 — What's new
intermediateSuiteScript25 min read

SuiteScript 2.1 User Event Scripts: Complete Guide

Learn how to create User Event Scripts in SuiteScript 2.1. Master beforeLoad, beforeSubmit, and afterSubmit entry points with practical examples.

Prerequisites

  • Basic JavaScript knowledge
  • NetSuite developer account access
  • Understanding of NetSuite records
SuiteScriptUser EventNetSuite DevelopmentAutomation

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:

SettingValue
ScriptYour User Event Script
Applies ToSales Order (or your record type)
StatusReleased
Execute As RoleAdministrator (or appropriate role)
Log LevelDebug (for development)

Best Practices

  1. Always check context.type - Don't run logic on delete if you're reading fields
  2. Use try-catch in afterSubmit - The record is already saved, errors here shouldn't affect the user
  3. Minimize governance usage - User Events have limited governance units
  4. Test with all user roles - Scripts may behave differently based on permissions
  5. Log extensively during development - Use log.debug() and log.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:


Need help implementing User Event Scripts in your NetSuite account? Contact our development team for a consultation.

Need hands-on training?

Our corporate training programs go beyond tutorials with personalized instruction using your actual NetSuite environment.

Get in Touch