# SuiteScript 2.1 Suitelet Form Generator for NetSuite

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

## Short answer

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.

## Category

developer

## Erp

NetSuite

## Audience

- SuiteScript developer
- NetSuite administrator

## Tool

**Kind:** template

**Output:** code

**Language:** javascript

**Fields:**

#### Form title

**Key:** form_title

**Type:** text

**Default:** Work Order Release

#### Fields (id|TYPE|Label|source, separated by ;)

**Key:** fields_spec

**Type:** text

**Default:** custpage_location|SELECT|Location|location;custpage_from|DATE|Due From|

**Help:** IDs must start with custpage. TYPE is a serverWidget.FieldType key such as TEXT, DATE, SELECT, CHECKBOX, INTEGER. Source is a record type for SELECT fields.

#### Field whose value is bound to the ? in the query

**Key:** param_field

**Type:** text

**Default:** custpage_location

#### SuiteQL query (one ? parameter)

**Key:** query_text

**Type:** textarea

**Default:** SELECT t.id, t.tranid, BUILTIN.DF(t.status) AS status, t.trandate FROM transaction t INNER JOIN transactionline tl ON tl.transaction = t.id AND tl.mainline = 'T' WHERE t.type = 'WorkOrd' AND tl.location = ? ORDER BY t.id

**Help:** Single line, single quotes only. Alias every column: aliases become sublist column labels.

#### Submit button label

**Key:** submit_label

**Type:** text

**Default:** Show work orders

#### Show results sublist on submit

**Key:** add_sublist

**Type:** select

**Options:**

- Yes
- No

**Default:** Yes

#### Client script path (optional)

**Key:** client_script_path

**Type:** text

**Placeholder:** ./wo_release_client.js

**Blocks:**

#### Suitelet

**Body:** /**
 * @NApiVersion 2.1
 * @NScriptType Suitelet
 * @NModuleScope SameAccount
 */
define(['N/ui/serverWidget', 'N/query', 'N/log'], (serverWidget, query, log) => {
  const FORM_TITLE = '{{form_title}}';
  // id|TYPE|Label|source ; separated. TYPE is a serverWidget.FieldType key.
  const FIELD_SPEC = '{{fields_spec}}';
  const PARAM_FIELD = '{{param_field}}';
  const QUERY_TEXT = "{{query_text}}";
  const SUBMIT_LABEL = '{{submit_label}}';
  const ADD_SUBLIST = '{{add_sublist}}' === 'Yes';
  const CLIENT_SCRIPT_PATH = '{{client_script_path}}';

  const buildForm = (values) => {
    const form = serverWidget.createForm({ title: FORM_TITLE });
    FIELD_SPEC.split(';').filter((s) => s.trim()).forEach((spec) => {
      const [id, type, label, source] = spec.split('|').map((p) => (p || '').trim());
      const options = { id: id, type: serverWidget.FieldType[type], label: label };
      if (source) options.source = source;
      const field = form.addField(options);
      if (values && values[id]) field.defaultValue = values[id];
    });
    form.addSubmitButton({ label: SUBMIT_LABEL });
    if (CLIENT_SCRIPT_PATH) form.clientScriptModulePath = CLIENT_SCRIPT_PATH;
    return form;
  };

  const addResults = (form, rows) => {
    const sublist = form.addSublist({
      id: 'custpage_results',
      type: serverWidget.SublistType.LIST,
      label: 'Results (' + rows.length + ')'
    });
    if (rows.length === 0) return;
    const keys = Object.keys(rows[0]);
    keys.forEach((key) => {
      sublist.addField({ id: 'custpage_col_' + key, type: serverWidget.FieldType.TEXT, label: key });
    });
    rows.forEach((row, line) => {
      keys.forEach((key) => {
        const value = row[key];
        // setSublistValue rejects empty values, so skip them
        if (value !== null && value !== undefined && value !== '') {
          sublist.setSublistValue({ id: 'custpage_col_' + key, line: line, value: String(value) });
        }
      });
    });
  };

  const onRequest = (context) => {
    if (context.request.method === 'GET') {
      context.response.writePage(buildForm(null));
      return;
    }
    const params = context.request.parameters;
    const form = buildForm(params);
    const paramValue = params[PARAM_FIELD];
    if (ADD_SUBLIST && paramValue) {
      const rows = query.runSuiteQL({ query: QUERY_TEXT, params: [paramValue] }).asMappedResults();
      log.audit({ title: 'Suitelet query', details: rows.length + ' rows for ' + PARAM_FIELD + '=' + paramValue });
      addResults(form, rows.slice(0, 1000));
    }
    context.response.writePage(form);
  };

  return { onRequest };
});

**Notes:** Verify against the N/ui/serverWidget and N/query topics for your release. Suitelets have a 1,000-unit governance limit; query.runSuiteQL costs 10 units and returns at most 5,000 rows, so use runSuiteQLPaged for more. All form field and sublist IDs must start with custpage. Leave Available Without Login unchecked on the deployment unless you intend a public page: that setting exposes the Suitelet externally and runs it without a user session.

## Content

**Intro:** A Suitelet is a SuiteScript that serves its own page inside NetSuite, and this generator writes one: a serverWidget form built from your field list, a submit button, and a results sublist filled by a SuiteQL query on POST. It is a quick base for internal tools such as a work order release screen or an exception list.

**Sections:**

#### How to use it

**Body:** List the filter fields in the id|TYPE|Label|source format, choose which field feeds the query parameter, and paste a single-line SuiteQL query with one ? placeholder. Copy the code into a .js file in the File Cabinet, create a Script record of type Suitelet, and deploy it with status Testing. Open the deployment's URL, pick a location and submit: the page reloads with the results sublist. To give users a menu link, add a link on the deployment's Links subtab under a center tab and section.

**Bullets:**

- Set the deployment audience to the roles that should open the page.
- Execute As Role decides what data the query can see.
- Add a client script path to validate fields before submit.

#### What the code does

**Body:** On GET it calls serverWidget.createForm({ title }) and adds each field with form.addField({ id, type, label, source }), using serverWidget.FieldType keys such as SELECT and DATE; a SELECT with source location lists locations. form.addSubmitButton adds the button, and form.clientScriptModulePath attaches an optional client script. context.response.writePage(form) renders it. On POST it reads context.request.parameters, rebuilds the form with the submitted values as defaults, runs query.runSuiteQL({ query, params }).asMappedResults(), and adds a LIST sublist with form.addSublist. Each column becomes a TEXT field, and values are written with sublist.setSublistValue({ id, line, value }).

#### Version differences and gotchas

**Body:** Field and sublist IDs must begin with custpage, or addField throws an error. setSublistValue fails on empty values, which is why the code skips null and empty strings. asMappedResults returns column names in lowercase, so the sublist field IDs stay valid. runSuiteQL returns up to 5,000 rows and the Suitelet has 1,000 units, so heavy reports belong in a saved search, workbook or a paged query. Dates entered in a DATE field arrive in the user's date format; convert them with N/format before binding them to a query. After processing a submit that writes data, redirect to the GET page so a browser refresh does not resubmit.

#### Checking the result

**Body:** Run the query alone first in a SuiteQL tool with a real location ID to confirm the columns and row count. In the Suitelet, check the execution log for the audit line showing rows returned for the parameter value. If the results sublist is empty but rows exist, the Execute As Role probably cannot see those transactions or locations. If the page errors on load, the log shows which field spec failed, usually a TYPE key that is not a FieldType name or an ID without the custpage prefix.

**Cta:** If your team wants internal screens like this answered by a private assistant that queries NetSuite under their own roles, we can build it on your infrastructure.

**Faq:**

#### What is a NetSuite Suitelet?

**Answer:** A Suitelet is a server-side SuiteScript that handles HTTP GET and POST requests and returns a page or data. With N/ui/serverWidget it renders forms, lists and sublists that look like native NetSuite pages, which makes it the usual way to build custom internal screens, wizards and approval pages.

#### How many governance units does a Suitelet have?

**Answer:** A Suitelet has 1,000 units per request. That is enough for forms and a few queries (runSuiteQL costs 10 units) but not for loading and saving many records. Move bulk work to a map/reduce or scheduled script and start it from the Suitelet with N/task.

#### How do I add a sublist to a Suitelet form?

**Answer:** Call form.addSublist({ id: 'custpage_...', type: serverWidget.SublistType.LIST, label }), add columns with sublist.addField({ id, type, label }), then fill rows with sublist.setSublistValue({ id, line, value }) for each non-empty value. Use INLINEEDITOR or EDITOR types when users must edit lines.

#### Where do I find field IDs for a Suitelet query?

**Answer:** Use the Records Catalog (Setup > Records Catalog) for SuiteQL field and join names, and the SuiteScript Records Browser for record and sublist field IDs used in N/record. Turn on Show Internal IDs in preferences to see IDs on forms. Custom fields show their ID on the field record.

## Search terms

what is a netsuite suitelet, netsuite suitelet example, netsuite suitescript, suitescript 2.1 suitelet form, netsuite serverwidget createform, netsuite suitelet sublist, netsuite suitescript record browser, netsuite html script notes

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