Developer ToolsFree generatorPDF, Excel, Word exportNetSuiteAll NetSuite tools

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.

Generated code
Download:

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.

Was this tool useful?

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.

ArchRay builder
Design the on-prem AI stack for NetSuite: model, GPUs, RAG pipeline, guardrails
See memory, speed and cost as you swap parts, then export the diagram. Free, in your browser.
Open the builder

Keep exploring

Free tools

ERP fixes and how-tos

Integrations

Software