Appendix A: Complete Code Examples & Exercises

Full working scripts with detailed annotations • Based on Oracle Student Guide

1. JavaScript Syntax Quick Reference

Creating Objects

// Anonymous object syntax - used everywhere in SuiteScript
var myObject = {
    property1: 'value1',
    property2: 123,
    property3: function() { return 'Hello'; }
};

The Define Statement

// Standard SuiteScript 2.x module pattern
define(['N/record', 'N/log'], function(record, log) {
    
    // Private functions (not exported)
    function helperFunction() {
        return 'I am internal';
    }
    
    // Entry point function
    function afterSubmit(context) {
        helperFunction();
        log.debug('Executed', 'Script ran!');
    }
    
    // Export entry points
    return {
        afterSubmit: afterSubmit
    };
});

Truthy/Falsey Checks

// These are all "falsey" in JavaScript:
// 0, "", null, undefined, NaN, false

// Quick empty check:
if (!someValue) {
    // someValue is empty, null, undefined, 0, or false
}

// Equivalent to:
if (someValue == 0 || someValue == "" || someValue == null || 
    someValue == undefined || someValue == NaN) {
}

Search Expressions (Shorthand)

// Filter expression syntax: [fieldId, operator, value]
var filters = [
    ['type', 'anyof', 'SalesOrd'],
    'AND',
    ['mainline', 'is', true],
    'AND',
    ['status', 'noneof', 'SalesOrd:C']  // Not Cancelled
];

// Column expression: just field IDs
var columns = ['tranid', 'entity', 'total', 'status'];

2. Hello World - Complete User Event Script

📚 See Module 2, Module 3
/**
 * @NApiVersion 2.1
 * @NScriptType UserEventScript
 * @NModuleScope SameAccount
 * 
 * Complete Hello World script demonstrating:
 * - Required annotations
 * - Module loading
 * - Context object usage
 * - All three User Event entry points
 * - Logging at different levels
 * 
 * Deploy to: Customer record
 */
define(['N/log', 'N/record'], function(log, record) {

    /**
     * beforeLoad - Triggered when record is loaded for view/edit
     * Use for: Adding buttons, hiding fields, modifying form
     * 
     * @param {Object} context
     * @param {Record} context.newRecord - Current record (read-only in View mode)
     * @param {string} context.type - UserEventType enum value
     * @param {Form} context.form - Form object for UI manipulation
     */
    function beforeLoad(context) {
        log.debug({
            title: 'beforeLoad Triggered',
            details: 'Type: ' + context.type + ' | Record ID: ' + context.newRecord.id
        });
        
        // Example: Add a custom button (only when editing)
        if (context.type === context.UserEventType.EDIT) {
            context.form.addButton({
                id: 'custpage_mybutton',
                label: 'My Custom Button',
                functionName: 'myClientFunction'  // Must exist in deployed Client Script
            });
        }
    }

    /**
     * beforeSubmit - Triggered before record is saved to database
     * Use for: Validation, setting calculated values, blocking saves
     * 
     * @param {Object} context
     * @param {Record} context.newRecord - Record being saved (writable)
     * @param {Record} context.oldRecord - Previous record state (on edit/delete)
     * @param {string} context.type - 'create', 'edit', 'delete', etc.
     */
    function beforeSubmit(context) {
        // Only run on create and edit
        if (context.type !== context.UserEventType.CREATE && 
            context.type !== context.UserEventType.EDIT) {
            return;
        }
        
        var rec = context.newRecord;
        var companyName = rec.getValue('companyname');
        
        // Validation example: require company name
        if (!companyName) {
            throw 'Company name is required!';  // Blocks save with error
        }
        
        // Set a calculated field
        rec.setValue({
            fieldId: 'custentity_last_modified',
            value: new Date()
        });
        
        log.audit('Customer Saved', 'Company: ' + companyName);
    }

    /**
     * afterSubmit - Triggered after record is saved
     * Use for: Creating related records, sending emails, external integrations
     * 
     * @param {Object} context
     * @param {Record} context.newRecord - Saved record (read-only)
     * @param {Record} context.oldRecord - Previous state (on edit)
     * @param {string} context.type - Trigger type
     */
    function afterSubmit(context) {
        var rec = context.newRecord;
        
        log.debug({
            title: 'afterSubmit Complete',
            details: JSON.stringify({
                id: rec.id,
                type: context.type,
                company: rec.getValue('companyname')
            })
        });
        
        // Example: Create a follow-up task for new customers
        if (context.type === context.UserEventType.CREATE) {
            var task = record.create({ type: record.Type.TASK });
            task.setValue('title', 'Follow up with new customer');
            task.setValue('company', rec.id);
            task.setValue('priority', 'HIGH');
            var taskId = task.save();
            
            log.audit('Task Created', 'Task ID: ' + taskId);
        }
    }

    return {
        beforeLoad: beforeLoad,
        beforeSubmit: beforeSubmit,
        afterSubmit: afterSubmit
    };
});

3. Client Script - Field Validation & Defaults

📚 See Module 4
/**
 * @NApiVersion 2.1
 * @NScriptType ClientScript
 * @NModuleScope SameAccount
 * 
 * Complete Client Script demonstrating all common entry points
 * 
 * Deploy to: Customer record
 * Custom fields required:
 *   - custentity_apply_coupon (Checkbox)
 *   - custentity_coupon_code (Free-form text)
 */
define(['N/currentRecord', 'N/log'], function(currentRecord, log) {

    /**
     * pageInit - Triggered when page loads
     * Use for: Setting defaults, enabling/disabling fields
     */
    function pageInit(context) {
        var rec = context.currentRecord;
        var mode = context.mode;  // 'create', 'edit', 'view', 'copy'
        
        log.debug('Page Initialized', 'Mode: ' + mode);
        
        // Disable coupon code field initially
        var couponField = rec.getField({ fieldId: 'custentity_coupon_code' });
        if (couponField) {
            couponField.isDisabled = true;
        }
        
        // Set default value on new records
        if (mode === 'create') {
            rec.setValue({
                fieldId: 'category',
                value: 1  // Default customer category
            });
        }
    }

    /**
     * fieldChanged - Triggered when any field value changes
     * Use for: Field dependencies, enabling/disabling other fields
     */
    function fieldChanged(context) {
        var rec = context.currentRecord;
        var fieldId = context.fieldId;
        
        // Only react to Apply Coupon checkbox
        if (fieldId === 'custentity_apply_coupon') {
            var applyCoupon = rec.getValue('custentity_apply_coupon');
            var couponField = rec.getField({ fieldId: 'custentity_coupon_code' });
            
            if (applyCoupon) {
                // Enable coupon code field
                couponField.isDisabled = false;
            } else {
                // Disable and clear coupon code
                couponField.isDisabled = true;
                rec.setValue({
                    fieldId: 'custentity_coupon_code',
                    value: ''
                });
            }
        }
    }

    /**
     * validateField - Triggered when leaving a field
     * Use for: Field-level validation
     * Return: true to accept, false to reject (keeps focus on field)
     */
    function validateField(context) {
        var rec = context.currentRecord;
        var fieldId = context.fieldId;
        
        if (fieldId === 'custentity_coupon_code') {
            var applyCoupon = rec.getValue('custentity_apply_coupon');
            var couponCode = rec.getValue('custentity_coupon_code');
            
            // Validate coupon code length if apply coupon is checked
            if (applyCoupon && couponCode && couponCode.length !== 5) {
                alert('Coupon code must be exactly 5 characters.');
                return false;  // Keep focus on field
            }
        }
        
        return true;  // Allow field change
    }

    /**
     * saveRecord - Triggered when user clicks Save
     * Use for: Form-level validation before submission
     * Return: true to save, false to cancel
     */
    function saveRecord(context) {
        var rec = context.currentRecord;
        
        var applyCoupon = rec.getValue('custentity_apply_coupon');
        var couponCode = rec.getValue('custentity_coupon_code');
        
        // Require coupon code if Apply Coupon is checked
        if (applyCoupon && (!couponCode || couponCode.length !== 5)) {
            alert('Please enter a valid 5-character coupon code.');
            return false;  // Cancel save
        }
        
        // Confirm save
        return confirm('Are you sure you want to save this customer?');
    }

    /**
     * lineInit - Triggered when a sublist line is selected/created
     * Use for: Setting line defaults
     */
    function lineInit(context) {
        var rec = context.currentRecord;
        var sublistId = context.sublistId;
        
        // Example: Default quantity on custom sublist
        if (sublistId === 'recmachcustrecord_product_pref_customer') {
            var qty = rec.getCurrentSublistValue({
                sublistId: sublistId,
                fieldId: 'custrecord_prod_pref_qty'
            });
            
            // Default to 1 if empty
            if (!qty || isNaN(parseInt(qty))) {
                rec.setCurrentSublistValue({
                    sublistId: sublistId,
                    fieldId: 'custrecord_prod_pref_qty',
                    value: 1
                });
            }
        }
    }

    /**
     * validateLine - Triggered when committing a sublist line
     * Return: true to accept line, false to reject
     */
    function validateLine(context) {
        var rec = context.currentRecord;
        var sublistId = context.sublistId;
        
        if (sublistId === 'recmachcustrecord_product_pref_customer') {
            var qty = parseInt(rec.getCurrentSublistValue({
                sublistId: sublistId,
                fieldId: 'custrecord_prod_pref_qty'
            }));
            
            if (qty > 10) {
                alert('Preferred quantity cannot exceed 10.');
                return false;
            }
        }
        
        return true;
    }

    return {
        pageInit: pageInit,
        fieldChanged: fieldChanged,
        validateField: validateField,
        saveRecord: saveRecord,
        lineInit: lineInit,
        validateLine: validateLine
    };
});

4. Working with Sublists

📚 See Module 6
/**
 * @NApiVersion 2.1
 * @NScriptType UserEventScript
 * 
 * Demonstrates sublist manipulation on Sales Orders
 */
define(['N/log', 'N/record'], function(log, record) {

    function afterSubmit(context) {
        if (context.type !== context.UserEventType.CREATE) return;
        
        var salesOrder = context.newRecord;
        
        // ==== READING SUBLISTS ====
        
        // Get line count
        var lineCount = salesOrder.getLineCount({ sublistId: 'item' });
        log.debug('Item Lines', 'Total: ' + lineCount);
        
        // Loop through all lines
        var orderTotal = 0;
        for (var i = 0; i < lineCount; i++) {
            // Get values from specific line
            var itemId = salesOrder.getSublistValue({
                sublistId: 'item',
                fieldId: 'item',
                line: i
            });
            
            var itemName = salesOrder.getSublistText({
                sublistId: 'item',
                fieldId: 'item',
                line: i
            });
            
            var quantity = salesOrder.getSublistValue({
                sublistId: 'item',
                fieldId: 'quantity',
                line: i
            });
            
            var amount = salesOrder.getSublistValue({
                sublistId: 'item',
                fieldId: 'amount',
                line: i
            });
            
            orderTotal += parseFloat(amount) || 0;
            
            log.debug('Line ' + (i + 1), 
                itemName + ' | Qty: ' + quantity + ' | Amount: ' + amount);
        }
        
        log.audit('Order Total Calculated', orderTotal);
    }

    /**
     * Adding lines to a sublist (dynamic mode required)
     */
    function addItemToOrder(orderId, itemId, quantity) {
        // Load in dynamic mode for sublist manipulation
        var order = record.load({
            type: record.Type.SALES_ORDER,
            id: orderId,
            isDynamic: true
        });
        
        // Select a new line
        order.selectNewLine({ sublistId: 'item' });
        
        // Set values on current line
        order.setCurrentSublistValue({
            sublistId: 'item',
            fieldId: 'item',
            value: itemId
        });
        
        order.setCurrentSublistValue({
            sublistId: 'item',
            fieldId: 'quantity',
            value: quantity
        });
        
        // Commit the line
        order.commitLine({ sublistId: 'item' });
        
        // Save changes
        order.save();
    }

    return {
        afterSubmit: afterSubmit
    };
});
📚 See Module 7
/**
 * @NApiVersion 2.1
 * @NScriptType ScheduledScript
 * 
 * Comprehensive search examples
 */
define(['N/search', 'N/log', 'N/record'], function(search, log, record) {

    function execute(context) {
        
        // ==== LOAD SAVED SEARCH ====
        var savedSearch = search.load({ id: 'customsearch_my_customers' });
        
        // ==== CREATE SEARCH FROM SCRATCH ====
        var customerSearch = search.create({
            type: search.Type.CUSTOMER,
            filters: [
                ['isinactive', 'is', 'F'],
                'AND',
                ['salesrep', 'noneof', '@NONE@'],  // Has a sales rep
                'AND',
                ['datecreated', 'within', 'lastmonth']
            ],
            columns: [
                search.createColumn({ name: 'entityid' }),
                search.createColumn({ name: 'companyname' }),
                search.createColumn({ name: 'email' }),
                search.createColumn({ name: 'salesrep' }),
                // Join to related record
                search.createColumn({
                    name: 'phone',
                    join: 'salesrep'  // Get sales rep's phone
                }),
                // Summary column
                search.createColumn({
                    name: 'internalid',
                    summary: search.Summary.COUNT
                })
            ]
        });
        
        // ==== RUN WITH EACH (up to 4000 results) ====
        var resultCount = 0;
        customerSearch.run().each(function(result) {
            var entityId = result.getValue('entityid');
            var company = result.getValue('companyname');
            var salesRepId = result.getValue('salesrep');
            var salesRepName = result.getText('salesrep');  // Display value
            
            log.debug('Customer', company + ' | Rep: ' + salesRepName);
            
            resultCount++;
            return true;  // Continue to next result (false stops iteration)
        });
        
        log.audit('Search Complete', 'Found ' + resultCount + ' customers');
        
        // ==== GETRANGE (for pagination) ====
        var results = customerSearch.run().getRange({
            start: 0,
            end: 100
        });
        log.debug('First 100', results.length + ' results');
        
        // ==== PAGED EXECUTION (for 4000+ results) ====
        var pagedData = customerSearch.runPaged({ pageSize: 1000 });
        log.debug('Paged Search', 'Total results: ' + pagedData.count);
        
        pagedData.pageRanges.forEach(function(pageRange) {
            var page = pagedData.fetch({ index: pageRange.index });
            page.data.forEach(function(result) {
                // Process each result
            });
        });
        
        // ==== LOOKUP FIELDS (single record, 1 unit) ====
        var customerData = search.lookupFields({
            type: search.Type.CUSTOMER,
            id: '123',
            columns: ['companyname', 'email', 'salesrep']
        });
        log.debug('Lookup Result', JSON.stringify(customerData));
        // Returns: { companyname: 'ABC Corp', email: 'info@abc.com', salesrep: [{value: '5', text: 'John Smith'}] }
    }

    return { execute: execute };
});

6. Scheduled Script with Governance

📚 See Module 8, Module 14
/**
 * @NApiVersion 2.1
 * @NScriptType ScheduledScript
 * 
 * Scheduled script with governance monitoring and rescheduling
 */
define(['N/search', 'N/record', 'N/runtime', 'N/task', 'N/log'], 
    function(search, record, runtime, task, log) {

    // Governance threshold - reschedule before running out
    var GOVERNANCE_THRESHOLD = 500;  // Out of 10,000 units

    function execute(context) {
        var script = runtime.getCurrentScript();
        
        // Log remaining governance at start
        log.audit('Script Started', 'Remaining units: ' + script.getRemainingUsage());
        
        // Search for records to process
        var mySearch = search.create({
            type: 'customrecord_my_queue',
            filters: [['custrecord_processed', 'is', 'F']],
            columns: ['internalid', 'name']
        });
        
        var processedCount = 0;
        
        mySearch.run().each(function(result) {
            // Check governance BEFORE processing
            var remaining = script.getRemainingUsage();
            
            if (remaining < GOVERNANCE_THRESHOLD) {
                log.audit('Governance Low', 'Rescheduling. Processed: ' + processedCount);
                rescheduleScript();
                return false;  // Stop iteration
            }
            
            // Process the record
            try {
                processRecord(result.id);
                processedCount++;
                
                // Log progress every 100 records
                if (processedCount % 100 === 0) {
                    log.audit('Progress', processedCount + ' records. Remaining: ' + remaining);
                }
            } catch (e) {
                log.error('Processing Error', 'Record ' + result.id + ': ' + e.message);
            }
            
            return true;  // Continue to next
        });
        
        log.audit('Script Complete', 'Processed ' + processedCount + ' records');
    }

    function processRecord(recordId) {
        // Load record (10 units)
        var rec = record.load({
            type: 'customrecord_my_queue',
            id: recordId
        });
        
        // Do processing...
        rec.setValue('custrecord_processed', true);
        rec.setValue('custrecord_processed_date', new Date());
        
        // Save record (20 units)
        rec.save();
    }

    function rescheduleScript() {
        var scriptTask = task.create({
            taskType: task.TaskType.SCHEDULED_SCRIPT,
            scriptId: runtime.getCurrentScript().id,
            deploymentId: runtime.getCurrentScript().deploymentId
        });
        
        var taskId = scriptTask.submit();
        log.audit('Rescheduled', 'New task ID: ' + taskId);
    }

    return { execute: execute };
});

7. Map/Reduce - Payment Processing

📚 See Module 9
/**
 * @NApiVersion 2.1
 * @NScriptType MapReduceScript
 * 
 * Calculate total payments per customer
 * Demonstrates all four M/R stages
 */
define(['N/search', 'N/log', 'N/runtime'], function(search, log, runtime) {

    /**
     * STAGE 1: getInputData
     * Returns data to be processed (search, array, or object)
     * Automatic governance handling
     */
    function getInputData() {
        log.audit('M/R Started', 'getInputData executing');
        
        // Return a search - each result becomes a map() input
        return search.create({
            type: 'transaction',
            filters: [
                ['type', 'anyof', 'CustPymt'],
                'AND',
                ['mainline', 'is', true]
            ],
            columns: ['entity', 'status', 'amount']
        });
        
        // Alternative: Return saved search by ID
        // return search.load({ id: 'customsearch_payments' });
        
        // Alternative: Return array
        // return [{ id: 1, name: 'A' }, { id: 2, name: 'B' }];
    }

    /**
     * STAGE 2: map
     * Process each input individually
     * Write key-value pairs for reduce stage
     */
    function map(context) {
        // context.value is JSON string of search result
        var searchResult = JSON.parse(context.value);
        
        // Extract values from search result structure
        var customerId = searchResult.values.entity.value;
        var customerName = searchResult.values.entity.text;
        var status = searchResult.values.status.value;
        var amount = parseFloat(searchResult.values.amount) || 0;
        
        // Write key-value pair
        // All values with same key go to same reduce()
        context.write({
            key: customerId,
            value: JSON.stringify({
                name: customerName,
                status: status,
                amount: amount
            })
        });
    }

    /**
     * STAGE 3: reduce
     * Process all values for each unique key
     */
    function reduce(context) {
        var customerId = context.key;
        var values = context.values;  // Array of all values for this key
        
        var depositedTotal = 0;
        var undepositedTotal = 0;
        var customerName = '';
        
        // Process all payments for this customer
        values.forEach(function(valueStr) {
            var payment = JSON.parse(valueStr);
            customerName = payment.name;
            
            if (payment.status === 'deposited') {
                depositedTotal += payment.amount;
            } else {
                undepositedTotal += payment.amount;
            }
        });
        
        // Log results for this customer
        log.audit('Customer Totals', customerName + 
            ' | Deposited: $' + depositedTotal.toFixed(2) +
            ' | Undeposited: $' + undepositedTotal.toFixed(2));
        
        // Optionally write to summarize stage
        context.write({
            key: customerId,
            value: depositedTotal + undepositedTotal
        });
    }

    /**
     * STAGE 4: summarize
     * Final processing, error handling, reporting
     */
    function summarize(summary) {
        // Log overall statistics
        log.audit('Summary', JSON.stringify({
            usage: summary.usage,
            concurrency: summary.concurrency,
            yields: summary.yields
        }));
        
        // Check for errors
        if (summary.inputSummary.error) {
            log.error('Input Error', summary.inputSummary.error);
        }
        
        summary.mapSummary.errors.iterator().each(function(key, error) {
            log.error('Map Error', 'Key: ' + key + ' Error: ' + error);
            return true;
        });
        
        summary.reduceSummary.errors.iterator().each(function(key, error) {
            log.error('Reduce Error', 'Key: ' + key + ' Error: ' + error);
            return true;
        });
        
        // Process final output
        var grandTotal = 0;
        summary.output.iterator().each(function(key, value) {
            grandTotal += parseFloat(value) || 0;
            return true;
        });
        
        log.audit('Grand Total', '$' + grandTotal.toFixed(2));
    }

    return {
        getInputData: getInputData,
        map: map,
        reduce: reduce,
        summarize: summarize
    };
});

8. Suitelet - Custom Form

📚 See Module 12
/**
 * @NApiVersion 2.1
 * @NScriptType Suitelet
 * 
 * Custom form with processing
 */
define(['N/ui/serverWidget', 'N/search', 'N/log', 'N/redirect'], 
    function(serverWidget, search, log, redirect) {

    function onRequest(context) {
        if (context.request.method === 'GET') {
            // Display the form
            showForm(context);
        } else {
            // Process form submission
            processForm(context);
        }
    }

    function showForm(context) {
        var form = serverWidget.createForm({
            title: 'Customer Lookup'
        });
        
        // Add fields (custpage_ prefix required!)
        form.addField({
            id: 'custpage_customer',
            type: serverWidget.FieldType.SELECT,
            label: 'Select Customer',
            source: 'customer'  // Populates from customer list
        });
        
        form.addField({
            id: 'custpage_start_date',
            type: serverWidget.FieldType.DATE,
            label: 'Start Date'
        });
        
        form.addField({
            id: 'custpage_end_date',
            type: serverWidget.FieldType.DATE,
            label: 'End Date'
        });
        
        // Add field group
        var filterGroup = form.addFieldGroup({
            id: 'custpage_filters',
            label: 'Filters'
        });
        
        // Checkbox
        var activeOnly = form.addField({
            id: 'custpage_active_only',
            type: serverWidget.FieldType.CHECKBOX,
            label: 'Active Transactions Only',
            container: 'custpage_filters'
        });
        activeOnly.defaultValue = 'T';
        
        // Add submit button
        form.addSubmitButton({ label: 'Search' });
        
        // Add sublist for results
        var sublist = form.addSublist({
            id: 'custpage_results',
            type: serverWidget.SublistType.LIST,
            label: 'Search Results'
        });
        
        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'
        });
        
        context.response.writePage(form);
    }

    function processForm(context) {
        var request = context.request;
        
        // Get submitted values
        var customerId = request.parameters.custpage_customer;
        var startDate = request.parameters.custpage_start_date;
        var endDate = request.parameters.custpage_end_date;
        
        log.debug('Form Submitted', JSON.stringify({
            customer: customerId,
            startDate: startDate,
            endDate: endDate
        }));
        
        // Redirect to customer record
        redirect.toRecord({
            type: 'customer',
            id: customerId
        });
        
        // Or redirect back to suitelet with results
        // redirect.toSuitelet({
        //     scriptId: 'customscript_my_suitelet',
        //     deploymentId: 'customdeploy_my_suitelet',
        //     parameters: { custid: customerId }
        // });
    }

    return { onRequest: onRequest };
});

9. RESTlet - CRUD Operations

📚 See Module 13
/**
 * @NApiVersion 2.1
 * @NScriptType Restlet
 * 
 * RESTful API for Customer records
 */
define(['N/record', 'N/search', 'N/log'], function(record, search, log) {

    /**
     * GET - Retrieve customer data
     * URL params come as requestParams object
     */
    function get(requestParams) {
        var customerId = requestParams.id;
        
        if (!customerId) {
            return { error: true, message: 'Customer ID required' };
        }
        
        try {
            var customer = record.load({
                type: record.Type.CUSTOMER,
                id: customerId
            });
            
            return {
                success: true,
                data: {
                    id: customer.id,
                    entityId: customer.getValue('entityid'),
                    companyName: customer.getValue('companyname'),
                    email: customer.getValue('email'),
                    phone: customer.getValue('phone'),
                    balance: customer.getValue('balance')
                }
            };
        } catch (e) {
            log.error('GET Error', e.message);
            return { error: true, message: e.message };
        }
    }

    /**
     * POST - Create new customer
     * Request body comes as requestBody object
     */
    function post(requestBody) {
        try {
            var customer = record.create({
                type: record.Type.CUSTOMER
            });
            
            customer.setValue('companyname', requestBody.companyName);
            customer.setValue('email', requestBody.email);
            
            if (requestBody.phone) {
                customer.setValue('phone', requestBody.phone);
            }
            
            var customerId = customer.save();
            
            return {
                success: true,
                id: customerId,
                message: 'Customer created'
            };
        } catch (e) {
            log.error('POST Error', e.message);
            return { error: true, message: e.message };
        }
    }

    /**
     * PUT - Update existing customer
     */
    function put(requestBody) {
        if (!requestBody.id) {
            return { error: true, message: 'Customer ID required' };
        }
        
        try {
            var customer = record.load({
                type: record.Type.CUSTOMER,
                id: requestBody.id
            });
            
            // Update only provided fields
            if (requestBody.companyName) {
                customer.setValue('companyname', requestBody.companyName);
            }
            if (requestBody.email) {
                customer.setValue('email', requestBody.email);
            }
            if (requestBody.phone) {
                customer.setValue('phone', requestBody.phone);
            }
            
            customer.save();
            
            return { success: true, message: 'Customer updated' };
        } catch (e) {
            log.error('PUT Error', e.message);
            return { error: true, message: e.message };
        }
    }

    /**
     * DELETE - Remove customer
     */
    function doDelete(requestParams) {
        if (!requestParams.id) {
            return { error: true, message: 'Customer ID required' };
        }
        
        try {
            record.delete({
                type: record.Type.CUSTOMER,
                id: requestParams.id
            });
            
            return { success: true, message: 'Customer deleted' };
        } catch (e) {
            log.error('DELETE Error', e.message);
            return { error: true, message: e.message };
        }
    }

    return {
        get: get,
        post: post,
        put: put,
        'delete': doDelete  // 'delete' is reserved word
    };
});

10. Workflow Action Script

📚 See Module 11
/**
 * @NApiVersion 2.1
 * @NScriptType WorkflowActionScript
 * 
 * Custom workflow action to update related records
 * Attach directly to workflow (no deployment needed)
 */
define(['N/record', 'N/runtime', 'N/log'], function(record, runtime, log) {

    function onAction(context) {
        // Get the record triggering the workflow
        var salesOrder = context.newRecord;
        var orderId = salesOrder.id;
        
        // Get script parameter from workflow
        var script = runtime.getCurrentScript();
        var orderDate = script.getParameter({ name: 'custscript_urk_order_date' });
        
        // Get values from the sales order
        var customerId = salesOrder.getValue('entity');
        var lineCount = salesOrder.getLineCount({ sublistId: 'item' });
        
        // Build notes for customer record
        var notes = 'Last Order Date: ' + orderDate + '\\n';
        notes += 'Unique items ordered: ' + lineCount;
        
        try {
            // Load and update customer
            var customer = record.load({
                type: record.Type.CUSTOMER,
                id: customerId
            });
            
            customer.setValue({
                fieldId: 'comments',
                value: notes
            });
            
            customer.save();
            
            log.audit('Workflow Action', 'Updated customer ' + customerId);
            
            // Return value for workflow branching
            return 'SUCCESS';
            
        } catch (e) {
            log.error('Workflow Action Error', e.message);
            return 'ERROR';
        }
    }

    return { onAction: onAction };
});

11. Governance Reference

📚 See Module 14

Script Type Limits

Script TypeGovernance Limit
Client Script1,000 units
User Event1,000 units
Suitelet1,000 units
RESTlet5,000 units
Scheduled Script10,000 units
Map/ReduceAutomatic (per stage)

Common Operation Costs

OperationEntity/CRMTransactionCustom
record.load()5 units10 units2 units
record.save()10 units20 units4 units
record.delete()20 units20 units4 units
record.submitFields()2 units (recommended!)
search.create().run()5 units
search.lookupFields()1 unit (recommended!)
ResultSet.each()10 units per 1000 results
email.send()20 units
http.get/post()10 units
✅ Optimization Tips
  • Use record.submitFields() (2 units) instead of load+save (15+ units)
  • Use search.lookupFields() (1 unit) for single record reads
  • Offload heavy processing to Scheduled or Map/Reduce scripts
  • Check getRemainingUsage() before expensive operations
  • Use script parameters instead of hardcoded values

12. Real-World Example: Invoice Accrual Journal Entry

🏢 Production Script Overview This is a real production Workflow Action Script that creates accrual Journal Entries when Invoices are created. It demonstrates 10 of 16 modules in action, including dynamic record creation, sublist iteration, searches, script parameters, and error handling with email notifications.

Modules Demonstrated

ModuleConcepts Used
0-2JSDoc annotations, define(), module loading pattern
3getValue(), getSublistValue(), getSublistText(), setValue()
4Entry points (onAction for Workflow Action)
5N/record, N/search, N/email, N/runtime modules
6Sublists: getLineCount(), selectNewLine(), setCurrentSublistValue(), commitLine()
7search.create() with filters, run().each()
10Script parameters via runtime.getCurrentScript().getParameter()
11Workflow Action Script type
14try/catch/finally, log.error(), email error notifications
15Modular helper functions, comprehensive logging

Complete Annotated Script

/**
 * @NApiVersion 2.x
 * @NScriptType workflowactionscript
 * ═══════════════════════════════════════════════════════════════════════════
 * 📚 MODULE 2: Required JSDoc Annotations
 *    - @NApiVersion 2.x tells NetSuite which API version
 *    - @NScriptType workflowactionscript defines the script type
 * ═══════════════════════════════════════════════════════════════════════════
 *
 * @Author:       Bruno
 * @Created:      2020-08-19
 * @ScriptName:   WA_Make_Accruals_JE_on_Inv_Create
 * @Filename:     WA_Make_Accruals_JE_on_Inv_Create.js
 * @ScriptID:     customscript_wa_make_je_on_inv_create
 * 
 * @SandBox:
 *  @FileID:        [SANDBOX_FILE_ID]
 *  @InternalURL:   https://[ACCOUNT_ID]-sb1.app.netsuite.com/app/common/scripting/script.nl?id=[SCRIPT_ID]
 *  @DeploymentURL: https://[ACCOUNT_ID]-sb1.app.netsuite.com/app/common/scripting/scriptrecord.nl?id=[DEPLOY_ID]
 * 
 * @Production:
 *  @FileID:        [PROD_FILE_ID]
 *  @InternalURL:   https://[ACCOUNT_ID].app.netsuite.com/app/common/scripting/script.nl?id=[SCRIPT_ID]
 *  @DeploymentURL: https://[ACCOUNT_ID].app.netsuite.com/app/common/scripting/scriptrecord.nl?id=[DEPLOY_ID]
 *
 * @AdditionalInfo:
 *    Accrual account: [ACCRUAL_ACCOUNT_ID] "Accrued Revenue (Invoice Pending)"
 *
 * @modifications
 *  Version     Author      Date          Remarks
 *  v1.0.0      Bruno       2020-08-19    Initial version
 *  v1.1.0      Bruno       2020-08-27    Recreate all line fields from invoice on JE
 *  v1.2.0      Bruno       2020-09-02    Set JE trandate = inv trandate
 */

// ═══════════════════════════════════════════════════════════════════════════
// 📚 MODULE 2 & 5: The define() Statement
//    - First parameter: Array of module dependencies
//    - Second parameter: Callback function receiving module objects
//    - N/record: Create, load, save records
//    - N/search: Query NetSuite data
//    - N/email: Send email notifications
//    - N/runtime: Access script parameters and execution context
// ═══════════════════════════════════════════════════════════════════════════
define(['N/record', 'N/search', 'N/email', 'N/runtime'], 
function(record, search, email, runtime) {

    // 📚 MODULE 10: Get script object ONCE at the top for efficiency
    // This gives us access to script parameters defined in the deployment
    var scriptObj = runtime.getCurrentScript();

    /**
     * ═══════════════════════════════════════════════════════════════════════
     * 📚 MODULE 11: Workflow Action Script Entry Point
     * 
     * onAction(context) is triggered when:
     *   - The workflow reaches this custom action
     *   - Can be triggered Before Record Submit or After Record Submit
     * 
     * context.newRecord - The record that triggered the workflow
     * Return value - Can be used for workflow branching (1, 'SUCCESS', etc.)
     * ═══════════════════════════════════════════════════════════════════════
     */
    function onAction(context) {
        // 📚 MODULE 14: Wrap everything in try/catch for error handling
        try {
            // 📚 MODULE 11: Get the triggering record from workflow context
            var newRec = context.newRecord;
            
            // 📚 MODULE 15: Debug logging - log the record ID and structure
            log.debug('newRec', newRec.id + ' >> ' + JSON.stringify(newRec));

            // 📚 MODULE 3: getValue() to read body field
            // Check if this invoice already has an accrual JE linked
            var existingJE;
            if (newRec.getValue({fieldId: 'custbody_accruals_journal_entry'})) {
                // If field has value, verify JE still exists via search
                existingJE = checkForExistingJE(newRec.id);
            } else {
                existingJE = newRec.getValue({fieldId: 'custbody_accruals_journal_entry'});
            }
            
            log.debug('existingJE', JSON.stringify(existingJE));
            
            // Only create new JE if one doesn't exist
            var jeID;
            if (existingJE == '') {
                jeID = createNewJE(newRec);
            }
            
            log.debug('jeID', jeID);
            
            // 📚 MODULE 5: record.submitFields() - Update without loading record
            // This is MORE EFFICIENT than load() + setValue() + save()
            // Only costs 2 governance units vs 15+ for load/save!
            if (jeID) {
                record.submitFields({
                    type: record.Type.INVOICE,
                    id: newRec.id,
                    values: {
                        custbody_accruals_journal_entry: jeID
                    },
                    options: {
                        enableSourcing: false,
                        ignoreMandatoryFields: true
                    }
                });
            }

        } catch (err01) {
            // 📚 MODULE 14: Log errors with context
            log.debug('onAction.err01', JSON.stringify(err01));
        } finally {
            // 📚 MODULE 11: Return value for workflow branching
            // 1 = success, workflow can use this to determine next state
            return 1;
        }
    }

    /***********************************************/
    /*********** BEGIN UTILITY FUNCTIONS ***********/
    /***********************************************/

    /**
     * ═══════════════════════════════════════════════════════════════════════
     * 📚 MODULE 7: Search for Existing Journal Entry
     * 
     * Uses search.create() to find any JE already linked to this invoice.
     * Demonstrates:
     *   - Filter expressions with AND operators
     *   - Multiple filter conditions
     *   - run().each() for processing results
     *   - Returning early with 'return false'
     * ═══════════════════════════════════════════════════════════════════════
     */
    function checkForExistingJE(invID) {
        var jeID = '';

        try {
            // 📚 MODULE 7: search.create() with filter expression syntax
            // Format: [fieldId, operator, value]
            // Use "AND" string between filter arrays
            var jeSrchObj = search.create({
                type: "journalentry",
                filters: [
                    ["type", "anyof", "Journal"],
                    "AND",
                    // Custom column linking JE line to original invoice
                    ["custcol_je_attached_invoice", "anyof", invID],
                    "AND",
                    // mainline=T gets header, not individual lines
                    ["mainline", "is", "T"],
                    "AND",
                    // Exclude reversed JEs
                    ["isreversal", "is", "F"],
                    "AND",
                    ["reversaldate", "isempty", ""]
                ]
            });
            
            // 📚 MODULE 7: run().each() processes up to 4000 results
            // Return false to stop after first match (we only need one)
            jeSrchObj.run().each(function(r) {
                log.debug('jeSrchObj.r', JSON.stringify(r));
                jeID = r.id;
                return false;  // Stop iterating - found what we need
            });
            
        } catch (err01) {
            // 📚 MODULE 14: log.error() for errors (shows in red in logs)
            log.error('checkForExistingJE.err01', JSON.stringify(err01));
        } finally {
            return jeID;
        }
    }

    /**
     * ═══════════════════════════════════════════════════════════════════════
     * 📚 MODULE 5 & 6: Create New Journal Entry from Invoice
     * 
     * This function demonstrates:
     *   - Dynamic record creation with record.create()
     *   - Reading sublist data with getLineCount() and getSublistValue()
     *   - Writing sublist data with selectNewLine(), setCurrentSublistValue(), commitLine()
     *   - Error handling with email notification
     * ═══════════════════════════════════════════════════════════════════════
     */
    function createNewJE(invRec) {
        var jeID = '';
        
        try {
            // ─────────────────────────────────────────────────────────────
            // 📚 MODULE 3: Read header fields from the invoice
            // ─────────────────────────────────────────────────────────────
            var invSub = invRec.getValue({fieldId: 'subsidiary'});
            var invEntity = invRec.getValue({fieldId: 'entity'});
            var revRecDate = invRec.getValue({fieldId: 'custbody30'});
            var tranDate = invRec.getValue({fieldId: 'trandate'});
            var today = new Date().toISOString();
            var memo = 'Created from invoice: ' + invRec.getValue({fieldId: 'tranid'}) + 
                       ' (invID: ' + invRec.id + ') on ' + today;
            
            // 📚 MODULE 6: getLineCount() returns number of sublist lines
            var numItems = invRec.getLineCount({sublistId: 'item'});

            // 📚 MODULE 15: Debug logging for troubleshooting
            log.debug('invSub', invSub);
            log.debug('invEntity', invEntity);
            log.debug('memo', memo);
            log.debug('numItems', numItems);

            // ─────────────────────────────────────────────────────────────
            // 📚 MODULE 5: Create new record with record.create()
            // isDynamic: true = use selectNewLine/commitLine pattern
            // defaultValues: Pre-populate fields during creation
            // ─────────────────────────────────────────────────────────────
            var jeRec = record.create({
                type: record.Type.JOURNAL_ENTRY,
                isDynamic: true,
                defaultValues: {
                    subsidiary: invSub,
                    customform: [JE_FORM_ID]
                }
            });
            
            // 📚 MODULE 3: setValue() to set header fields
            jeRec.setValue({fieldId: 'trandate', value: tranDate});
            jeRec.setValue({fieldId: 'memo', value: invRec.getValue({fieldId: 'tranid'})});
            
            if (revRecDate != '') {
                jeRec.setValue({fieldId: 'custbody30', value: revRecDate});
            }

            var invAmount = 0;
            
            // ─────────────────────────────────────────────────────────────
            // 📚 MODULE 6: Loop through invoice lines to create JE lines
            // ─────────────────────────────────────────────────────────────
            for (var i = 0; i < numItems; i++) {
                
                // 📚 MODULE 6: getSublistValue() reads line item fields
                // Parameters: sublistId, fieldId, line (0-based index)
                var itemID = invRec.getSublistValue({
                    sublistId: 'item', 
                    fieldId: 'item', 
                    line: i
                }) || '';
                
                var description = invRec.getSublistValue({
                    sublistId: 'item', 
                    fieldId: 'description', 
                    line: i
                }) || '';
                
                var amount = invRec.getSublistValue({
                    sublistId: 'item', 
                    fieldId: 'amount', 
                    line: i
                }) || 0.00;
                
                // Additional line fields for complete data transfer
                var role = invRec.getSublistValue({sublistId: 'item', fieldId: 'custcol9', line: i}) || '';
                var payStart = invRec.getSublistValue({sublistId: 'item', fieldId: 'custcol7', line: i}) || '';
                var payEnd = invRec.getSublistValue({sublistId: 'item', fieldId: 'custcol8', line: i}) || '';
                var qty = invRec.getSublistValue({sublistId: 'item', fieldId: 'quantity', line: i}) || '';
                var rate = invRec.getSublistValue({sublistId: 'item', fieldId: 'rate', line: i}) || 0.00;
                var dept = invRec.getSublistValue({sublistId: 'item', fieldId: 'department', line: i}) || '';
                var classID = invRec.getSublistValue({sublistId: 'item', fieldId: 'class', line: i}) || '';
                var loc = invRec.getSublistValue({sublistId: 'item', fieldId: 'location', line: i}) || '';
                
                // 📚 MODULE 6: getSublistText() returns display value (not ID)
                var taxCode = invRec.getSublistText({
                    sublistId: 'item', 
                    fieldId: 'taxcode', 
                    line: i
                }) || '';

                // Determine which account to use (custom override or default)
                var itemAcct = '';
                var customAcct = invRec.getSublistValue({
                    sublistId: 'item', 
                    fieldId: 'custcol_item_account', 
                    line: i
                });
                
                if (customAcct != '' && customAcct !== undefined) {
                    itemAcct = customAcct;
                } else {
                    itemAcct = invRec.getSublistValue({
                        sublistId: 'item', 
                        fieldId: 'account', 
                        line: i
                    });
                }

                // Skip lines without an account
                if (!itemAcct) continue;

                // Running total for the debit side
                invAmount = parseFloat(invAmount) + parseFloat(amount);

                // ─────────────────────────────────────────────────────────────
                // 📚 MODULE 6: CREDIT LINE - Dynamic mode sublist pattern
                // 1. selectNewLine() - Start a new line
                // 2. setCurrentSublistValue() - Set field values
                // 3. commitLine() - Save the line
                // ─────────────────────────────────────────────────────────────
                jeRec.selectNewLine({sublistId: 'line'});
                
                // Required fields for JE line
                jeRec.setCurrentSublistValue({
                    sublistId: 'line', 
                    fieldId: 'account', 
                    value: itemAcct
                });
                jeRec.setCurrentSublistValue({
                    sublistId: 'line', 
                    fieldId: 'credit', 
                    value: amount
                });
                jeRec.setCurrentSublistValue({
                    sublistId: 'line', 
                    fieldId: 'entity', 
                    value: invEntity
                });
                
                // Link back to original invoice for audit trail
                jeRec.setCurrentSublistValue({
                    sublistId: 'line', 
                    fieldId: 'custcol_je_attached_invoice', 
                    value: invRec.id
                });
                
                // Optional fields - only set if they have values
                if (description != '') {
                    jeRec.setCurrentSublistValue({
                        sublistId: 'line', 
                        fieldId: 'memo', 
                        value: description
                    });
                }
                if (itemID != '') {
                    jeRec.setCurrentSublistValue({
                        sublistId: 'line', 
                        fieldId: 'custcol_invoice_item', 
                        value: itemID
                    });
                }
                if (dept != '') {
                    jeRec.setCurrentSublistValue({
                        sublistId: 'line', 
                        fieldId: 'department', 
                        value: dept
                    });
                }
                if (classID != '') {
                    jeRec.setCurrentSublistValue({
                        sublistId: 'line', 
                        fieldId: 'class', 
                        value: classID
                    });
                }
                if (loc != '') {
                    jeRec.setCurrentSublistValue({
                        sublistId: 'line', 
                        fieldId: 'location', 
                        value: loc
                    });
                }
                
                // 📚 MODULE 6: commitLine() saves the current line
                jeRec.commitLine({sublistId: 'line'});
            }

            // ─────────────────────────────────────────────────────────────
            // DEBIT LINE - Single line to Accrual Account
            // This balances all the credit lines above
            // ─────────────────────────────────────────────────────────────
            jeRec.selectNewLine({sublistId: 'line'});
            jeRec.setCurrentSublistValue({
                sublistId: 'line', 
                fieldId: 'account', 
                value: [ACCRUAL_ACCOUNT_ID]  // Accrued Revenue account
            });
            jeRec.setCurrentSublistValue({
                sublistId: 'line', 
                fieldId: 'debit', 
                value: invAmount  // Total of all credit lines
            });
            jeRec.setCurrentSublistValue({
                sublistId: 'line', 
                fieldId: 'entity', 
                value: invEntity
            });
            jeRec.setCurrentSublistValue({
                sublistId: 'line', 
                fieldId: 'custcol_je_attached_invoice', 
                value: invRec.id
            });
            jeRec.setCurrentSublistValue({
                sublistId: 'line', 
                fieldId: 'memo', 
                value: memo
            });
            jeRec.commitLine({sublistId: 'line'});

            log.debug('jeRec', JSON.stringify(jeRec));
            
            // 📚 MODULE 5: save() commits the record to database
            // Returns the internal ID of the new record
            jeID = jeRec.save();

        } catch (err01) {
            // ─────────────────────────────────────────────────────────────
            // 📚 MODULE 14: Error Handling with Email Notification
            // In production, always notify someone when critical scripts fail!
            // ─────────────────────────────────────────────────────────────
            log.error('createNewJE.err01', JSON.stringify(err01));
            
            var mailSubject = 'Invoice ' + invRec.getValue('tranid') + 
                              ' Accrual JE Creation Error.';
            var mailBody = err01.message;

            // 📚 MODULE 10: Get script parameters for configurable values
            // This allows changing recipients without modifying code!
            var toEmails = scriptObj.getParameter('custscript_je_create_notify_emails');
            var emailArray = toEmails.split(",");

            // 📚 MODULE 5: email.send() for notifications
            // relatedRecords links the email to the transaction for easy access
            email.send({
                author: scriptObj.getParameter('custscript_error_email_from'),
                recipients: emailArray,
                subject: mailSubject,
                body: mailBody,
                relatedRecords: {
                    transactionId: Number(invRec.id)
                }
            });
        } finally {
            return jeID;
        }
    }

    /***********************************************/
    /************ END UTILITY FUNCTIONS ************/
    /***********************************************/

    // 📚 MODULE 2: Return object exports the entry point function
    return {
        onAction: onAction
    };
});
⚠️ Configuration Required

Before deploying, replace these placeholders with your actual values:

  • [ACCOUNT_ID] - Your NetSuite account number
  • [SANDBOX_FILE_ID], [PROD_FILE_ID] - File Cabinet internal IDs
  • [SCRIPT_ID], [DEPLOY_ID] - Script and deployment internal IDs
  • [ACCRUAL_ACCOUNT_ID] - Internal ID of your Accrued Revenue GL account
  • [JE_FORM_ID] - Internal ID of your Journal Entry custom form
📘 Script Parameters Required

Create these parameters in the Script Record → Parameters subtab:

IDTypeDescription
custscript_je_create_notify_emailsFree-Form TextComma-separated email addresses for error notifications
custscript_error_email_fromList/Record (Employee)Employee to send error emails from
✅ Key Patterns Demonstrated
  • Modular Design - Main function calls helper functions for readability
  • Defensive Coding - Check for empty values before setting fields
  • Audit Trail - Link JE back to source invoice via custom column
  • Configurable Values - Use script parameters instead of hardcoded IDs
  • Error Recovery - Email notifications when critical operations fail
  • Efficient Updates - Use submitFields() instead of load/save pattern