πŸ“ Appendix C: Project Case Studies

Real-world multi-script solutions with architecture patterns and annotated code

🏒 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.

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.type before 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: functionName executes 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, 12

sl_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, '&amp;')
                       .replace(/'&lt;')
                       .replace(/>/g, '&gt;');
        }

        return { onRequest: onRequest };
    });

Suitelet: Customer Form (Key Excerpts)

πŸ“š Modules 3, 5, 12

sl_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, 8

ss_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 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 Public for 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/format for parsing dates from record fields
  • Error Handling: Wrap each operation in try/catch so one failure doesn't stop the batch