Skip to main content

Client Script Development

Client Scripts run in the user's browser to validate data, control field behavior, and enhance the user interface on NetSuite forms.


When to Use Client Scripts

Use CaseExample
Field validationEnsure email format is valid
Calculated fieldsUpdate total when quantity changes
UI controlShow/hide fields based on selection
User guidanceDisplay warnings before save
Field sourcingAuto-populate fields from related records

Client Script Execution Flow

┌─────────────────────────────────────────────────────────────────────────────┐
│ CLIENT SCRIPT EXECUTION FLOW │
└─────────────────────────────────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────────────────────┐
│ PAGE LOAD │
└──────────────────────────────┬───────────────────────────────────┘


┌──────────────────────────────────────────────────────────────────┐
│ pageInit() │
│ ────────────────────────────────────────────────────────────────│
│ • Runs when page first loads │
│ • Set default values │
│ • Initialize UI state │
│ • Enable/disable fields │
└──────────────────────────────┬───────────────────────────────────┘


┌──────────────────────────────────────────────────────────────────┐
│ USER INTERACTS WITH FORM │
└──────────────────────────────┬───────────────────────────────────┘

┌────────────────────────┼────────────────────────┐
│ │ │
▼ ▼ ▼
┌─────────────────────┐ ┌─────────────────────┐ ┌─────────────────────┐
│ fieldChanged() │ │ postSourcing() │ │ lineInit() │
│ ───────────────── │ │ ───────────────── │ │ ───────────────── │
│ User changes a │ │ After NetSuite │ │ User selects a │
│ field value │ │ sources values │ │ sublist line │
│ │ │ from parent │ │ │
│ • Validate input │ │ │ │ • Set line defaults│
│ • Update other │ │ • Modify sourced │ │ • Initialize line │
│ fields │ │ values │ │ state │
│ • Show/hide fields │ │ • Add calculations │ │ │
└─────────────────────┘ └─────────────────────┘ └─────────────────────┘
│ │ │
└────────────────────────┼────────────────────────┘


┌──────────────────────────────────────────────────────────────────┐
│ SUBLIST OPERATIONS │
└──────────────────────────────┬───────────────────────────────────┘

┌────────────────────────┴────────────────────────┐
│ │
▼ ▼
┌─────────────────────────────┐ ┌─────────────────────────────┐
│ validateLine() │ │ sublistChanged() │
│ ───────────────────────── │ │ ───────────────────────── │
│ Before line is committed │ │ After line is committed │
│ │ │ │
│ • Validate line data │ │ • Update header totals │
│ • Return true/false │ │ • Recalculate values │
│ • Block invalid lines │ │ │
└──────────────┬──────────────┘ └─────────────────────────────┘


┌─────────────┐
│ Line Valid? │
└──────┬──────┘

┌─────────┴─────────┐
│ Yes │ No
▼ ▼
┌──────────┐ ┌──────────────┐
│ Commit │ │ Stay on line │
│ Line │ │ Show error │
└──────────┘ └──────────────┘



┌──────────────────────────────────────────────────────────────────┐
│ USER CLICKS SAVE │
└──────────────────────────────┬───────────────────────────────────┘


┌──────────────────────────────────────────────────────────────────┐
│ saveRecord() │
│ ────────────────────────────────────────────────────────────────│
│ • Final validation before submit │
│ • Return true to allow save │
│ • Return false to block save │
│ • Show confirmation dialogs │
└──────────────────────────────┬───────────────────────────────────┘

┌─────┴─────┐
│ │
true │ │ false
▼ ▼
┌──────────┐ ┌──────────────┐
│ SAVE │ │ STAY ON │
│ RECORD │ │ FORM │
└──────────┘ └──────────────┘

Entry Points Reference

Entry PointTriggers WhenCommon Uses
pageInitPage loadsSet defaults, initialize UI
fieldChangedField value changesValidation, calculations
postSourcingAfter field sourcing completesModify sourced values
lineInitSublist line selectedSet line defaults
validateFieldBefore field change commitsBlock invalid values
validateLineBefore line commitsValidate line data
validateInsertBefore new line insertValidate new lines
validateDeleteBefore line deleteConfirm deletion
sublistChangedAfter line commitsUpdate totals
saveRecordBefore record savesFinal validation

Basic Client Script Structure

/**
* @NApiVersion 2.1
* @NScriptType ClientScript
* @NModuleScope SameAccount
*/
define(['N/currentRecord', 'N/dialog', 'N/log'],
(currentRecord, dialog, log) => {

/**
* Page initialization
* @param {Object} context
* @param {Record} context.currentRecord - Current form record
* @param {string} context.mode - create, copy, or edit
*/
const pageInit = (context) => {
const rec = context.currentRecord;
const mode = context.mode;

log.debug('Page Init', `Mode: ${mode}`);

if (mode === 'create') {
// Set default values for new records
}
};

/**
* Field changed handler
* @param {Object} context
* @param {Record} context.currentRecord
* @param {string} context.fieldId - Changed field ID
* @param {string} context.sublistId - Sublist ID (if applicable)
* @param {number} context.line - Line number (if applicable)
*/
const fieldChanged = (context) => {
const rec = context.currentRecord;
const fieldId = context.fieldId;

if (fieldId === 'custbody_some_field') {
// React to field change
}
};

/**
* Save record validation
* @param {Object} context
* @param {Record} context.currentRecord
* @returns {boolean} - True to save, false to cancel
*/
const saveRecord = (context) => {
const rec = context.currentRecord;

// Validate before save
const isValid = validateForm(rec);

if (!isValid) {
dialog.alert({
title: 'Validation Error',
message: 'Please correct the errors before saving.'
});
return false;
}

return true;
};

/**
* Custom validation function
*/
const validateForm = (rec) => {
// Add validation logic
return true;
};

return {
pageInit,
fieldChanged,
saveRecord
};
});

Field Manipulation

Getting and Setting Field Values

const fieldChanged = (context) => {
const rec = context.currentRecord;
const fieldId = context.fieldId;

// Get field values
const textValue = rec.getValue({ fieldId: 'custbody_text_field' });
const selectValue = rec.getValue({ fieldId: 'entity' });
const selectText = rec.getText({ fieldId: 'entity' });

// Set field values
rec.setValue({
fieldId: 'custbody_calculated',
value: textValue.toUpperCase()
});

// Set select field by text
rec.setText({
fieldId: 'salesrep',
text: 'John Smith'
});
};

Show/Hide Fields

const fieldChanged = (context) => {
const rec = context.currentRecord;
const fieldId = context.fieldId;

if (fieldId === 'custbody_payment_type') {
const paymentType = rec.getValue({ fieldId: 'custbody_payment_type' });

// Get field objects
const creditCardField = rec.getField({ fieldId: 'custbody_credit_card' });
const checkNumberField = rec.getField({ fieldId: 'custbody_check_number' });

if (paymentType === 'credit_card') {
creditCardField.isDisplay = true;
checkNumberField.isDisplay = false;
} else if (paymentType === 'check') {
creditCardField.isDisplay = false;
checkNumberField.isDisplay = true;
} else {
creditCardField.isDisplay = false;
checkNumberField.isDisplay = false;
}
}
};

Enable/Disable Fields

const pageInit = (context) => {
const rec = context.currentRecord;
const mode = context.mode;

// Disable field in edit mode
if (mode === 'edit') {
const customerField = rec.getField({ fieldId: 'entity' });
customerField.isDisabled = true;
}
};

const fieldChanged = (context) => {
const rec = context.currentRecord;
const fieldId = context.fieldId;

if (fieldId === 'custbody_approved') {
const isApproved = rec.getValue({ fieldId: 'custbody_approved' });
const amountField = rec.getField({ fieldId: 'custbody_amount' });

// Disable amount if approved
amountField.isDisabled = isApproved;
}
};

Make Fields Mandatory

const fieldChanged = (context) => {
const rec = context.currentRecord;
const fieldId = context.fieldId;

if (fieldId === 'custbody_requires_approval') {
const requiresApproval = rec.getValue({ fieldId: 'custbody_requires_approval' });
const approverField = rec.getField({ fieldId: 'custbody_approver' });

// Make approver mandatory if approval required
approverField.isMandatory = requiresApproval;
}
};

Sublist Operations

Working with Sublist Lines

/**
* Line initialization
*/
const lineInit = (context) => {
const rec = context.currentRecord;
const sublistId = context.sublistId;

if (sublistId === 'item') {
// Set default quantity for new lines
const quantity = rec.getCurrentSublistValue({
sublistId: 'item',
fieldId: 'quantity'
});

if (!quantity) {
rec.setCurrentSublistValue({
sublistId: 'item',
fieldId: 'quantity',
value: 1
});
}
}
};

/**
* Field changed on sublist
*/
const fieldChanged = (context) => {
const rec = context.currentRecord;
const sublistId = context.sublistId;
const fieldId = context.fieldId;
const line = context.line;

// Only process item sublist changes
if (sublistId === 'item') {
if (fieldId === 'quantity' || fieldId === 'rate') {
// Calculate line amount
const quantity = rec.getCurrentSublistValue({
sublistId: 'item',
fieldId: 'quantity'
}) || 0;

const rate = rec.getCurrentSublistValue({
sublistId: 'item',
fieldId: 'rate'
}) || 0;

const amount = quantity * rate;

rec.setCurrentSublistValue({
sublistId: 'item',
fieldId: 'amount',
value: amount
});
}
}
};

/**
* Validate line before commit
*/
const validateLine = (context) => {
const rec = context.currentRecord;
const sublistId = context.sublistId;

if (sublistId === 'item') {
const quantity = rec.getCurrentSublistValue({
sublistId: 'item',
fieldId: 'quantity'
});

if (quantity <= 0) {
alert('Quantity must be greater than zero.');
return false;
}
}

return true;
};

/**
* After sublist line is committed
*/
const sublistChanged = (context) => {
const rec = context.currentRecord;
const sublistId = context.sublistId;

if (sublistId === 'item') {
// Recalculate header total
updateHeaderTotal(rec);
}
};

/**
* Helper: Update header total from lines
*/
const updateHeaderTotal = (rec) => {
const lineCount = rec.getLineCount({ sublistId: 'item' });
let total = 0;

for (let i = 0; i < lineCount; i++) {
const amount = rec.getSublistValue({
sublistId: 'item',
fieldId: 'amount',
line: i
}) || 0;

total += parseFloat(amount);
}

rec.setValue({
fieldId: 'custbody_calculated_total',
value: total
});
};

Sublist Iteration Flow

┌─────────────────────────────────────────────────────────────────────────────┐
│ SUBLIST ITERATION PATTERN │
└─────────────────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────────────────┐
│ const lineCount = rec.getLineCount({ sublistId: 'item' }); │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ for (let i = 0; i < lineCount; i++) { │ │
│ │ // Get value from committed line │ │
│ │ const value = rec.getSublistValue({ │ │
│ │ sublistId: 'item', │ │
│ │ fieldId: 'amount', │ │
│ │ line: i ◄──── Use line index │ │
│ │ }); │ │
│ │ } │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ // For CURRENT (uncommitted) line: │ │
│ │ const currentValue = rec.getCurrentSublistValue({ │ │
│ │ sublistId: 'item', │ │
│ │ fieldId: 'amount' ◄──── No line parameter needed │ │
│ │ }); │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘

Dialogs and Messages

Alert Dialog

const dialog = require('N/dialog');

const saveRecord = (context) => {
const rec = context.currentRecord;
const amount = rec.getValue({ fieldId: 'total' });

if (amount > 10000) {
dialog.alert({
title: 'High Value Warning',
message: 'This order exceeds $10,000. Please verify.'
});
}

return true;
};

Confirmation Dialog

const saveRecord = (context) => {
const rec = context.currentRecord;
const isRush = rec.getValue({ fieldId: 'custbody_rush_order' });

if (isRush) {
// Note: dialog.confirm is async, need different approach
const confirmed = confirm('Rush orders have additional fees. Continue?');
return confirmed;
}

return true;
};

Async Confirmation (Modern Approach)

/**
* For async operations, use button click handler instead
*/
const pageInit = (context) => {
// Add custom button via User Event beforeLoad instead
};

/**
* Custom button function (called from User Event button)
*/
const confirmAndProcess = () => {
const rec = currentRecord.get();

dialog.confirm({
title: 'Confirm Action',
message: 'Are you sure you want to process this order?'
}).then((result) => {
if (result) {
// User clicked OK
processOrder(rec);
}
}).catch((reason) => {
console.log('Dialog error: ' + reason);
});
};

Complete Example: Sales Order Validation

/**
* @NApiVersion 2.1
* @NScriptType ClientScript
* @NModuleScope SameAccount
* @description Sales Order client-side validation and calculations
*/
define(['N/currentRecord', 'N/dialog', 'N/search', 'N/log'],
(currentRecord, dialog, search, log) => {

// Constants
const MAX_ORDER_AMOUNT = 50000;
const MIN_QUANTITY = 1;

/**
* Page initialization - set defaults
*/
const pageInit = (context) => {
const rec = context.currentRecord;
const mode = context.mode;

log.debug('Page Init', `Mode: ${mode}`);

if (mode === 'create') {
// Set default order date to today
rec.setValue({
fieldId: 'trandate',
value: new Date()
});

// Set default memo
rec.setValue({
fieldId: 'memo',
value: 'Created via web interface'
});
}

// Initialize custom total field
updateOrderTotal(rec);
};

/**
* Handle field changes
*/
const fieldChanged = (context) => {
const rec = context.currentRecord;
const fieldId = context.fieldId;
const sublistId = context.sublistId;

// Handle body field changes
if (!sublistId) {
handleBodyFieldChange(rec, fieldId);
}

// Handle item sublist changes
if (sublistId === 'item') {
handleItemFieldChange(rec, fieldId);
}
};

/**
* Handle body field changes
*/
const handleBodyFieldChange = (rec, fieldId) => {
switch (fieldId) {
case 'entity':
// Customer changed - update sales rep
loadCustomerDefaults(rec);
break;

case 'custbody_payment_terms':
// Payment terms changed - show/hide fields
updatePaymentFields(rec);
break;

case 'custbody_rush_order':
// Rush order toggled
updateRushOrderFields(rec);
break;
}
};

/**
* Handle item line field changes
*/
const handleItemFieldChange = (rec, fieldId) => {
if (fieldId === 'quantity' || fieldId === 'rate') {
calculateLineAmount(rec);
}

if (fieldId === 'item') {
// Item selected - check inventory
checkItemAvailability(rec);
}
};

/**
* Load customer default values
*/
const loadCustomerDefaults = (rec) => {
const customerId = rec.getValue({ fieldId: 'entity' });

if (!customerId) return;

try {
const customerLookup = search.lookupFields({
type: search.Type.CUSTOMER,
id: customerId,
columns: ['salesrep', 'terms', 'creditlimit']
});

// Set sales rep if exists
if (customerLookup.salesrep && customerLookup.salesrep.length > 0) {
rec.setValue({
fieldId: 'salesrep',
value: customerLookup.salesrep[0].value
});
}

// Store credit limit for validation
const creditLimit = customerLookup.creditlimit || 0;
rec.setValue({
fieldId: 'custbody_customer_credit_limit',
value: creditLimit
});

} catch (e) {
log.error('Customer Lookup Error', e.message);
}
};

/**
* Calculate line amount
*/
const calculateLineAmount = (rec) => {
const quantity = parseFloat(rec.getCurrentSublistValue({
sublistId: 'item',
fieldId: 'quantity'
})) || 0;

const rate = parseFloat(rec.getCurrentSublistValue({
sublistId: 'item',
fieldId: 'rate'
})) || 0;

const amount = quantity * rate;

rec.setCurrentSublistValue({
sublistId: 'item',
fieldId: 'amount',
value: amount,
ignoreFieldChange: true
});
};

/**
* Check item availability
*/
const checkItemAvailability = (rec) => {
const itemId = rec.getCurrentSublistValue({
sublistId: 'item',
fieldId: 'item'
});

if (!itemId) return;

try {
const itemLookup = search.lookupFields({
type: search.Type.INVENTORY_ITEM,
id: itemId,
columns: ['quantityavailable', 'quantityonhand']
});

const available = parseFloat(itemLookup.quantityavailable) || 0;

if (available <= 0) {
dialog.alert({
title: 'Low Inventory',
message: 'This item is currently out of stock.'
});
} else if (available < 10) {
dialog.alert({
title: 'Low Inventory',
message: `Only ${available} units available.`
});
}

} catch (e) {
// Item may not be inventory item
log.debug('Item Lookup', 'Not an inventory item or lookup failed');
}
};

/**
* Update rush order related fields
*/
const updateRushOrderFields = (rec) => {
const isRush = rec.getValue({ fieldId: 'custbody_rush_order' });
const rushFeeField = rec.getField({ fieldId: 'custbody_rush_fee' });

if (rushFeeField) {
rushFeeField.isDisplay = isRush;
rushFeeField.isMandatory = isRush;

if (isRush) {
// Set default rush fee
rec.setValue({
fieldId: 'custbody_rush_fee',
value: 25.00
});
} else {
rec.setValue({
fieldId: 'custbody_rush_fee',
value: 0
});
}
}
};

/**
* Update payment-related fields
*/
const updatePaymentFields = (rec) => {
const terms = rec.getValue({ fieldId: 'custbody_payment_terms' });
const poField = rec.getField({ fieldId: 'otherrefnum' });

// Require PO number for Net 30+ terms
if (poField) {
poField.isMandatory = (terms === 'net30' || terms === 'net60');
}
};

/**
* Initialize line with defaults
*/
const lineInit = (context) => {
const rec = context.currentRecord;
const sublistId = context.sublistId;

if (sublistId === 'item') {
const quantity = rec.getCurrentSublistValue({
sublistId: 'item',
fieldId: 'quantity'
});

if (!quantity) {
rec.setCurrentSublistValue({
sublistId: 'item',
fieldId: 'quantity',
value: 1
});
}
}
};

/**
* Validate line before commit
*/
const validateLine = (context) => {
const rec = context.currentRecord;
const sublistId = context.sublistId;

if (sublistId === 'item') {
const item = rec.getCurrentSublistValue({
sublistId: 'item',
fieldId: 'item'
});

const quantity = parseFloat(rec.getCurrentSublistValue({
sublistId: 'item',
fieldId: 'quantity'
})) || 0;

const rate = parseFloat(rec.getCurrentSublistValue({
sublistId: 'item',
fieldId: 'rate'
})) || 0;

// Validate item selected
if (!item) {
alert('Please select an item.');
return false;
}

// Validate quantity
if (quantity < MIN_QUANTITY) {
alert(`Quantity must be at least ${MIN_QUANTITY}.`);
return false;
}

// Validate rate
if (rate <= 0) {
alert('Rate must be greater than zero.');
return false;
}
}

return true;
};

/**
* After line is committed, update totals
*/
const sublistChanged = (context) => {
const rec = context.currentRecord;
const sublistId = context.sublistId;

if (sublistId === 'item') {
updateOrderTotal(rec);
}
};

/**
* Update order total from lines
*/
const updateOrderTotal = (rec) => {
const lineCount = rec.getLineCount({ sublistId: 'item' });
let subtotal = 0;

for (let i = 0; i < lineCount; i++) {
const amount = parseFloat(rec.getSublistValue({
sublistId: 'item',
fieldId: 'amount',
line: i
})) || 0;

subtotal += amount;
}

// Add rush fee if applicable
const rushFee = parseFloat(rec.getValue({
fieldId: 'custbody_rush_fee'
})) || 0;

const total = subtotal + rushFee;

rec.setValue({
fieldId: 'custbody_order_total',
value: total,
ignoreFieldChange: true
});
};

/**
* Final validation before save
*/
const saveRecord = (context) => {
const rec = context.currentRecord;

// Validate has items
const lineCount = rec.getLineCount({ sublistId: 'item' });
if (lineCount === 0) {
dialog.alert({
title: 'Validation Error',
message: 'Please add at least one item to the order.'
});
return false;
}

// Validate order amount
const orderTotal = parseFloat(rec.getValue({
fieldId: 'custbody_order_total'
})) || 0;

if (orderTotal > MAX_ORDER_AMOUNT) {
const confirmed = confirm(
`This order exceeds $${MAX_ORDER_AMOUNT.toLocaleString()}. ` +
'Manager approval will be required. Continue?'
);

if (!confirmed) {
return false;
}

// Flag for approval
rec.setValue({
fieldId: 'custbody_requires_approval',
value: true
});
}

// Validate credit limit
const creditLimit = parseFloat(rec.getValue({
fieldId: 'custbody_customer_credit_limit'
})) || 0;

if (creditLimit > 0 && orderTotal > creditLimit) {
dialog.alert({
title: 'Credit Limit Exceeded',
message: `Order total ($${orderTotal.toFixed(2)}) exceeds ` +
`customer credit limit ($${creditLimit.toFixed(2)}).`
});
return false;
}

// Validate rush order has ship date
const isRush = rec.getValue({ fieldId: 'custbody_rush_order' });
if (isRush) {
const shipDate = rec.getValue({ fieldId: 'shipdate' });
if (!shipDate) {
dialog.alert({
title: 'Validation Error',
message: 'Rush orders require a ship date.'
});
return false;
}
}

log.debug('Save Record', 'Validation passed');
return true;
};

return {
pageInit,
fieldChanged,
lineInit,
validateLine,
sublistChanged,
saveRecord
};
});

Script Deployment XML

<?xml version="1.0" encoding="UTF-8"?>
<clientscript scriptid="customscript_sales_order_cs">
<name>Sales Order Client Script</name>
<scriptfile>[/SuiteScripts/ClientScripts/sales_order_cs.js]</scriptfile>
<description>Client-side validation for sales orders</description>
<isinactive>F</isinactive>
<notifyowner>T</notifyowner>

<scriptdeployments>
<scriptdeployment scriptid="customdeploy_sales_order_cs">
<status>RELEASED</status>
<recordtype>salesorder</recordtype>
<isdeployed>T</isdeployed>
<loglevel>DEBUG</loglevel>
<allroles>T</allroles>
</scriptdeployment>
</scriptdeployments>
</clientscript>

Best Practices

PracticeDescription
Minimize API callsCache values, avoid repeated lookups
Use ignoreFieldChangePrevent recursive triggers when setting values
Handle errors gracefullyWrap lookups in try/catch
Clear user messagesUse dialog for errors, not just alert()
Test all modesTest create, edit, and copy modes
Validate sublist operationsAlways validate before line commit

Common Patterns

Cascading Select Lists

const fieldChanged = (context) => {
const rec = context.currentRecord;
const fieldId = context.fieldId;

if (fieldId === 'custbody_country') {
const country = rec.getValue({ fieldId: 'custbody_country' });

// Clear dependent field
rec.setValue({
fieldId: 'custbody_state',
value: ''
});

// State field options are filtered by saved search
// that references country field
}
};

Date Validation

const fieldChanged = (context) => {
const rec = context.currentRecord;
const fieldId = context.fieldId;

if (fieldId === 'shipdate') {
const orderDate = rec.getValue({ fieldId: 'trandate' });
const shipDate = rec.getValue({ fieldId: 'shipdate' });

if (shipDate < orderDate) {
dialog.alert({
title: 'Invalid Date',
message: 'Ship date cannot be before order date.'
});

rec.setValue({
fieldId: 'shipdate',
value: orderDate
});
}
}
};

Next Steps