Developer ToolsFree generatorPDF, Excel, Word exportNetSuiteAll NetSuite tools

SuiteScript 2.1 Suitelet Form Generator for NetSuite

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.

Fill in

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.

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

Generated code
Download:

Suitelet

/**
 * @NApiVersion 2.1
 * @NScriptType Suitelet
 * @NModuleScope SameAccount
 */
define(['N/ui/serverWidget', 'N/query', 'N/log'], (serverWidget, query, log) => {
  const FORM_TITLE = 'Work Order Release';
  // id|TYPE|Label|source ; separated. TYPE is a serverWidget.FieldType key.
  const FIELD_SPEC = 'custpage_location|SELECT|Location|location;custpage_from|DATE|Due From|';
  const PARAM_FIELD = 'custpage_location';
  const QUERY_TEXT = "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";
  const SUBMIT_LABEL = 'Show work orders';
  const ADD_SUBLIST = 'Yes' === 'Yes';
  const CLIENT_SCRIPT_PATH = '[Client script path (optional)]';

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

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.

Need a custom NetSuite screen?

Describe the screen and the data behind it and we will tell you whether a Suitelet, saved search or workbook is the right fit.

Optional. The tool and every export work without this. No spam.

Was this tool useful?

How to use it

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.

  • 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

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

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

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.

Frequently Asked Questions

What is a NetSuite Suitelet?

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?

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?

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?

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.

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.

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

AI on your ERP

Integrations

Software