π’ About These Case Studies
These are production-grade solutions showing how multiple script types work together. Each script is annotated with
π MODULE X: references linking concepts back to the teaching modules. Company-specific details have been replaced with placeholders.
π Contents
- Case Study 1: Custom Button Controller (Standalone)
- ββ Complete Annotated Code
- Case Study 2: Case Completion System (4 Scripts)
- ββ System Architecture
- ββ Suitelet: Email Sender
- ββ Suitelet: Customer Form (Excerpts)
- ββ Scheduled Script: Deadline Reminders
- ββ Custom Fields Reference
Case Study 1: Custom Button Controller
v1.2
π― Why Start Here?
This is a standalone User Event script - simple, self-contained, and demonstrates core patterns. It's the perfect first script to study before diving into multi-script systems.
π Business Problem
Users need quick access to common actions when viewing a Case record:
- Print a PDF report of the case
- Send an email to initiate customer completion workflow
- The email button should hide once the case is already complete
Solution Overview
| Script | Type | Entry Point | Modules Used |
|---|---|---|---|
ue_case_buttons.js |
User Event | beforeLoad | N/url, N/runtime, N/log |
π‘ Key Pattern: Conditional Button Visibility
This script checks a custom checkbox field (
custevent_service_complete) to decide whether to show the "Get Customer Completion Email" button. This is a common pattern for controlling UI based on record state.
Complete Annotated Code
π Modules 3, 4, 12/**
* @NApiVersion 2.1
* @NScriptType UserEventScript
* @NModuleScope SameAccount
* @version 1.2
*
* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
* π MODULES DEMONSTRATED
* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
* MODULE 4: User Event entry points (beforeLoad)
* MODULE 3: Reading record fields with getValue()
* MODULE 12: url.resolveScript() to generate Suitelet URLs
* MODULE 4: form.addButton() to add custom buttons
* MODULE 14: try/catch error handling
* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
*
* Adds buttons to Case record:
* 1. Print PDF - Always visible
* 2. Get Customer Completion Email - Only if service NOT complete
*/
define(['N/url', 'N/runtime', 'N/log'],
function(url, runtime, log) {
/**
* π MODULE 4: beforeLoad Entry Point
* ββββββββββββββββββββββββββββββββββββ
* Triggered BEFORE the page renders when user views the record.
* Perfect for: Adding buttons, hiding/showing fields, modifying form
*
* context.type values:
* - VIEW: User is viewing record
* - EDIT: User is editing record
* - CREATE: User is creating new record
* - COPY: User is copying record
* - PRINT: User is printing record
*/
function beforeLoad(context) {
try {
log.debug('beforeLoad Started', 'Type: ' + context.type);
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// π MODULE 4: Check execution context
// Only add buttons when VIEWING the record
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
if (context.type !== context.UserEventType.VIEW) {
log.debug('beforeLoad', 'Not VIEW mode, skipping buttons');
return;
}
// Get form and record objects from context
var form = context.form;
var newRecord = context.newRecord;
var caseId = newRecord.id;
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// π MODULE 3: getValue() to read custom checkbox field
// This determines if we show the email button
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
var serviceComplete = newRecord.getValue({
fieldId: 'custevent_service_complete'
});
log.debug('Service Complete Check',
'Case ID: ' + caseId + ', Complete: ' + serviceComplete);
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// π MODULE 12: url.resolveScript() generates Suitelet URL
// This creates a URL that calls another script
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
var printPdfURL = url.resolveScript({
scriptId: 'customscript_case_pdf_suitelet',
deploymentId: 'customdeploy_case_pdf_suitelet',
params: {
'caseid': caseId // Pass case ID as URL parameter
}
});
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// π MODULE 4: form.addButton() adds custom button
// functionName: Client-side JavaScript to execute
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
form.addButton({
id: 'custpage_print_pdf',
label: 'Print PDF',
functionName: 'window.open(\'' + printPdfURL + '\', \'_blank\')'
});
log.debug('Print PDF Button Added', 'URL: ' + printPdfURL);
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// π BUSINESS LOGIC: Conditional button visibility
// Only show email button if service is NOT complete
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
if (!serviceComplete || serviceComplete === false) {
var sendEmailURL = url.resolveScript({
scriptId: 'customscript_case_send_email',
deploymentId: 'customdeploy_case_send_email',
params: {
'caseid': caseId
}
});
form.addButton({
id: 'custpage_send_email',
label: 'Get Customer Completion Email',
functionName: 'window.open(\'' + sendEmailURL + '\', \'_blank\')'
});
log.debug('Email Button Added', 'URL: ' + sendEmailURL);
} else {
// π MODULE 14: Use log.audit for important business events
log.audit('Button Skipped',
'Case ' + caseId + ' - Service complete, email button hidden');
}
} catch (e) {
// π MODULE 14: Error handling with stack trace for debugging
log.error('Error in beforeLoad', e.toString() + ' | Stack: ' + e.stack);
}
}
// π MODULE 4: Return object maps function to entry point
return {
beforeLoad: beforeLoad
};
});
β
Key Takeaways from This Script
- Single Entry Point: Uses only
beforeLoad- no afterSubmit or beforeSubmit needed - Context Checking: Always check
context.typebefore adding UI elements - Conditional UI: Read field values to show/hide buttons based on record state
- Script Linking: Use
url.resolveScript()to call other scripts (Suitelets) - Client-Side Actions:
functionNameexecutes JavaScript in the browser - Error Handling: Wrap in try/catch so errors don't break the page load
π§ Deployment Settings
| Deploy To: | Case (supportcase) |
| Event Type: | View (beforeLoad only fires on View) |
| Status: | Released |
| Audience: | All roles that need to see buttons |
Case Study 2: Case Completion System
v1.65.0
π Business Problem
Field Service Engineers complete work at customer sites and need customers to sign off on the work. The company needs:
- Digital signature capture from both FSE and customer
- 72-hour deadline for customer signatures (auto-accept if missed)
- 42-hour reminder emails to customers
- Professional PDF generation for invoicing
- Automatic notifications to billing team
π Relationship to Case Study 1
The User Event script from Case Study 1 (
ue_case_buttons.js) adds buttons that trigger these Suitelets. While that script is standalone, these 4 scripts work together as a coordinated system.
System Architecture
Script Components (4 Scripts)
| Script | Type | Purpose | Modules Used |
|---|---|---|---|
sl_case_send_email.js |
Suitelet | Sends email with form link to FSE | N/email, N/record, N/url, N/log |
sl_case_customer_form.js |
Suitelet (Public) | Customer signature form & PDF generation | N/ui/serverWidget, N/record, N/render, N/file, N/email, N/format, N/url |
sl_case_pdf_inline.js |
Suitelet | Simple case report PDF | N/render, N/record, N/log |
ss_case_deadline_reminder.js |
Scheduled | 42/72-hour reminder processing | N/search, N/record, N/email, N/url, N/format |
Workflow Diagram
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β TRIGGER: Button click from Case record (via ue_case_buttons.js) β
βββββββββββββββββββββββββββββββββ¬ββββββββββββββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββ βββββββββββββββββββββββββββββββββββ
β sl_case_send_email ββββββΆβ Email sent to FSE with link β
β (Suitelet) β β to customer form β
βββββββββββββββββββββββββββββββββββ βββββββββββββββββ¬ββββββββββββββββββ
β
βββββββββββββββββββββββββββββββββ
βΌ
βββββββββββββββββββββββββββββββββββ βββββββββββββββββββββββββββββββββββ
β sl_case_customer_form ββββββΆβ FSE signs β Customer signs β
β (Public Suitelet) β β β PDF generated β Team notifiedβ
βββββββββββββββββββββββββββββββββββ βββββββββββββββββββββββββββββββββββ
β²
β (Reminder links)
βββββββββββββββββββββββββββββββββββ
β ss_case_deadline_reminder β βββββ Runs hourly
β (Scheduled Script) β β’ 42-hour: Send reminder
βββββββββββββββββββββββββββββββββββ β’ 72-hour: Auto-close case
Suitelet: Email Sender
π Modules 5, 12sl_case_send_email.js Suitelet
Sends an email to the assigned FSE containing a link to the customer form. Also creates a Message activity record for audit trail.
/**
* @NApiVersion 2.1
* @NScriptType Suitelet
* @NModuleScope SameAccount
*
* π MODULE 5: N/email for sending emails
* π MODULE 5: N/record for loading cases and creating messages
* π MODULE 12: Suitelet as a processing endpoint
*/
define(['N/email', 'N/record', 'N/log', 'N/url', 'N/runtime'],
function(email, record, log, url, runtime) {
function onRequest(context) {
try {
// π MODULE 12: Get URL parameters from request
var caseId = context.request.parameters.caseid;
if (!caseId) {
context.response.write('Error: No Case ID provided');
return;
}
log.debug('Sending email', 'Case ID: ' + caseId);
// π MODULE 5: record.load() to get case data (10 governance units)
var caseRecord = record.load({
type: record.Type.SUPPORT_CASE,
id: caseId
});
// π MODULE 3: getValue() vs getText()
// getValue = internal ID, getText = display name
var caseNumber = caseRecord.getValue({fieldId: 'casenumber'});
var caseTitle = caseRecord.getValue({fieldId: 'title'});
var customerName = caseRecord.getText({fieldId: 'company'});
// π MODULE 12: Generate external URL for customer form
// returnExternalUrl: true = full URL for non-NetSuite users
var customerFormURL = url.resolveScript({
scriptId: 'customscript_case_customer_form',
deploymentId: 'customdeploy_case_customer_form',
params: { 'caseid': caseId },
returnExternalUrl: true
});
// π BUSINESS LOGIC: Dynamic recipient from case assignment
var emailAuthor = [SERVICE_EMPLOYEE_ID];
var emailRecipient = caseRecord.getValue({fieldId: 'assigned'});
if (!emailRecipient) {
var errorMsg = 'Error: Case ' + caseNumber + ' has no assigned user.';
log.error('sl_case_send_email Error', errorMsg);
context.response.write(errorMsg);
return;
}
// Build HTML email body
var subject = 'ACTION REQUIRED: Get Customer Completion for Case #' + caseNumber;
var body = '<html><body style="font-family: Arial, sans-serif;">';
body += '<p>Please use this link to collect customer completion:</p>';
body += '<p><strong>Case: ' + caseNumber + ' - ' + escapeHTML(caseTitle) + '</strong></p>';
body += '<p><a href="' + customerFormURL + '">START CUSTOMER FORM</a></p>';
body += '</body></html>';
// π MODULE 5: email.send() - sends email (20 governance units)
email.send({
author: emailAuthor,
recipients: emailRecipient,
subject: subject,
body: body
});
log.audit('Email Sent', 'To: ' + emailRecipient + ', Case: ' + caseNumber);
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// π MODULE 5: Create Message record for audit trail
// This logs the email in the Case's Communication subtab
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
try {
var messageRecord = record.create({
type: record.Type.MESSAGE
});
messageRecord.setValue({fieldId: 'author', value: emailAuthor});
messageRecord.setValue({fieldId: 'recipient', value: emailRecipient});
messageRecord.setValue({fieldId: 'subject', value: subject});
messageRecord.setValue({fieldId: 'message', value: body});
messageRecord.setValue({fieldId: 'entity', value: caseRecord.getValue({fieldId: 'company'})});
messageRecord.setValue({fieldId: 'activity', value: caseId});
messageRecord.setValue({fieldId: 'emailed', value: true});
messageRecord.setValue({fieldId: 'messagetype', value: 'emailout'});
var messageId = messageRecord.save();
log.audit('Message activity created', 'ID: ' + messageId);
} catch (msgError) {
// π MODULE 14: Non-critical error - log but don't fail
log.error('Could not create message', msgError.toString());
}
// π MODULE 12: Write HTML response to browser
var successHTML = '<html><body><h1>Email Sent Successfully!</h1>';
successHTML += '<p>Case: ' + escapeHTML(caseNumber) + '</p>';
successHTML += '<script>setTimeout(function(){ window.close(); }, 3000);</script>';
successHTML += '</body></html>';
context.response.write(successHTML);
} catch (e) {
log.error('Error in sl_case_send_email', e.toString());
context.response.write('Error: ' + e.toString());
}
}
// π MODULE 15: Helper function for security
function escapeHTML(text) {
if (!text) return '';
return text.replace(/&/g, '&')
.replace(/'<')
.replace(/>/g, '>');
}
return { onRequest: onRequest };
});
Suitelet: Customer Form (Key Excerpts)
π Modules 3, 5, 12sl_case_customer_form.js Suitelet (Public)
1,300+ line Suitelet handling signature capture, deadline calculation, PDF generation, and email notifications. Key excerpts shown below.
β οΈ Public Suitelet Configuration
This script uses
@NModuleScope Public because customers access it via external URL without NetSuite login. The deployment must be set to "Available Without Login" for external access.
GET/POST Request Handling
/**
* π MODULE 12: Suitelet with GET/POST handling
*
* GET = Display the form
* POST = Process form submission
*/
function onRequest(context) {
if (context.request.method === 'GET') {
showForm(context);
} else {
processForm(context);
}
}
Deadline Checking Logic
/**
* π MODULE 5: Using N/format for date parsing
* Checks if the 72-hour deadline has passed
*/
var deadline = caseRecord.getValue({fieldId: 'custevent_signature_deadline'});
var deadlinePassed = false;
if (deadline) {
// π MODULE 5: format.parse() converts string to Date object
var deadlineDate = format.parse({
value: deadline,
type: format.Type.DATETIME
});
var now = new Date();
deadlinePassed = (now > deadlineDate);
}
// If locked, use submitFields to mark complete (more efficient than load/save)
if (deadlinePassed && !serviceCompleteFlag) {
// π MODULE 5: record.submitFields() - only 2 governance units!
record.submitFields({
type: record.Type.SUPPORT_CASE,
id: caseId,
values: {
'custevent_service_complete': true
},
options: {
enableSourcing: false,
ignoreMandatoryFields: true
}
});
}
72-Hour Deadline Calculation
/**
* When FSE signs, calculate the 72-hour deadline
* π MODULE 3: Working with Date objects
*/
var fseSignatureTimestamp = new Date();
// Calculate 72 hours from now (72 * 60 * 60 * 1000 milliseconds)
var deadlineTimestamp = new Date(fseSignatureTimestamp.getTime() + (72 * 60 * 60 * 1000));
// π MODULE 3: Save both timestamps to case record
caseRecord.setValue({fieldId: 'custevent_fse_signature_dt_tm', value: fseSignatureTimestamp});
caseRecord.setValue({fieldId: 'custevent_signature_deadline', value: deadlineTimestamp});
caseRecord.save();
PDF Generation with N/render
/**
* π MODULE 12: PDF Generation with N/render
* Uses FreeMarker template syntax in XML
*/
function generatePDF(caseId, signatureData) {
// Load case for PDF data
var caseRecord = record.load({
type: record.Type.SUPPORT_CASE,
id: caseId
});
// π MODULE 12: XML template with BFO PDF syntax
var xmlTemplate = '<?xml version="1.0"?>' +
'<!DOCTYPE pdf PUBLIC "-//big.faceless.org//report" "report-1.1.dtd">' +
'<pdf><head><style>...</style></head>' +
'<body>' +
'<h1>Case Completion Report</h1>' +
'<p>Case: ${record.casenumber}</p>' +
'...more template content...' +
'</body></pdf>';
// π MODULE 12: render.create() for PDF generation
var pdfRenderer = render.create();
pdfRenderer.templateContent = xmlTemplate;
// Add case record for template variables
pdfRenderer.addRecord({
templateName: 'record',
record: caseRecord
});
// π MODULE 12: renderAsPdf() creates file object
var pdfFile = pdfRenderer.renderAsPdf();
pdfFile.name = 'Case_Completion_' + caseNumber + '.pdf';
pdfFile.folder = [PDF_FOLDER_ID];
// π MODULE 5: file.save() returns file ID
var fileId = pdfFile.save();
// Attach to case record
record.attach({
record: { type: 'file', id: fileId },
to: { type: record.Type.SUPPORT_CASE, id: caseId }
});
return pdfFile;
}
Scheduled Script: Deadline Reminders
π Modules 7, 8ss_case_deadline_reminder.js Scheduled Script
Runs hourly to check for cases that have reached the 42-hour or 72-hour mark. Sends reminder emails and handles auto-closure.
/**
* @NApiVersion 2.1
* @NScriptType ScheduledScript
*
* π MODULE 7: N/search for finding cases by criteria
* π MODULE 8: Scheduled Script processing pattern
*/
define(['N/search', 'N/record', 'N/log', 'N/email', 'N/url', 'N/runtime', 'N/format'],
function(search, record, log, email, url, runtime, format) {
/**
* π MODULE 8: execute() is the entry point for Scheduled Scripts
*/
function execute(context) {
log.audit('START', 'Scheduled Reminder Script Started');
try {
process42HourReminders();
process72HourReminders();
log.audit('END', 'Script Finished');
} catch (err) {
log.error('CRITICAL ERROR', err.toString());
}
}
/**
* π MODULE 7: Search for cases eligible for 42-hour reminder
*/
function process42HourReminders() {
log.audit('42-HOUR', 'Starting 42-Hour Processing');
// π MODULE 7: search.create() with multiple filters
var search42Hr = search.create({
type: search.Type.SUPPORT_CASE,
filters: [
// FSE has signed (signature timestamp exists)
['custevent_fse_signature_dt_tm', 'isnotempty', ''],
'AND',
// Customer has NOT signed
['custevent_signature_base64', 'isempty', ''],
'AND',
// 42-hour reminder not yet sent
['custevent_42hr_reminder_sent', 'isempty', ''],
'AND',
// Customer email exists
['custevent_customer_reminder_email', 'isnotempty', '']
],
columns: [
'internalid',
'casenumber',
'company',
'custevent_customer_reminder_email',
'custevent_fse_signature_dt_tm',
'custevent_signature_deadline'
]
});
var count = 0;
// π MODULE 7: run().each() processes results one at a time
search42Hr.run().each(function(result) {
var caseId = result.getValue('internalid');
try {
// Get FSE signature timestamp
var fseSignatureDateString = result.getValue('custevent_fse_signature_dt_tm');
if (!fseSignatureDateString) {
return true; // Continue to next result
}
// π MODULE 5: format.parse() to convert string β Date
var fseSignatureDate = format.parse({
value: fseSignatureDateString,
type: format.Type.DATETIME
});
// Calculate 42 hours after FSE signature
var fortyTwoHoursAfter = new Date(
fseSignatureDate.getTime() + (42 * 60 * 60 * 1000)
);
var now = new Date();
// Only process if 42 hours have passed
if (now < fortyTwoHoursAfter) {
return true; // Not time yet, continue
}
// Get case details for email
var caseNumber = result.getValue('casenumber');
var companyName = result.getText('company') || 'Customer';
var customerEmail = result.getValue('custevent_customer_reminder_email');
// π MODULE 12: Generate form URL for reminder email
var formURL = url.resolveScript({
scriptId: 'customscript_case_customer_form',
deploymentId: 'customdeploy_case_customer_form',
params: { 'caseid': caseId },
returnExternalUrl: true
});
// Build and send reminder email
var emailSubject = 'Reminder: Signature Deadline Approaching - Case #' + caseNumber;
var emailBody = '<p>Dear ' + companyName + ',</p>';
emailBody += '<p>Your Case Completion Form deadline is approaching. You have 30 hours remaining.</p>';
emailBody += '<p><a href="' + formURL + '">CLICK HERE TO SIGN</a></p>';
// π MODULE 5: email.send() with relatedRecords for case linking
email.send({
author: [SERVICE_EMPLOYEE_ID],
recipients: customerEmail,
subject: emailSubject,
body: emailBody,
relatedRecords: {
activityId: caseId // Links email to case
}
});
// π MODULE 5: Mark reminder as sent (efficient update)
record.submitFields({
type: record.Type.SUPPORT_CASE,
id: caseId,
values: {
'custevent_42hr_reminder_sent': new Date()
}
});
log.audit('42-HOUR SENT', 'Case #' + caseNumber);
count++;
} catch (e) {
log.error('42-HOUR Case ' + caseId + ' FAILED', e.toString());
}
return true; // Continue to next result
});
log.audit('42-HOUR COMPLETE', 'Processed ' + count + ' reminder(s)');
}
// Similar pattern for process72HourReminders()...
// At 72 hours: send timeout notice, set service complete, notify billing team
return { execute: execute };
});
Custom Fields Reference
π Custom Fields on Case Record
These fields are required for the system to function. Create them before deploying scripts.
| Field ID | Type | Purpose |
|---|---|---|
custevent_service_complete |
Checkbox | Controls button visibility, set TRUE when complete |
custevent_fse_signature_dt_tm |
Date/Time | When FSE signed the form |
custevent_signature_deadline |
Date/Time | 72 hours after FSE signature |
custevent_signature_dt_tm |
Date/Time | When customer signed |
custevent_signature_base64 |
Long Text | Customer signature image data |
custevent_fse_signature_base64 |
Long Text | FSE signature image data |
custevent_customer_reminder_email |
Customer email for reminders | |
custevent_42hr_reminder_sent |
Date/Time | Timestamp when 42-hour reminder sent |
custevent_reminder_sent |
Date/Time | Timestamp when 72-hour notice sent |
custevent_to_be_invoiced |
Checkbox | If TRUE, billing team is notified on completion |
custevent_team_notified |
Date/Time | When billing team was notified |
π Key Takeaways from These Case Studies
- Case Study 1 (Standalone): A single User Event script can provide significant value by adding buttons and conditional UI
- Case Study 2 (Multi-Script): Suitelets + Scheduled Scripts work together for complex workflows
- Public vs Private: Customer-facing forms use
@NModuleScope Publicfor external access - Efficient Updates: Use
submitFields()(2 units) instead of load/save (15+ units) - Audit Trail: Create Message records to log emails in Case communication tab
- Date Handling: Use
N/formatfor parsing dates from record fields - Error Handling: Wrap each operation in try/catch so one failure doesn't stop the batch