Suitelet Development
Suitelets are server-side scripts that create custom NetSuite pages, forms, and reports accessible via URL.
When to Use Suitelets
| Use Case | Example |
|---|---|
| Custom forms | Data entry forms not tied to records |
| Reports | Custom reports with export functionality |
| Wizards | Multi-step processes |
| Dashboards | Custom data visualizations |
| Integration pages | External 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
| FieldType | Description |
|---|---|
TEXT | Single-line text |
TEXTAREA | Multi-line text |
DATE | Date picker |
DATETIME | Date and time picker |
SELECT | Dropdown list |
MULTISELECT | Multi-select dropdown |
CHECKBOX | Boolean checkbox |
INTEGER | Whole numbers |
FLOAT | Decimal numbers |
CURRENCY | Currency field |
INLINEHTML | Raw HTML content |
PASSWORD | Password field |
RADIO | Radio 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
| Practice | Description |
|---|---|
| Prefix field IDs | Always use custpage_ prefix |
| Validate input | Check required fields in POST handler |
| Handle errors | Wrap in try/catch, show user-friendly messages |
| Limit results | Use pagination or limits for large datasets |
| Log parameters | Log incoming parameters for debugging |
| Use field groups | Organize forms with field groups |
Next Steps
- User Event Scripts - Automate record operations
- Client Scripts - Add client-side validation
- Invoice Approval Scenario - Complete implementation