Suitelets are custom NetSuite pages that allow you to build interactive applications, dashboards, and forms. Unlike record-bound scripts, Suitelets give you complete control over the UI and logic.
What Are Suitelets?
Suitelets are server-side scripts that generate custom pages accessible via URL. They're perfect for:
- Custom data entry forms
- Reporting dashboards
- Integration endpoints
- Approval workflows
- Bulk operations interfaces
Basic Suitelet Structure
Every Suitelet needs an onRequest entry point that handles both GET and POST requests:
/**
* @NApiVersion 2.1
* @NScriptType Suitelet
*/
define(['N/ui/serverWidget'], (serverWidget) => {
const onRequest = (context) => {
if (context.request.method === 'GET') {
// Display the form
const form = serverWidget.createForm({
title: 'My Custom Form'
});
// Add fields, buttons, sublists...
context.response.writePage(form);
} else {
// Handle form submission (POST)
const request = context.request;
const name = request.parameters.custpage_name;
// Process the data...
}
};
return { onRequest };
});Building a Complete Form
Let's build a practical example: a customer feedback form that creates a support case.
/**
* @NApiVersion 2.1
* @NScriptType Suitelet
* @NModuleScope SameAccount
*
* Customer Feedback Form Suitelet
*/
define(['N/ui/serverWidget', 'N/record', 'N/redirect', 'N/log'],
(serverWidget, record, redirect, log) => {
/**
* Build the feedback form
*/
const buildForm = () => {
const form = serverWidget.createForm({
title: 'Customer Feedback Form',
hideNavBar: false
});
// Add field group for organization
form.addFieldGroup({
id: 'custpage_contact_info',
label: 'Contact Information'
});
// Customer selection (lookup field)
const customerField = form.addField({
id: 'custpage_customer',
type: serverWidget.FieldType.SELECT,
label: 'Customer',
source: 'customer',
container: 'custpage_contact_info'
});
customerField.isMandatory = true;
// Contact name
form.addField({
id: 'custpage_contact_name',
type: serverWidget.FieldType.TEXT,
label: 'Contact Name',
container: 'custpage_contact_info'
}).isMandatory = true;
// Email
const emailField = form.addField({
id: 'custpage_email',
type: serverWidget.FieldType.EMAIL,
label: 'Email Address',
container: 'custpage_contact_info'
});
emailField.isMandatory = true;
// Feedback section
form.addFieldGroup({
id: 'custpage_feedback_info',
label: 'Feedback Details'
});
// Category dropdown
const categoryField = form.addField({
id: 'custpage_category',
type: serverWidget.FieldType.SELECT,
label: 'Category',
container: 'custpage_feedback_info'
});
categoryField.addSelectOption({ value: '', text: '-- Select --' });
categoryField.addSelectOption({ value: 'product', text: 'Product Issue' });
categoryField.addSelectOption({ value: 'service', text: 'Service Feedback' });
categoryField.addSelectOption({ value: 'billing', text: 'Billing Question' });
categoryField.addSelectOption({ value: 'other', text: 'Other' });
categoryField.isMandatory = true;
// Priority
const priorityField = form.addField({
id: 'custpage_priority',
type: serverWidget.FieldType.SELECT,
label: 'Priority',
container: 'custpage_feedback_info'
});
priorityField.addSelectOption({ value: '3', text: 'Low' });
priorityField.addSelectOption({ value: '2', text: 'Medium' });
priorityField.addSelectOption({ value: '1', text: 'High' });
priorityField.defaultValue = '2';
// Description (long text)
const descField = form.addField({
id: 'custpage_description',
type: serverWidget.FieldType.TEXTAREA,
label: 'Description',
container: 'custpage_feedback_info'
});
descField.isMandatory = true;
descField.updateDisplaySize({
height: 10,
width: 60
});
// Add submit button
form.addSubmitButton({
label: 'Submit Feedback'
});
// Add reset button
form.addResetButton({
label: 'Clear Form'
});
return form;
};
/**
* Process form submission and create support case
*/
const processSubmission = (request) => {
const params = request.parameters;
// Create support case record
const supportCase = record.create({
type: record.Type.SUPPORT_CASE,
isDynamic: true
});
supportCase.setValue({
fieldId: 'company',
value: params.custpage_customer
});
supportCase.setValue({
fieldId: 'title',
value: `Feedback: ${params.custpage_category}`
});
supportCase.setValue({
fieldId: 'incomingmessage',
value: params.custpage_description
});
supportCase.setValue({
fieldId: 'priority',
value: params.custpage_priority
});
supportCase.setValue({
fieldId: 'email',
value: params.custpage_email
});
const caseId = supportCase.save();
log.audit('Feedback Submitted', {
caseId: caseId,
customer: params.custpage_customer,
category: params.custpage_category
});
return caseId;
};
/**
* Show success message
*/
const showConfirmation = (context, caseId) => {
const form = serverWidget.createForm({
title: 'Thank You!'
});
form.addField({
id: 'custpage_message',
type: serverWidget.FieldType.INLINEHTML,
label: ' '
}).defaultValue = `
<div style="padding: 20px; text-align: center;">
<h2 style="color: #4CAF50;">Feedback Submitted Successfully</h2>
Your support case **#${caseId}** has been created.
Our team will review your feedback and respond within 24 hours.
<a href="/app/common/custom/custrecordentry.nl?rectype=YOUR_SUITELET_URL"
style="padding: 10px 20px; background: #607D8B; color: white; text-decoration: none; border-radius: 4px;">
Submit Another
</a>
</div>
`;
context.response.writePage(form);
};
/**
* Main entry point
*/
const onRequest = (context) => {
if (context.request.method === 'GET') {
const form = buildForm();
context.response.writePage(form);
} else {
try {
const caseId = processSubmission(context.request);
showConfirmation(context, caseId);
} catch (e) {
log.error('Submission Error', e.message);
// Show error form
const errorForm = serverWidget.createForm({ title: 'Error' });
errorForm.addField({
id: 'custpage_error',
type: serverWidget.FieldType.INLINEHTML,
label: ' '
}).defaultValue = `<p style="color: red;">Error: ${e.message}
`;
context.response.writePage(errorForm);
}
}
};
return { onRequest };
});Adding Sublists
Sublists let you display and edit tabular data:
/**
* Add a sublist to display items
*/
const addItemSublist = (form, items) => {
const sublist = form.addSublist({
id: 'custpage_items',
type: serverWidget.SublistType.LIST,
label: 'Items'
});
// Define columns
sublist.addField({
id: 'custpage_item_name',
type: serverWidget.FieldType.TEXT,
label: 'Item Name'
});
sublist.addField({
id: 'custpage_quantity',
type: serverWidget.FieldType.INTEGER,
label: 'Quantity'
});
sublist.addField({
id: 'custpage_price',
type: serverWidget.FieldType.CURRENCY,
label: 'Price'
});
// Populate data
items.forEach((item, index) => {
sublist.setSublistValue({
id: 'custpage_item_name',
line: index,
value: item.name
});
sublist.setSublistValue({
id: 'custpage_quantity',
line: index,
value: item.quantity
});
sublist.setSublistValue({
id: 'custpage_price',
line: index,
value: item.price
});
});
return sublist;
};Editable Sublist Example
For data entry with multiple rows:
const addEditableSublist = (form) => {
const sublist = form.addSublist({
id: 'custpage_line_items',
type: serverWidget.SublistType.INLINEEDITOR,
label: 'Line Items'
});
// Item selection
sublist.addField({
id: 'custpage_item',
type: serverWidget.FieldType.SELECT,
label: 'Item',
source: 'item'
});
// Quantity (editable)
sublist.addField({
id: 'custpage_qty',
type: serverWidget.FieldType.INTEGER,
label: 'Quantity'
});
// Rate
sublist.addField({
id: 'custpage_rate',
type: serverWidget.FieldType.CURRENCY,
label: 'Rate'
});
return sublist;
};
// Reading sublist data on POST
const readSublistData = (request) => {
const lineCount = request.getLineCount({ group: 'custpage_line_items' });
const items = [];
for (let i = 0; i < lineCount; i++) {
items.push({
item: request.getSublistValue({
group: 'custpage_line_items',
name: 'custpage_item',
line: i
}),
quantity: request.getSublistValue({
group: 'custpage_line_items',
name: 'custpage_qty',
line: i
}),
rate: request.getSublistValue({
group: 'custpage_line_items',
name: 'custpage_rate',
line: i
})
});
}
return items;
};Adding Client Script for Interactivity
Attach a Client Script for real-time validation:
// In your Suitelet, reference the client script:
form.clientScriptModulePath = './MyClientScript.js';
// Or inline:
form.clientScriptFileId = 12345; // File Cabinet IDSample Client Script:
/**
* @NApiVersion 2.1
* @NScriptType ClientScript
*/
define(['N/currentRecord'], (currentRecord) => {
const pageInit = (context) => {
console.log('Form loaded');
};
const fieldChanged = (context) => {
if (context.fieldId === 'custpage_customer') {
// Auto-populate email from customer record
const rec = currentRecord.get();
// Fetch customer data...
}
};
const saveRecord = (context) => {
const rec = currentRecord.get();
const description = rec.getValue({ fieldId: 'custpage_description' });
if (description.length < 20) {
alert('Please provide a more detailed description (min 20 characters)');
return false;
}
return true;
};
return { pageInit, fieldChanged, saveRecord };
});Deployment and URL Access
After deploying your Suitelet:
- Create a Script record pointing to your file
- Create a Script Deployment
- Set "Available Without Login" if it's a public form
- Get the External URL from the deployment record
The URL format is:
https://ACCOUNT_ID.app.netsuite.com/app/site/hosting/scriptlet.nl?script=SCRIPT_ID&deploy=DEPLOY_ID
Best Practices
- Validate on both client and server - Never trust client-side validation alone
- Use field groups - Organize complex forms logically
- Handle errors gracefully - Show user-friendly messages
- Log important actions - Audit trail for troubleshooting
- Consider governance - Suitelets have 1,000 unit limits
Security Considerations
- Always validate user permissions
- Sanitize input to prevent injection attacks
- Use HTTPS for sensitive data
- Consider role-based access in deployment settings
Next Steps
Now that you can build Suitelets, explore:
Need custom Suitelet development for your business? Contact us to discuss your requirements.