# SuiteScript 2.1 Client Script Generator for NetSuite

> Source: https://netray.co/tools/netsuite-suitescript-21-client-script-generator
> Publisher: Netray (netray.co), AI and integration engineering for ERP and manufacturing systems.
> Section: Free Tools

## Short answer

Generate a working client script for a transaction or customer form: field checks, line validation with a dialog, sublist totals and a save guard, exporting only the entry points you pick.

## Category

developer

## Erp

NetSuite

## Audience

- SuiteScript developer
- NetSuite administrator

## Tool

**Kind:** template

**Output:** code

**Language:** javascript

**Fields:**

#### Record type to deploy on

**Key:** record_type

**Type:** select

**Options:**

- salesorder
- purchaseorder
- vendorbill
- workorder
- customer

**Default:** salesorder

**Help:** Recorded in the header comment; set the actual record on the script deployment.

#### Entry points to export

**Key:** entry_points

**Type:** select

**Options:**

- pageInit, fieldChanged, saveRecord
- validateLine, sublistChanged
- pageInit, fieldChanged, validateField, postSourcing, lineInit, validateLine, validateInsert, validateDelete, sublistChanged, saveRecord

**Default:** pageInit, fieldChanged, validateField, postSourcing, lineInit, validateLine, validateInsert, validateDelete, sublistChanged, saveRecord

#### Body field to watch and require on save

**Key:** watched_field

**Type:** text

**Default:** custbody_ship_priority

#### Sublist ID

**Key:** sublist_id

**Type:** text

**Default:** item

**Help:** item on sales and purchase orders, expense for bill expense lines.

#### Line field to validate

**Key:** line_field

**Type:** text

**Default:** quantity

#### Maximum allowed line value

**Key:** max_line_qty

**Type:** number

**Default:** 1000

#### Save blocked message

**Key:** alert_message

**Type:** text

**Default:** Set Ship Priority before saving this order.

**Help:** Avoid apostrophes, the value sits in a single-quoted string.

**Blocks:**

#### Client script

**Body:** /**
 * @NApiVersion 2.1
 * @NScriptType ClientScript
 * @NModuleScope SameAccount
 */
// Deploy to record type: {{record_type}}
define(['N/currentRecord', 'N/ui/dialog', 'N/log'], (currentRecord, dialog, log) => {
  const WATCHED_FIELD = '{{watched_field}}';
  const SUBLIST_ID = '{{sublist_id}}';
  const LINE_FIELD = '{{line_field}}';
  const MAX_LINE_QTY = {{max_line_qty}};
  const ALERT_MESSAGE = '{{alert_message}}';

  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 { {{entry_points}} };
});

**Notes:** 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.

## Content

**Intro:** 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.

**Sections:**

#### How to use it

**Body:** 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.

**Bullets:**

- 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

**Body:** 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

**Body:** 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

**Body:** 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.

**Cta:** 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.

**Faq:**

#### What are client scripts in NetSuite?

**Answer:** 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?

**Answer:** 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?

**Answer:** 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?

**Answer:** 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.

## Search terms

client scripts in netsuite, netsuite suitescript, netsuite suitescript tutorial, netsuite test suitescript to display results, suitescript 2.1 client script example, netsuite validateline example, netsuite saverecord client script, suitescript fieldchanged

---
Talk to an engineer: https://netray.co/contact?intent=erp_ai | Machine catalog: https://netray.co/agent/catalog.json
