SuiteScript 2.1 Client Script Generator for NetSuite
This generator writes a SuiteScript 2.1 client script with the common entry points wired to real context properties: a watched body field, line validation that blocks bad quantities with a dialog, a running sublist total and a save guard. Pick which entry points to export and deploy it on a sales order, purchase order, vendor bill, work order or customer form.
Fill in
Recorded in the header comment; set the actual record on the script deployment.
item on sales and purchase orders, expense for bill expense lines.
Avoid apostrophes, the value sits in a single-quoted string.
Client script
/**
* @NApiVersion 2.1
* @NScriptType ClientScript
* @NModuleScope SameAccount
*/
// Deploy to record type: salesorder
define(['N/currentRecord', 'N/ui/dialog', 'N/log'], (currentRecord, dialog, log) => {
const WATCHED_FIELD = 'custbody_ship_priority';
const SUBLIST_ID = 'item';
const LINE_FIELD = 'quantity';
const MAX_LINE_QTY = 1000;
const ALERT_MESSAGE = 'Set Ship Priority before saving this order.';
const pageInit = (context) => {
// context.mode is 'create', 'copy' or 'edit'
if (context.mode === 'create' || context.mode === 'copy') {
log.debug({ title: 'pageInit', details: 'New record, mode ' + context.mode });
}
};
const fieldChanged = (context) => {
if (!context.sublistId && context.fieldId === WATCHED_FIELD) {
const value = context.currentRecord.getValue({ fieldId: WATCHED_FIELD });
log.debug({ title: 'fieldChanged', details: WATCHED_FIELD + ' = ' + value });
}
};
const validateField = (context) => {
if (context.sublistId === SUBLIST_ID && context.fieldId === LINE_FIELD) {
const qty = Number(context.currentRecord.getCurrentSublistValue({ sublistId: SUBLIST_ID, fieldId: LINE_FIELD })) || 0;
if (qty < 0) {
dialog.alert({ title: 'Invalid value', message: 'The value cannot be negative.' });
return false;
}
}
return true;
};
const postSourcing = (context) => {
if (context.sublistId === SUBLIST_ID && context.fieldId === 'item') {
log.debug({ title: 'postSourcing', details: 'Item sourced on line' });
}
};
const lineInit = (context) => {
if (context.sublistId === SUBLIST_ID) {
log.debug({ title: 'lineInit', details: 'Line selected on ' + SUBLIST_ID });
}
};
const validateLine = (context) => {
if (context.sublistId !== SUBLIST_ID) return true;
const qty = Number(context.currentRecord.getCurrentSublistValue({ sublistId: SUBLIST_ID, fieldId: LINE_FIELD })) || 0;
if (qty > MAX_LINE_QTY) {
dialog.alert({ title: 'Line not accepted', message: LINE_FIELD + ' ' + qty + ' is above the limit of ' + MAX_LINE_QTY + '.' });
return false;
}
return true;
};
const validateInsert = (context) => true;
const validateDelete = (context) => true;
const sublistChanged = (context) => {
if (context.sublistId !== SUBLIST_ID) return;
const rec = context.currentRecord;
let total = 0;
for (let i = 0; i < rec.getLineCount({ sublistId: SUBLIST_ID }); i++) {
total += Number(rec.getSublistValue({ sublistId: SUBLIST_ID, fieldId: LINE_FIELD, line: i })) || 0;
}
log.debug({ title: 'sublistChanged', details: 'operation ' + context.operation + ', total ' + LINE_FIELD + ' ' + total });
};
const saveRecord = (context) => {
const value = context.currentRecord.getValue({ fieldId: WATCHED_FIELD });
if (value === '' || value === null || value === false) {
dialog.alert({ title: 'Cannot save yet', message: ALERT_MESSAGE });
return false;
}
return true;
};
return { pageInit, fieldChanged, validateField, postSourcing, lineInit, validateLine, validateInsert, validateDelete, sublistChanged, saveRecord };
});Verify against the SuiteScript 2.x Client Script Type topic for your release. Client scripts run only in the browser UI: not for CSV import, web services, RESTlets, scheduled scripts or workflows, so put rules that must always hold in a user event script too. Use N/currentRecord (context.currentRecord), not N/record, for the open form. Client scripts have a 1,000-unit governance limit per script.
Get your form scripts reviewed
Send us the client and user event scripts on a form and we will flag rules that only hold in the UI and scripts that slow it down.
Optional. The tool and every export work without this. No spam.
How to use it
Choose the record type, the entry points to export, the body field to require, and the sublist and line field to validate. Copy the code into a .js file, upload it to the File Cabinet, and create a Script record of type Client Script. Deploy it on the record type, set the deployment status to Testing with yourself as audience, and open a record in edit mode. Messages from log.debug appear in the script execution log, and dialog.alert shows a popup. Once it behaves, set the status to Released and the audience to the right roles.
- Alternative: attach the file on a custom form's Custom Code subtab, so it runs only on that form.
- Use the browser console and debugger statements for step-through testing.
- Hard refresh the page after uploading a new file version.
Entry points and context properties
pageInit receives context.currentRecord and context.mode (create, copy or edit). fieldChanged and validateField receive currentRecord, sublistId, fieldId, line and column; validateField must return true or false. postSourcing fires after a field's dependent values are sourced, for example after an item is set on a line. lineInit fires when a line is selected. validateLine, validateInsert and validateDelete receive currentRecord and sublistId and return a boolean that accepts or rejects the line action. sublistChanged receives sublistId and operation after a line commit. saveRecord takes currentRecord and returns false to stop the save. All return values that are not true reject the action, so always end validators with return true.
Worked example
With the defaults on a sales order: when a user commits an item line with quantity 1200, validateLine reads the current line with getCurrentSublistValue, sees it is above 1000, shows a dialog and returns false, so the line stays open for correction. A line with 400 commits, and sublistChanged then sums quantity across all lines with getSublistValue and logs the total. When the user clicks Save without Ship Priority set, saveRecord shows the message and returns false. Note the difference: getCurrentSublistValue reads the line being edited, getSublistValue reads committed lines by index.
Common mistakes
Treating a client script as validation for every channel: imports, integrations and server scripts bypass it, so duplicate critical rules in a beforeSubmit user event. Loading N/record and calling record.load inside fieldChanged, which costs units and slows every keystroke. Forgetting return true at the end of validateField or validateLine, which blocks every edit. Setting a field inside fieldChanged that triggers fieldChanged again without a guard, causing loops; pass ignoreFieldChange: true on setValue. Calling server-side modules such as N/task that are not available in client scripts. Using an alert message with an apostrophe without escaping it.
Frequently Asked Questions
What are client scripts in NetSuite?
Client scripts are SuiteScript files that run in the user's browser on a record form. They react to page load, field changes, line edits and save, so they can validate input, default values and warn users before data is submitted. They do not run for imports, web services or server-side scripts.
How do I test a SuiteScript client script and see results?
Deploy with status Testing and yourself in the audience, open the record, and watch dialog.alert popups and log.debug entries on the deployment's execution log. For step-through debugging, open the browser developer tools; the script file appears under the sources, where you can set breakpoints.
What is the difference between validateLine and sublistChanged?
validateLine runs before a line is committed and returns true to accept or false to reject it. sublistChanged runs after a line is committed, inserted or removed, returns nothing, and is the place to recalculate totals or update body fields based on all lines.
Should I use N/record or N/currentRecord in a client script?
For the form the user has open, use the record passed as context.currentRecord, which is an N/currentRecord object. N/record is for loading or changing other records and costs governance units. In client scripts, prefer N/search lookupFields for small reads of related records.
If you want an assistant that explains a NetSuite form's scripts and rules in plain English for your admins, running on your own infrastructure, we can set that up.
Related Tools
SuiteScript 2.1 Suitelet Form Generator for NetSuite
Describe the filter fields and a SuiteQL query and get a working Suitelet that renders a NetSuite-styled form, runs the query on submit and lists the results.
Developer ToolsSuiteScript 2.1 Scheduled Script Generator for NetSuite
Generate a restartable scheduled script that processes a saved search in internal ID order, checks remaining usage and resubmits itself before hitting the 10,000-unit limit.
NetSuiteSuiteScript 2.1 User Event Script Skeleton
A copy-ready SuiteScript 2.1 user event script with the JSDoc annotations, entry points, context checks and error logging you need to start.
NetSuiteSuiteScript Script Types and Entry Points Reference
Every SuiteScript 2.x script type with its entry point functions, typical use and governance guidance in one searchable table.
NetSuiteSuiteScript Code Review Checklist
A practical review checklist for SuiteScript 2.x covering structure, governance, error handling, security and deployment.
More Developer Tools tools
See all- NetSuite SuiteScript 2.1 RESTlet Skeleton Generator
- NetSuite SuiteScript Governance Units by Script Type
- NetSuite API Retry Backoff Wait Time Calculator
- NetSuite OAuth 2.0 Access Token Refresh Calculator
- NetSuite REST Web Services Headers and Query Parameters Reference
- SuiteScript 1.0 to 2.1 API Mapping Reference for NetSuite
- NetSuite REST API SuiteQL Request Generator
- NetSuite REST API Sales Order and Journal Entry Request Generator
- NetSuite Token-Based Auth OAuth 1.0 Signature Code Generator
Keep exploring
Free tools
- NetSuite Workflow Action Script Generator (SuiteScript 2.1)Generate a working workflow action script that reads a threshold parameter, looks up an approver and returns a value your workflow can store in a field.
- SuiteScript 2.1 User Event Script SkeletonA copy-ready SuiteScript 2.1 user event script with the JSDoc annotations, entry points, context checks and error logging you need to start.
- SuiteScript 2.1 Scheduled Script Generator for NetSuiteGenerate a restartable scheduled script that processes a saved search in internal ID order, checks remaining usage and resubmits itself before hitting the 10,000-unit limit.
ERP fixes and how-tos
- N/query versus N/search: choosing the right SuiteScript data access moduleCompare NetSuite's N/query (SuiteQL) and N/search modules for performance, governance cost and capability, and decide which to use for joins, aggregation, sublist reads and large exports in SuiteScript 2.1.
- Choosing between User Event and Client scripts in NetSuiteChoose between NetSuite User Event and Client scripts correctly: which runs server-side and cannot be bypassed, which is interactive-only, and how beforeLoad, beforeSubmit, afterSubmit and pageInit, fieldChanged, saveRecord map to real use cases.
- Avoiding governance limit errors in NetSuite Map/Reduce scriptsUnderstand how governance is allocated per key in NetSuite Map/Reduce scripts, why SSS_USAGE_LIMIT_EXCEEDED still happens, and how to design getInputData, map, reduce and summarize stages that do not time out.
Integrations
- How to add WhatsApp to Oracle NetSuiteHow to connect WhatsApp Business to Oracle NetSuite: SuiteScript triggers, RESTlets, template messages, opt-in rules and two-way replies that post back to records.
- How to integrate Microsoft Teams with Oracle NetSuiteConnect Oracle NetSuite to Microsoft Teams: approval cards, alerts, bots and Entra ID sign-in using SuiteScript, RESTlets and Power Automate or Bot Framework.