Skip to main content

Suitelet Development

Suitelets are server-side scripts that create custom NetSuite pages, forms, and reports accessible via URL.


When to Use Suitelets​

Use CaseExample
Custom formsData entry forms not tied to records
ReportsCustom reports with export functionality
WizardsMulti-step processes
DashboardsCustom data visualizations
Integration pagesExternal system data display

Suitelet Execution Flow​

┌─────────────────────────────────────────────────────────────────────────────┐
│ SUITELET EXECUTION FLOW │
└─────────────────────────────────────────────────────────────────────────────┘

┌──────────────┐
│ User │
│ Clicks URL │
└──────┬───────┘
│
▼
┌─────────────────────────────────────┐
│ 1. GET Request │
│ ───────────────────────────────────│
│ Browser sends request to Suitelet │
│ URL with any parameters │
└───────────┬─────────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ 2. onRequest (GET) │
│ ───────────────────────────────────│
│ • Create form with fields │
│ • Load dropdown data │
│ • Display initial page │
└───────────┬─────────────────────────┘
│
▼
┌──────────────┐
│ User │
│ Fills Form │
│ Clicks │
│ Submit │
└──────┬───────┘
│
▼
┌─────────────────────────────────────┐
│ 3. POST Request │
│ ───────────────────────────────────│
│ Form data sent back to Suitelet │
└───────────┬─────────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ 4. onRequest (POST) │
│ ───────────────────────────────────│
│ • Read form parameters │
│ • Process data (query, create) │
│ • Display results │
└───────────┬─────────────────────────┘
│
▼
┌──────────────┐
│ Results │
│ Displayed │
└──────────────┘

Basic Suitelet Structure​

/**
* @NApiVersion 2.1
* @NScriptType Suitelet
* @NModuleScope SameAccount
*/
define(['N/ui/serverWidget', 'N/search', 'N/record', 'N/log'],
(serverWidget, search, record, log) => {

/**
* Main entry point for Suitelet
* @param {Object} context
* @param {ServerRequest} context.request - HTTP request
* @param {ServerResponse} context.response - HTTP response
*/
const onRequest = (context) => {
if (context.request.method === 'GET') {
// Handle GET - display form
handleGet(context);
} else {
// Handle POST - process form
handlePost(context);
}
};

/**
* Handle GET request - display form
*/
const handleGet = (context) => {
const form = serverWidget.createForm({
title: 'My Suitelet Form'
});

// Add fields here

context.response.writePage(form);
};

/**
* Handle POST request - process form
*/
const handlePost = (context) => {
const params = context.request.parameters;

// Process form data here

// Display results or redirect
};

return { onRequest };
});

Creating Forms​

Form with Field Group​

const handleGet = (context) => {
const form = serverWidget.createForm({
title: 'Customer Search'
});

// Add field group for organization
form.addFieldGroup({
id: 'custpage_search_group',
label: 'Search Criteria'
});

// Add fields to the group
const customerField = form.addField({
id: 'custpage_customer',
type: serverWidget.FieldType.SELECT,
label: 'Customer',
source: 'customer', // Links to customer record
container: 'custpage_search_group'
});

const startDate = form.addField({
id: 'custpage_start_date',
type: serverWidget.FieldType.DATE,
label: 'Start Date',
container: 'custpage_search_group'
});

const endDate = form.addField({
id: 'custpage_end_date',
type: serverWidget.FieldType.DATE,
label: 'End Date',
container: 'custpage_search_group'
});

// Add submit button
form.addSubmitButton({
label: 'Search'
});

context.response.writePage(form);
};

Field Types Reference​

FieldTypeDescription
TEXTSingle-line text
TEXTAREAMulti-line text
DATEDate picker
DATETIMEDate and time picker
SELECTDropdown list
MULTISELECTMulti-select dropdown
CHECKBOXBoolean checkbox
INTEGERWhole numbers
FLOATDecimal numbers
CURRENCYCurrency field
INLINEHTMLRaw HTML content
PASSWORDPassword field
RADIORadio buttons

Field Configuration Flow​

┌─────────────────────────────────────────────────────────────────────────────┐
│ FIELD CONFIGURATION │
└─────────────────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────┐
│ form.addField({ │
│ id: 'custpage_customer', │──── Unique ID (must start with custpage_)
│ type: serverWidget.FieldType. │
│ SELECT, │──── Field type determines input control
│ label: 'Customer', │──── Display label
│ source: 'customer', │──── For SELECT: record type to list
│ container: 'group_id' │──── Optional: field group
│ }); │
└───────────────────┬─────────────────┘
│
▼
┌─────────────────────────────────────┐
│ Additional Configuration │
│ ───────────────────────────────── │
│ field.isMandatory = true; │──── Required field
│ field.defaultValue = 'value'; │──── Pre-populate
│ field.updateDisplayType({ │
│ displayType: serverWidget. │
│ FieldDisplayType.HIDDEN │──── Hidden, inline, disabled
│ }); │
└─────────────────────────────────────┘

Processing Form Data​

Reading Parameters​

const handlePost = (context) => {
const params = context.request.parameters;

// Read individual parameters
const customerId = params.custpage_customer;
const startDate = params.custpage_start_date;
const endDate = params.custpage_end_date;

log.debug('Parameters', { customerId, startDate, endDate });

// Process the data
const results = searchTransactions(customerId, startDate, endDate);

// Display results
displayResults(context, results);
};

Multi-Select Parameters​

// Multi-select values come as array or special delimiter
const parseMultiSelect = (param) => {
if (!param) return [];
return String(param)
.split('\u0005') // NetSuite multi-select delimiter
.filter(v => v.length > 0);
};

const handlePost = (context) => {
const params = context.request.parameters;
const selectedItems = parseMultiSelect(params.custpage_items);

selectedItems.forEach(itemId => {
log.debug('Selected Item', itemId);
});
};

Displaying Results​

Using Sublists​

const displayResults = (context, results) => {
const form = serverWidget.createForm({
title: 'Search Results'
});

// Add sublist for results
const sublist = form.addSublist({
id: 'custpage_results',
type: serverWidget.SublistType.LIST,
label: `Results (${results.length} found)`
});

// Add columns
sublist.addField({
id: 'custpage_tranid',
type: serverWidget.FieldType.TEXT,
label: 'Transaction #'
});

sublist.addField({
id: 'custpage_date',
type: serverWidget.FieldType.DATE,
label: 'Date'
});

sublist.addField({
id: 'custpage_amount',
type: serverWidget.FieldType.CURRENCY,
label: 'Amount'
});

// Populate rows
results.forEach((row, index) => {
sublist.setSublistValue({
id: 'custpage_tranid',
line: index,
value: row.tranid
});
sublist.setSublistValue({
id: 'custpage_date',
line: index,
value: row.date
});
sublist.setSublistValue({
id: 'custpage_amount',
line: index,
value: row.amount
});
});

context.response.writePage(form);
};

Using Inline HTML​

const displayResults = (context, results) => {
const form = serverWidget.createForm({
title: 'Search Results'
});

// Add HTML field
const htmlField = form.addField({
id: 'custpage_html',
type: serverWidget.FieldType.INLINEHTML,
label: ' '
});

// Build HTML table
let html = '<table border="1" style="border-collapse: collapse;">';
html += '<tr><th>Transaction</th><th>Date</th><th>Amount</th></tr>';

results.forEach(row => {
html += `<tr>
<td>${row.tranid}</td>
<td>${row.date}</td>
<td>${row.amount}</td>
</tr>`;
});

html += '</table>';

htmlField.defaultValue = html;

context.response.writePage(form);
};

Complete Example: Customer Transaction Report​

/**
* @NApiVersion 2.1
* @NScriptType Suitelet
* @NModuleScope SameAccount
*/
define(['N/ui/serverWidget', 'N/search', 'N/log'],
(serverWidget, search, log) => {

const onRequest = (context) => {
if (context.request.method === 'GET') {
showSearchForm(context);
} else {
showResults(context);
}
};

/**
* Display the search form
*/
const showSearchForm = (context) => {
const form = serverWidget.createForm({
title: 'Customer Transaction Report'
});

// Search criteria group
form.addFieldGroup({
id: 'grp_criteria',
label: 'Search Criteria'
});

// Customer dropdown
const custField = form.addField({
id: 'custpage_customer',
type: serverWidget.FieldType.SELECT,
label: 'Customer',
source: 'customer',
container: 'grp_criteria'
});
custField.isMandatory = true;

// Date range
form.addField({
id: 'custpage_from_date',
type: serverWidget.FieldType.DATE,
label: 'From Date',
container: 'grp_criteria'
});

form.addField({
id: 'custpage_to_date',
type: serverWidget.FieldType.DATE,
label: 'To Date',
container: 'grp_criteria'
});

form.addSubmitButton({ label: 'Search' });

context.response.writePage(form);
};

/**
* Display search results
*/
const showResults = (context) => {
const params = context.request.parameters;
const customerId = params.custpage_customer;
const fromDate = params.custpage_from_date;
const toDate = params.custpage_to_date;

// Create results form
const form = serverWidget.createForm({
title: 'Transaction Results'
});

// Add back button
form.addButton({
id: 'custpage_back',
label: 'Back to Search',
functionName: 'history.back()'
});

// Build search
const filters = [
['entity', 'is', customerId],
'AND',
['mainline', 'is', 'T']
];

if (fromDate) {
filters.push('AND', ['trandate', 'onorafter', fromDate]);
}
if (toDate) {
filters.push('AND', ['trandate', 'onorbefore', toDate]);
}

const transactionSearch = search.create({
type: search.Type.TRANSACTION,
filters: filters,
columns: [
search.createColumn({ name: 'tranid' }),
search.createColumn({ name: 'trandate', sort: search.Sort.DESC }),
search.createColumn({ name: 'type' }),
search.createColumn({ name: 'amount' }),
search.createColumn({ name: 'status' })
]
});

// Create sublist
const sublist = form.addSublist({
id: 'custpage_results',
type: serverWidget.SublistType.LIST,
label: 'Transactions'
});

sublist.addField({ id: 'col_tranid', type: serverWidget.FieldType.TEXT, label: 'Transaction #' });
sublist.addField({ id: 'col_date', type: serverWidget.FieldType.DATE, label: 'Date' });
sublist.addField({ id: 'col_type', type: serverWidget.FieldType.TEXT, label: 'Type' });
sublist.addField({ id: 'col_amount', type: serverWidget.FieldType.CURRENCY, label: 'Amount' });
sublist.addField({ id: 'col_status', type: serverWidget.FieldType.TEXT, label: 'Status' });

// Populate results
let line = 0;
let totalAmount = 0;

transactionSearch.run().each((result) => {
const amount = parseFloat(result.getValue('amount')) || 0;
totalAmount += amount;

sublist.setSublistValue({ id: 'col_tranid', line: line, value: result.getValue('tranid') || '' });
sublist.setSublistValue({ id: 'col_date', line: line, value: result.getValue('trandate') || '' });
sublist.setSublistValue({ id: 'col_type', line: line, value: result.getText('type') || '' });
sublist.setSublistValue({ id: 'col_amount', line: line, value: amount });
sublist.setSublistValue({ id: 'col_status', line: line, value: result.getText('status') || '' });

line++;
return line < 1000; // Limit to 1000 results
});

// Add summary
const summaryField = form.addField({
id: 'custpage_summary',
type: serverWidget.FieldType.INLINEHTML,
label: ' '
});
summaryField.defaultValue = `
<div style="margin: 20px 0; padding: 10px; background: #f5f5f5; border-radius: 4px;">
<strong>Total Transactions:</strong> ${line} |
<strong>Total Amount:</strong> $${totalAmount.toFixed(2)}
</div>
`;

context.response.writePage(form);
};

return { onRequest };
});

Script Deployment XML​

<?xml version="1.0" encoding="UTF-8"?>
<customscript scriptid="customscript_cust_trans_report">
<name>Customer Transaction Report</name>
<scripttype>SUITELET</scripttype>
<scriptfile>[/SuiteScripts/Suitelets/cust_trans_report_sl.js]</scriptfile>
<description>Customer transaction search and report</description>
<isinactive>F</isinactive>
<notifyowner>T</notifyowner>

<scriptdeployments>
<scriptdeployment scriptid="customdeploy_cust_trans_report">
<status>RELEASED</status>
<title>Customer Transaction Report</title>
<isdeployed>T</isdeployed>
<loglevel>DEBUG</loglevel>
<allroles>F</allroles>
<audslctrole>
<role>[Administrator]</role>
</audslctrole>
</scriptdeployment>
</scriptdeployments>
</customscript>

Getting Suitelet URL​

// In another script, get URL to this Suitelet
const url = require('N/url');

const suiteletUrl = url.resolveScript({
scriptId: 'customscript_cust_trans_report',
deploymentId: 'customdeploy_cust_trans_report',
returnExternalUrl: false
});

// Add parameters
const urlWithParams = `${suiteletUrl}&custpage_customer=123`;

Best Practices​

PracticeDescription
Prefix field IDsAlways use custpage_ prefix
Validate inputCheck required fields in POST handler
Handle errorsWrap in try/catch, show user-friendly messages
Limit resultsUse pagination or limits for large datasets
Log parametersLog incoming parameters for debugging
Use field groupsOrganize forms with field groups

Next Steps​