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.
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.
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.
Related Tools
SuiteScript 2.1 Client Script Generator for NetSuite
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.
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.
Developer ToolsNetSuite REST API SuiteQL Request Generator
Turn a SuiteQL statement into a ready POST to the REST query/v1/suiteql endpoint, plus curl and a Python script that pages through every result.
NetSuiteSuiteQL Query Generator
Fill in a table, columns, filter and sort and get a SuiteQL statement ready for the N/query module, the REST SuiteQL endpoint or the SuiteAnalytics Workbook.
NetSuiteSuiteScript 2.x Modules Reference
A searchable table of the SuiteScript 2.x N/ modules, what each one is for and the methods you reach for most.
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 Sales Order and Journal Entry Request Generator
- NetSuite Token-Based Auth OAuth 1.0 Signature Code Generator
- NetSuite OAuth 2.0 Client Credentials (M2M) JWT Code Generator
Keep exploring
Free tools
- NetSuite SuiteScript 2.1 RESTlet Skeleton GeneratorFill in a script name, record type and fields to get a SuiteScript 2.1 RESTlet with GET lookup, POST create, logging and error handling.
- SuiteScript 2.1 Client Script Generator for NetSuiteGenerate 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.
- 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.
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.
- Fix NetSuite SSS_REQUEST_TIME_EXCEEDED ErrorSSS_REQUEST_TIME_EXCEEDED means a synchronous NetSuite request ran too long and was killed on wall-clock time, not governance units. Cause and fix explained.
- How to write and run SuiteQL queries in NetSuiteRun SuiteQL from Analytics > SuiteQL Query Tool or via N/query in SuiteScript. Syntax basics, joins, pagination and real examples for NetSuite reporting.
AI on your ERP
- AI for NetSuite, Beyond the Built-In Text ToolsNetSuite's built-in AI covers text generation, not grounded answers on your own data. See how a private LLM over SuiteQL adds real Q&A and controls.
- AI agents for NetSuite manufacturing operationsAI agents for NetSuite manufacturing: WIP tracking, routing exceptions, and work order status grounded in SuiteQL, with human approval on anything that writes back.
Integrations
- How to add an AI chatbot to Oracle NetSuiteAdd an AI chatbot to Oracle NetSuite for staff, customers or suppliers: Suitelet or portal embed, grounded answers from SuiteQL, roles, and on-prem models.
- 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.