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 Case | Example |
|---|---|
| Field validation | Ensure email format is valid |
| Calculated fields | Update total when quantity changes |
| UI control | Show/hide fields based on selection |
| User guidance | Display warnings before save |
| Field sourcing | Auto-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 Point | Triggers When | Common Uses |
|---|---|---|
pageInit | Page loads | Set defaults, initialize UI |
fieldChanged | Field value changes | Validation, calculations |
postSourcing | After field sourcing completes | Modify sourced values |
lineInit | Sublist line selected | Set line defaults |
validateField | Before field change commits | Block invalid values |
validateLine | Before line commits | Validate line data |
validateInsert | Before new line insert | Validate new lines |
validateDelete | Before line delete | Confirm deletion |
sublistChanged | After line commits | Update totals |
saveRecord | Before record saves | Final 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
| Practice | Description |
|---|---|
| Minimize API calls | Cache values, avoid repeated lookups |
| Use ignoreFieldChange | Prevent recursive triggers when setting values |
| Handle errors gracefully | Wrap lookups in try/catch |
| Clear user messages | Use dialog for errors, not just alert() |
| Test all modes | Test create, edit, and copy modes |
| Validate sublist operations | Always 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
- Scheduled Script - Run background processing
- Map/Reduce Script - Process large datasets
- User Event Scripts - Server-side automation