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