Chapter 7.1

SuiteScript Overview

Understand NetSuite's JavaScript-based scripting platform, API versions, script types, governance model, and when to use code vs. configuration for business requirements.

What is SuiteScript?

SuiteScript is NetSuite's JavaScript-based API that enables developers to extend and customize the platform beyond its native capabilities. It provides programmatic access to NetSuite data, business logic, and user interface components.

ℹ️ SuiteScript Versions
  • SuiteScript 1.0: Legacy API, still supported but no longer enhanced
  • SuiteScript 2.0: Modern module-based API (recommended for all new development)
  • SuiteScript 2.1: ES2019+ JavaScript features (async/await, arrow functions)

All new scripts should use SuiteScript 2.1 unless specific compatibility requirements exist.

When to Use SuiteScript

Before writing code, evaluate whether your requirement can be met through configuration. SuiteScript should be used when native features are insufficient.

Requirement Configuration Option When SuiteScript Needed
Field validation Mandatory fields, validation rules Complex cross-field logic, external validation
Auto-populate fields Sourcing, default values, formulas Complex calculations, external lookups
Approval workflows SuiteFlow workflows Complex routing, external system integration
Scheduled tasks Saved search alerts, reminders Data processing, integration syncs
Custom UI Custom forms, custom records Custom pages, portlets, dynamic interfaces
Integrations Native connectors (if available) Custom APIs, bidirectional sync
🎯 The Configuration-First Principle

Always try configuration before coding. Native features are:

  • Maintained by NetSuite (automatic upgrades)
  • Better documented and supported
  • No governance consumption
  • Easier for administrators to maintain

Script Types Overview

Script Type Execution Context Primary Use Cases
Client Script Browser (user's machine) Field validation, UI behavior, real-time calculations
User Event Script Server (on record save/load) Data validation, auto-population, transformations
Scheduled Script Server (time-based) Batch processing, data cleanup, integrations
Map/Reduce Script Server (parallel processing) Large data sets, complex transformations
Suitelet Server (on-demand page) Custom UI pages, wizards, internal tools
Restlet Server (HTTP endpoint) REST APIs, external integrations

Governance Model

NetSuite uses a governance system to ensure fair resource allocation. Each script execution is allocated governance units, and API calls consume these units.

⚠️ Governance Limits by Script Type
Script Type Governance Units
Client Script1,000 units
User Event Script1,000 units
Suitelet1,000 units
Restlet5,000 units
Scheduled Script10,000 units
Map/Reduce Script10,000 units per phase

Common API Governance Costs

Operation                               Units
--------------------------------------------------
record.load()                           10
record.save()                           20
record.delete()                         20
search.create().run()                   10
search.lookupFields()                   1
email.send()                            20
http.request()                          10
file.load()                             10

Script Deployment

Every script requires two components: the Script record and the Script Deployment record.

Customization Scripting Scripts New
1

Upload Script File

Upload .js file to File Cabinet (SuiteScripts folder recommended)

2

Create Script Record

Select script file, configure script parameters if needed

3

Create Deployment

Link script to record types/contexts, set execution roles

4

Set Status to Released

Change deployment status from Testing to Released for production

SuiteScript 2.x Module System

SuiteScript 2.x uses AMD (Asynchronous Module Definition) pattern for organizing code:

Basic Script Structure (SuiteScript 2.1)
/**
 * @NApiVersion 2.1
 * @NScriptType UserEventScript
 * @NModuleScope SameAccount
 */
define(['N/record', 'N/search', 'N/log'], (record, search, log) => {

    const beforeLoad = (context) => {
        log.debug('Before Load', `Record ID: ${context.newRecord.id}`);
    };

    const afterSubmit = (context) => {
        // Runs after record is saved to database
    };

    return { beforeLoad, afterSubmit };
});

Key Modules

Module Purpose Common Methods
N/record CRUD operations on records load, create, copy, transform, delete
N/search Saved searches and lookups create, load, lookupFields
N/query SuiteQL queries runSuiteQL, runSuiteQLPaged
N/log Script logging debug, audit, error, emergency
N/email Send emails send, sendBulk
N/file File Cabinet operations load, create, delete
N/http / N/https HTTP/HTTPS requests get, post, put, delete, request
N/runtime Runtime information getCurrentUser, getCurrentScript
N/ui/serverWidget UI components (Suitelets) createForm, createList
N/task Trigger scheduled scripts create (MapReduceTask, ScheduledTask)

Development Best Practices

💡 SuiteScript Development Tips
  • Use SuiteCloud IDE: Eclipse-based or VS Code extension for syntax checking, upload, debugging
  • Test in sandbox: Never develop directly in production
  • Log strategically: Use log.debug() during development, minimize in production
  • Handle errors: Always wrap code in try/catch blocks
  • Monitor governance: Check remaining units for long-running scripts
  • Use script parameters: Externalize configuration for flexibility
  • Document: JSDoc comments, README files, version history
Error Handling Pattern
const afterSubmit = (context) => {
    try {
        const customerId = context.newRecord.getValue('entity');
        if (!customerId) {
            log.error('Validation Error', 'Customer is required');
            return;
        }
        // Process logic...
        log.audit('Success', `Processed customer ${customerId}`);
    } catch (error) {
        log.error({
            title: 'Script Error',
            details: `Error: ${error.message}\nStack: ${error.stack}`
        });
    }
};

SuiteCloud Development Framework (SDF)

SDF enables source control, automated deployment, and team collaboration for NetSuite customizations:

ℹ️ SDF Benefits
  • Version control: Track all customizations in Git
  • Automated deployment: CLI commands for sandbox/production releases
  • Team collaboration: Multiple developers working on same project
  • Object dependencies: Automatic ordering of deployment objects
  • Account customization projects (ACP): Deploy scripts, records, forms together

SuiteScript Readiness Checklist

Chapter 7.2

User Event Scripts

Master server-side scripts that execute on record load, before save, and after save—the workhorses of NetSuite automation.

What Are User Event Scripts?

User Event scripts run on the server whenever a record is loaded, created, edited, or deleted. They execute automatically with record operations—no user action required.

ℹ️ User Event Entry Points
  • beforeLoad: Runs before record is displayed (view/edit modes)
  • beforeSubmit: Runs before record is saved to database
  • afterSubmit: Runs after record is saved to database

Entry Point Details

beforeLoad

Executes before the record form is displayed. Use for UI modifications.

Use Case Example
Add custom buttonsAdd "Generate PDF" button to sales order
Hide/show fieldsHide discount field for non-managers
Set field defaultsDefault ship date to tomorrow
Add sublist columnsAdd calculated column to item sublist

beforeSubmit

Executes before the record is written to the database. Perfect for validation and data transformation.

Use Case Example
Data validationEnsure margin meets minimum threshold
Data transformationUppercase customer name
Auto-populate fieldsSet approval status based on amount
Block savesPrevent duplicate PO numbers

afterSubmit

Executes after the record is saved. Use for operations that require the saved record ID.

Use Case Example
Create related recordsCreate task when opportunity closes
Send notificationsEmail customer when order ships
Update other recordsUpdate customer status based on orders
External integrationsPush data to external CRM

Context Object

The context parameter provides essential information about the execution:

Property Description Available In
context.newRecord Current record being processed All entry points
context.oldRecord Record before changes (edit/delete only) beforeSubmit, afterSubmit
context.type Operation type (CREATE, EDIT, DELETE, etc.) All entry points
context.form Form object for UI modifications beforeLoad only

Comparing Old and New Values

In beforeSubmit and afterSubmit, compare old and new record values to detect changes:

Detecting Field Changes
const beforeSubmit = (context) => {
    if (context.type !== context.UserEventType.EDIT) return;

    const oldRec = context.oldRecord;
    const newRec = context.newRecord;

    const oldStatus = oldRec.getValue('status');
    const newStatus = newRec.getValue('status');

    if (oldStatus !== newStatus) {
        log.audit('Status Changed', `From ${oldStatus} to ${newStatus}`);
        if (newStatus === 'approved' && oldStatus !== 'approved') {
            // Trigger approval notification
        }
    }
};

Execution Context

User Event scripts execute in different contexts. Use runtime module to detect source:

Checking Execution Context
define(['N/runtime', 'N/log'], (runtime, log) => {

    const beforeSubmit = (context) => {
        const execContext = runtime.executionContext;

        // Log the execution context
        log.debug('Execution Context', execContext);

        // Skip if running from CSV import
        if (execContext === runtime.ContextType.CSV_IMPORT) {
            log.audit('Skipped', 'Running from CSV import');
            return;
        }

        // Skip if running from another script
        if (execContext === runtime.ContextType.SCHEDULED ||
            execContext === runtime.ContextType.MAP_REDUCE) {
            log.audit('Skipped', 'Running from scheduled processing');
            return;
        }

        // Continue with validation for UI and web service contexts
        // ...
    };

    return { beforeSubmit };
});

Common Execution Contexts

Context Description
USER_INTERFACERecord edited through NetSuite UI
CSV_IMPORTCSV Import operation
WEBSERVICESWeb Services (SOAP)
RESTLETRESTlet API call
SCHEDULEDScheduled script
MAP_REDUCEMap/Reduce script
SUITELETSuitelet execution
WORKFLOWWorkflow action

Best Practices

🎯 User Event Script Best Practices
  • Always check context.type: Don't run validation on DELETE or logic on VIEW
  • Handle errors gracefully: Use try/catch and log errors for debugging
  • Minimize governance: Avoid record.load() when context.newRecord suffices
  • Consider all entry points: Remember scripts run on CSV import, web services, etc.
  • Use afterSubmit for external calls: Don't block saves with slow API calls
  • Validate in beforeSubmit: Throw errors to prevent invalid data
⚠️ Common Mistakes
  • Infinite loops: afterSubmit that edits the same record triggers another afterSubmit
  • Blocking on external APIs: Slow beforeSubmit makes saves timeout
  • Not handling null: Fields may be empty; always check before operations
  • Ignoring oldRecord: Always compare to detect actual changes vs. re-saves
  • Over-logging: Excessive log.debug() in production wastes resources

Preventing Infinite Loops

Safe Record Update Pattern
define(['N/record', 'N/runtime', 'N/log'], (record, runtime, log) => {

    const afterSubmit = (context) => {
        // Method 1: Check execution context
        if (runtime.executionContext === runtime.ContextType.USER_EVENT) {
            log.debug('Skipped', 'Avoiding recursive call');
            return;
        }

        // Method 2: Use a flag field
        const rec = context.newRecord;
        if (rec.getValue('custbody_processed_flag')) {
            return;  // Already processed
        }

        // Method 3: Track in script parameter or global
        const scriptObj = runtime.getCurrentScript();
        const processedIds = scriptObj.getParameter('custscript_processed') || '';
        if (processedIds.includes(rec.id)) {
            return;
        }

        // Safe to update
        record.submitFields({
            type: rec.type,
            id: rec.id,
            values: {
                'custbody_processed_flag': true,
                'custbody_processed_date': new Date()
            }
        });
    };

    return { afterSubmit };
});

Deployment Configuration

Customization Scripting Scripts [Script] Deployments
Setting Recommendation
Applies ToSelect specific record types; avoid "All Records"
Execute As RoleUse dedicated role with minimum required permissions
Log LevelDEBUG during testing, ERROR in production
StatusTesting until fully validated, then Released
All EmployeesUsually yes; restrict only if role-specific logic needed

User Event Script Checklist

Chapter 7.3

Client Scripts

Browser-side scripts that enhance user experience with real-time validation, field manipulation, and dynamic form behavior.

Client Script Overview

Client Scripts execute in the user's browser when interacting with NetSuite forms. They provide immediate feedback, enforce business rules before submission, and create dynamic user experiences.

Client Script Entry Points

Entry Point Trigger Common Uses
pageInit Form loads in browser Initialize field values, hide/show fields
fieldChanged User changes field value Dynamic sourcing, conditional logic
postSourcing After sourcing completes Override sourced values
sublistChanged Line added/removed/changed Sublist totals, line validation
validateField Before field value commits Field-level validation
saveRecord Before form submits Final validation, confirmation dialogs
⚖️ Key Decision

Client Script vs. User Event

Client Script Template

A comprehensive Client Script structure with all entry points:

Complete Client Script Structure (All Entry Points)
/**
 * @NApiVersion 2.1
 * @NScriptType ClientScript
 * @NModuleScope SameAccount
 */
define(['N/currentRecord', 'N/dialog', 'N/runtime', 'N/search'],
    (currentRecord, dialog, runtime, search) => {

    /**
     * Page initialization - runs when form loads
     * @param {Object} context
     * @param {Record} context.currentRecord - Current form record
     * @param {string} context.mode - create, edit, copy, or view
     */
    const pageInit = (context) => {
        const rec = context.currentRecord;
        const mode = context.mode;

        console.log(`Page initialized in ${mode} mode`);

        // Set default values on create
        if (mode === 'create') {
            rec.setValue({
                fieldId: 'custbody_created_by',
                value: runtime.getCurrentUser().id
            });
        }

        // Disable fields based on status
        if (mode === 'edit') {
            const status = rec.getValue('status');
            if (status === 'approved') {
                // Use jQuery to disable field (standard approach)
                jQuery('#custbody_amount').attr('disabled', true);
            }
        }
    };

    /**
     * Field change handler
     * @param {Object} context
     * @param {Record} context.currentRecord
     * @param {string} context.sublistId - Sublist internal ID (if applicable)
     * @param {string} context.fieldId - Changed field's internal ID
     * @param {number} context.line - Line index (if sublist)
     */
    const fieldChanged = (context) => {
        const rec = context.currentRecord;
        const fieldId = context.fieldId;
        const sublistId = context.sublistId;

        // Body field change
        if (!sublistId && fieldId === 'entity') {
            const customerId = rec.getValue('entity');
            if (customerId) {
                lookupCustomerDefaults(rec, customerId);
            }
        }

        // Sublist field change
        if (sublistId === 'item' && fieldId === 'quantity') {
            calculateLineAmount(rec, context.line);
        }
    };

    /**
     * Post-sourcing handler - after NetSuite finishes sourcing
     * @param {Object} context
     */
    const postSourcing = (context) => {
        const rec = context.currentRecord;
        const fieldId = context.fieldId;
        const sublistId = context.sublistId;

        // Override sourced price for specific customer
        if (sublistId === 'item' && fieldId === 'item') {
            const customerId = rec.getValue('entity');
            const itemId = rec.getCurrentSublistValue({
                sublistId: 'item',
                fieldId: 'item'
            });

            // Look up special pricing
            const specialPrice = getSpecialPrice(customerId, itemId);
            if (specialPrice) {
                rec.setCurrentSublistValue({
                    sublistId: 'item',
                    fieldId: 'rate',
                    value: specialPrice
                });
            }
        }
    };

    /**
     * Line initialization
     * @param {Object} context
     */
    const lineInit = (context) => {
        const rec = context.currentRecord;
        const sublistId = context.sublistId;

        if (sublistId === 'item') {
            // Default quantity to 1 for new lines
            rec.setCurrentSublistValue({
                sublistId: 'item',
                fieldId: 'quantity',
                value: 1
            });
        }
    };

    /**
     * Field validation - return false to reject change
     * @param {Object} context
     * @returns {boolean}
     */
    const validateField = (context) => {
        const rec = context.currentRecord;
        const fieldId = context.fieldId;
        const sublistId = context.sublistId;

        // Validate discount percentage
        if (sublistId === 'item' && fieldId === 'custcol_discount_pct') {
            const discount = rec.getCurrentSublistValue({
                sublistId: 'item',
                fieldId: 'custcol_discount_pct'
            });

            if (discount > 25) {
                dialog.alert({
                    title: 'Discount Limit',
                    message: 'Discount cannot exceed 25%. Contact management for approval.'
                });
                return false; // Reject the change
            }
        }

        return true; // Accept the change
    };

    /**
     * Line validation - return false to reject line commit
     * @param {Object} context
     * @returns {boolean}
     */
    const validateLine = (context) => {
        const rec = context.currentRecord;
        const sublistId = context.sublistId;

        if (sublistId === 'item') {
            const quantity = rec.getCurrentSublistValue({
                sublistId: 'item',
                fieldId: 'quantity'
            });
            const rate = rec.getCurrentSublistValue({
                sublistId: 'item',
                fieldId: 'rate'
            });

            if (!quantity || quantity <= 0) {
                dialog.alert({
                    title: 'Invalid Quantity',
                    message: 'Quantity must be greater than zero.'
                });
                return false;
            }

            if (!rate || rate < 0) {
                dialog.alert({
                    title: 'Invalid Rate',
                    message: 'Rate cannot be negative.'
                });
                return false;
            }
        }

        return true;
    };

    /**
     * Delete validation
     * @param {Object} context
     * @returns {boolean}
     */
    const validateDelete = (context) => {
        const rec = context.currentRecord;
        const sublistId = context.sublistId;

        if (sublistId === 'item') {
            const lineCount = rec.getLineCount({ sublistId: 'item' });
            if (lineCount <= 1) {
                dialog.alert({
                    title: 'Cannot Delete',
                    message: 'Transaction must have at least one line item.'
                });
                return false;
            }
        }

        return true;
    };

    /**
     * Sublist changed - after line commit or delete
     * @param {Object} context
     */
    const sublistChanged = (context) => {
        const rec = context.currentRecord;
        const sublistId = context.sublistId;

        if (sublistId === 'item') {
            updateHeaderTotals(rec);
        }
    };

    /**
     * Save validation - return false to prevent save
     * @param {Object} context
     * @returns {boolean}
     */
    const saveRecord = (context) => {
        const rec = context.currentRecord;

        // Validate required custom fields
        const projectCode = rec.getValue('custbody_project_code');
        if (!projectCode) {
            dialog.alert({
                title: 'Missing Required Field',
                message: 'Please enter a Project Code before saving.'
            });
            return false;
        }

        // Confirm large orders
        const total = rec.getValue('total');
        if (total > 100000) {
            // Note: dialog.confirm returns a Promise in 2.1
            // For synchronous behavior, use window.confirm
            if (!window.confirm('Order total exceeds $100,000. Continue?')) {
                return false;
            }
        }

        return true;
    };

    // Helper functions
    const lookupCustomerDefaults = (rec, customerId) => {
        const customerLookup = search.lookupFields({
            type: search.Type.CUSTOMER,
            id: customerId,
            columns: ['custentity_default_terms', 'custentity_sales_rep']
        });
        // Apply defaults...
    };

    const calculateLineAmount = (rec, line) => {
        // Calculate logic...
    };

    const getSpecialPrice = (customerId, itemId) => {
        // Price lookup logic...
        return null;
    };

    const updateHeaderTotals = (rec) => {
        // Recalculate header...
    };

    return {
        pageInit,
        fieldChanged,
        postSourcing,
        lineInit,
        validateField,
        validateLine,
        validateDelete,
        sublistChanged,
        saveRecord
    };
});

Entry Point Execution Order

Understanding the sequence helps debug and design scripts:

  1. pageInit — Form loads
  2. fieldChanged — User edits field
  3. postSourcing — Sourcing completes
  4. validateField — Before value is set
  5. lineInit — Select new sublist line
  6. validateLine — Commit line
  7. sublistChanged — Line committed
  8. saveRecord — Save clicked

Client-Side vs Server-Side Validation

Aspect Client Script User Event Script
ExecutionBrowser (JavaScript)NetSuite server
Triggered byUI interaction onlyUI, CSV imports, web services, scripts
Response timeImmediate (no server round-trip)After form submission
Bypassable?Yes (browser tools, API calls)No (runs on server)
Best forUX improvement, non-critical validationCritical business rules, data integrity
⚠️ Critical: Duplicate Validation
Always duplicate critical validations on the server. Client Scripts only run in the browser. Records created via CSV import, web services, or SuiteScript bypass Client Script validation entirely. Use Client Scripts for UX; use User Event Scripts for enforcement.

Common Client Script Patterns

Dynamic Field Show/Hide

Toggle Field Visibility
const fieldChanged = (context) => {
    if (context.fieldId === 'custbody_payment_type') {
        const rec = context.currentRecord;
        const paymentType = rec.getValue('custbody_payment_type');
        const ccField = rec.getField({ fieldId: 'custbody_cc_number' });
        const checkField = rec.getField({ fieldId: 'custbody_check_number' });
        ccField.isDisplay = (paymentType === 'credit_card');
        checkField.isDisplay = (paymentType === 'check');
    }
};

Calculated Fields

Auto-Calculate Margin
const fieldChanged = (context) => {
    if (context.fieldId === 'custbody_unit_cost' || context.fieldId === 'custbody_sale_price') {
        const rec = context.currentRecord;
        const cost = rec.getValue('custbody_unit_cost') || 0;
        const price = rec.getValue('custbody_sale_price') || 0;
        const margin = price > 0 ? ((price - cost) / price * 100).toFixed(2) : 0;
        rec.setValue({ fieldId: 'custbody_margin_pct', value: margin, ignoreFieldChange: true });
    }
};

Cascading Dropdowns

Rebuild Dependent Dropdown on Parent Change
const fieldChanged = (context) => {
    const rec = context.currentRecord;

    if (context.fieldId === 'custbody_country') {
        const countryId = rec.getValue('custbody_country');

        // Clear dependent field
        rec.setValue({
            fieldId: 'custbody_state',
            value: ''
        });

        // Rebuild state dropdown based on country
        const stateField = rec.getField({ fieldId: 'custbody_state' });

        // Remove existing options (except blank)
        stateField.removeSelectOption({ value: null }); // Clear all
        stateField.insertSelectOption({ value: '', text: '-- Select --' });

        // Lookup states for selected country
        const states = lookupStates(countryId);
        states.forEach(state => {
            stateField.insertSelectOption({
                value: state.id,
                text: state.name
            });
        });
    }
};

Sublist Totals in Real-Time

Calculate Header Totals from Line Items
const sublistChanged = (context) => {
    if (context.sublistId === 'item') {
        calculateTotals(context.currentRecord);
    }
};

const calculateTotals = (rec) => {
    const lineCount = rec.getLineCount({ sublistId: 'item' });
    let totalQty = 0;
    let totalWeight = 0;

    for (let i = 0; i < lineCount; i++) {
        totalQty += rec.getSublistValue({
            sublistId: 'item',
            fieldId: 'quantity',
            line: i
        }) || 0;

        totalWeight += rec.getSublistValue({
            sublistId: 'item',
            fieldId: 'custcol_weight',
            line: i
        }) || 0;
    }

    // Update header fields
    rec.setValue({ fieldId: 'custbody_total_qty', value: totalQty });
    rec.setValue({ fieldId: 'custbody_total_weight', value: totalWeight });

    // Update shipping estimate based on weight
    const shippingRate = getShippingRate(totalWeight);
    rec.setValue({ fieldId: 'custbody_est_shipping', value: shippingRate });
};

Debugging Client Scripts

💡 Browser Developer Tools

Client Scripts run in the browser--use developer tools (F12) for debugging:

  • Console: console.log() statements appear here
  • Sources: Set breakpoints, step through code
  • Network: Monitor AJAX calls from N/https or N/search
Debugging Techniques
// Debugging techniques
const fieldChanged = (context) => {
    // Log context for debugging
    console.log('fieldChanged triggered');
    console.log('Field ID:', context.fieldId);
    console.log('Sublist ID:', context.sublistId);
    console.log('Line:', context.line);

    // Log current values
    const rec = context.currentRecord;
    console.log('Customer:', rec.getValue('entity'));
    console.log('Total:', rec.getValue('total'));

    // Inspect record structure
    console.log('Record object:', rec);
};

Client Script Deployment

Setting Description Best Practice
Applies ToRecord types the script monitorsLimit to specific records needed
FormSpecific form or AllTarget specific forms when possible
StatusReleased, Testing, Not ScheduledUse Testing in Sandbox
Log LevelDebug, Audit, ErrorDebug in Sandbox, Audit in Production

Performance Considerations

⚠️ Avoid Synchronous Server Calls
Client Scripts run in the browser. Synchronous server calls (like N/search) block the UI and create poor user experience. Use sparingly and consider caching results. Use ignoreFieldChange to prevent recursive fieldChanged calls when setting values programmatically.

Industry Patterns

👔 Professional Services
Professional Services Consideration
BOM Validation: Validate component availability when selecting assembly items. Calculate lead times based on component stock levels. Prevent selection of components on hold.
👔 Professional Services
Professional Services Consideration
Pricing Tiers: Dynamically update pricing when quantity thresholds are reached. Show customer-specific pricing immediately upon customer selection.
👔 Professional Services
Professional Services Consideration
Subscription Validation: Calculate prorated amounts when start dates change. Validate term lengths and renewal dates. Auto-populate ARR/MRR fields.
👔 Professional Services
Professional Services Consideration
Resource Availability: Check consultant availability when assigning to projects. Calculate utilization impact in real-time. Warn on overbooking.

Client Script Checklist

Chapter 7.4

Scheduled Scripts

Time-based server scripts for batch processing, data maintenance, and scheduled integrations.

Scheduled Script Overview

Scheduled Scripts run on a defined schedule (daily, hourly, or specific times) to perform background processing without user interaction. They're ideal for batch operations, data cleanup, and scheduled integrations.

Customization Scripting Scripts [Script] Deployments Schedule

Common Use Cases

Use Case Schedule Description
Invoice Generation Daily, early morning Create invoices from fulfilled orders
Data Cleanup Weekly, off-hours Archive old records, clean temp data
Integration Sync Every 15-30 minutes Pull/push data from external systems
Report Generation Daily/Weekly Generate and email reports
Reminder Emails Daily Send overdue invoice reminders

Scheduled Script Entry Point

Scheduled Scripts have a single entry point that receives execution context:

Scheduled Script Template
/**
 * @NApiVersion 2.1
 * @NScriptType ScheduledScript
 */
define(['N/search', 'N/record', 'N/runtime', 'N/log'], (search, record, runtime, log) => {
    const execute = (context) => {
        log.audit('Script Start', `Type: ${context.type}`);
        try {
            const script = runtime.getCurrentScript();
            mySearch.run().each((result) => {
                if (script.getRemainingUsage() < 100) return false;
                processRecord(result);
                return true;
            });
        } catch (e) {
            log.error('Script Error', e.message);
        }
    };
    return { execute };
});

Execution Types

Type Description Use Case
SCHEDULEDTriggered by scheduleRegular automated processing
ON_DEMANDManually via "Save and Execute"Testing, one-time runs
USER_INTERFACETriggered from another scriptQueued from User Event or Suitelet
ABORTEDPrevious execution was abortedRecovery logic, cleanup
SKIPPEDPrevious execution was skippedCatch-up processing

Scheduling Options

Schedule Type Options Example Use Case
Single ExecutionSpecific date and timeOne-time data cleanup
DailyEvery day at specified timeNightly report generation
WeeklySpecific days of the weekMonday morning KPI emails
MonthlyDay of month or first/last weekdayMonth-end close tasks
RepeatEvery N minutes (15, 30, 60)Integration polling
⚠️ Time Zone Awareness
Scheduled Scripts execute in the time zone of the deployment's owner. For consistent timing across subsidiaries, set a single owner with a known time zone, or use script parameters to handle offsets.

Governance Management

Scheduled Scripts must manage their 10,000-unit budget carefully:

Governance Check with Reschedule
const processRecords = () => {
    const script = runtime.getCurrentScript();
    const RESERVE_UNITS = 500;

    mySearch.run().each((result) => {
        if (script.getRemainingUsage() < RESERVE_UNITS) {
            log.audit('Governance Limit', 'Stopping - reschedule needed');
            rescheduleScript();
            return false;
        }
        processRecord(result);
        return true;
    });
};

Script Parameters

Use script parameters to pass configuration and maintain state across executions:

Accessing Script Parameters
const script = runtime.getCurrentScript();
const batchSize = script.getParameter({ name: 'custscript_batch_size' }) || 1000;
const emailRecipient = script.getParameter({ name: 'custscript_notify_email' });
const lastProcessedId = script.getParameter({ name: 'custscript_last_id' }) || 0;

Triggering Scheduled Scripts Programmatically

Queue Scheduled Scripts from other scripts using the N/task module:

Queue from User Event Script
const queueProcessingScript = (recordId) => {
    try {
        const scheduledTask = task.create({
            taskType: task.TaskType.SCHEDULED_SCRIPT,
            scriptId: 'customscript_process_order',
            deploymentId: 'customdeploy_process_order_queue',
            params: { 'custscript_order_id': recordId }
        });
        scheduledTask.submit();
    } catch (e) {
        if (e.name === 'FAILED_TO_SUBMIT_JOB_REQUEST_1') {
            log.debug('Already Queued', 'Script already pending');
        } else { throw e; }
    }
};

Checking Script Status

Check Task Status
const taskStatus = task.checkStatus({ taskId: taskId });
// Possible statuses: PENDING, PROCESSING, COMPLETE, FAILED
log.debug('Status', taskStatus.status);

Common Patterns

Batch Email Sender

Process email queue records in batches with governance checks and error handling per record.

Data Synchronization

Get records modified since last sync, push to external system, save checkpoint for next run.

Report Generation

Generate report data, create CSV/PDF file in File Cabinet, email to recipients.

Deployment Best Practices

Setting Recommendation Rationale
StatusTesting then ReleasedTest thoroughly before releasing
Log LevelAudit (Production)Capture key events without noise
Execute As RoleDedicated script roleControlled permissions
Queue LimitSet based on processing timePrevent queue overflow
Yield HandlingEnable for long processesAllow recovery from interruption
💡 When to Use Scheduled vs Map/Reduce
  • Scheduled Script: Under 10,000 records, simple processing, time-sensitive execution
  • Map/Reduce Script: Large data volumes, parallel processing beneficial, can tolerate longer queue times

Industry Patterns

👔 Professional Services
Professional Services Consideration
MRP Processing: Nightly scripts to calculate material requirements, generate planned orders, and identify shortages. Batch update lead times based on supplier performance data.
👔 Professional Services
Professional Services Consideration
Price Updates: Scheduled overnight to apply cost changes, update margin calculations, and synchronize prices with e-commerce platforms.
👔 Professional Services
Professional Services Consideration
Usage Processing: Hourly scripts to pull usage metrics, calculate overages, and trigger billing events. Daily scripts for license compliance checks.

Scheduled Script Checklist

Chapter 7.5

Map/Reduce Scripts

Parallel processing scripts for high-volume data operations with automatic governance management.

Map/Reduce Overview

Map/Reduce scripts process large data sets by breaking work into parallel stages. They automatically manage governance, handle failures, and scale across NetSuite's infrastructure.

The Four Stages

Stage Purpose Concurrency
getInputData Define data to process (search, query, array) Single execution
map Transform each input into key-value pairs Parallel (up to 50)
reduce Process grouped values by key Parallel (up to 50)
summarize Handle results, errors, generate summary Single execution
⚖️ Key Decision

Scheduled vs. Map/Reduce

ℹ️ Map/Reduce Benefits
  • Automatic parallelization: NetSuite manages concurrent execution
  • Governance reset: Each map/reduce invocation gets fresh governance
  • Error isolation: One failed record doesn't stop the entire job
  • Built-in reporting: Summary stage provides execution statistics

Basic Map/Reduce Template

Complete Map/Reduce Script
/**
 * @NApiVersion 2.1
 * @NScriptType MapReduceScript
 * @NModuleScope SameAccount
 */
define(['N/search', 'N/record', 'N/email', 'N/runtime', 'N/log'],
    (search, record, email, runtime, log) => {

    const getInputData = () => {
        log.audit('Stage', 'getInputData started');
        // Return a saved search - NetSuite streams results
        return search.create({
            type: search.Type.SALES_ORDER,
            filters: [
                ['status', 'anyof', 'SalesOrd:B'],
                ['mainline', 'is', 'T']
            ],
            columns: ['entity', 'tranid', 'total', 'trandate']
        });
    };

    const map = (context) => {
        const searchResult = JSON.parse(context.value);
        const orderId = searchResult.id;
        const customerId = searchResult.values.entity.value;
        try {
            const orderData = processOrder(orderId);
            context.write({
                key: customerId,
                value: { orderId, total: orderData.total, status: orderData.status }
            });
        } catch (e) {
            log.error('Map Error', `Order ${orderId}: ${e.message}`);
        }
    };

    const reduce = (context) => {
        const customerId = context.key;
        let totalAmount = 0;
        let orderCount = 0;
        context.values.forEach((valueJson) => {
            const value = JSON.parse(valueJson);
            totalAmount += value.total;
            orderCount++;
        });
        try {
            record.submitFields({
                type: record.Type.CUSTOMER,
                id: customerId,
                values: {
                    'custentity_pending_order_total': totalAmount,
                    'custentity_pending_order_count': orderCount
                }
            });
            context.write({ key: 'success', value: customerId });
        } catch (e) {
            context.write({ key: 'error', value: { customerId, error: e.message } });
        }
    };

    const summarize = (summary) => {
        log.audit('Usage', JSON.stringify({
            mapErrors: summary.mapSummary.errors.length,
            reduceErrors: summary.reduceSummary.errors.length,
            inputStageTime: summary.inputSummary.seconds,
            mapStageTime: summary.mapSummary.seconds,
            reduceStageTime: summary.reduceSummary.seconds
        }));

        let successCount = 0;
        let errorList = [];
        summary.output.iterator().each((key, value) => {
            if (key === 'success') successCount++;
            else if (key === 'error') errorList.push(JSON.parse(value));
            return true;
        });

        summary.mapSummary.errors.iterator().each((key, error) => {
            log.error('Map Stage Error', `Key: ${key}, Error: ${error}`);
            return true;
        });

        sendSummaryEmail(successCount, errorList);
    };

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

Parallel Execution

Map and Reduce stages run in parallel across multiple processors:

💡 Concurrency Settings
By default, NetSuite uses up to 5 parallel processors. This can be configured in the deployment up to a maximum of 50 concurrent processes, depending on your account's service tier.
Check Concurrency in Summarize
const summarize = (summary) => {
    log.audit('Concurrency Used', {
        mapConcurrency: summary.mapSummary.concurrency,
        reduceConcurrency: summary.reduceSummary.concurrency
    });
};

Common Patterns

Pattern 1: Map Only (No Reduce)

When you don't need aggregation, skip the reduce stage:

Map Only Pattern
const map = (context) => {
    const data = JSON.parse(context.value);
    record.submitFields({
        type: record.Type.CUSTOMER,
        id: data.id,
        values: { 'custentity_processed': true }
    });
    // Don't call context.write() - skip reduce stage
};

const reduce = null;  // Or simply don't define it

Pattern 2: Using Script Parameters

Script Parameters in getInputData
const getInputData = () => {
    const script = runtime.getCurrentScript();
    const subsidiaryId = script.getParameter({ name: 'custscript_subsidiary' });
    const startDate = script.getParameter({ name: 'custscript_start_date' });

    return search.create({
        type: search.Type.INVOICE,
        filters: [
            ['subsidiary', 'anyof', subsidiaryId],
            ['trandate', 'onorafter', startDate],
            ['mainline', 'is', 'T']
        ],
        columns: ['entity', 'total']
    });
};

Pattern 3: Processing Large Files

CSV File Processing
const getInputData = () => {
    const script = runtime.getCurrentScript();
    const fileId = script.getParameter({ name: 'custscript_import_file' });

    const csvFile = file.load({ id: fileId });
    const contents = csvFile.getContents();
    const lines = contents.split('\n');

    return lines.slice(1).map((line, index) => ({
        lineNumber: index + 2,
        data: line
    }));
};

Pattern 4: Yield Point Recovery

Resume from Last Processed ID
const getInputData = () => {
    const script = runtime.getCurrentScript();
    if (script.getParameter({ name: 'custscript_resume_id' })) {
        return search.create({
            type: search.Type.SALES_ORDER,
            filters: [
                ['internalid', 'greaterthan', script.getParameter({ name: 'custscript_resume_id' })],
                ['mainline', 'is', 'T']
            ]
        });
    }
    return search.create({
        type: search.Type.SALES_ORDER,
        filters: [['mainline', 'is', 'T']]
    });
};

Error Handling

Comprehensive Error Handling
const map = (context) => {
    try {
        const data = JSON.parse(context.value);
        processRecord(data);
        context.write({ key: 'success', value: data.id });
    } catch (e) {
        log.error('Map Error', {
            key: context.key,
            error: e.message,
            stack: e.stack
        });
        context.write({
            key: 'error',
            value: JSON.stringify({ key: context.key, error: e.message })
        });
    }
};

const summarize = (summary) => {
    if (summary.inputSummary.error) {
        log.error('Input Error', summary.inputSummary.error);
        sendAlertEmail('getInputData failed', summary.inputSummary.error);
        return;
    }

    let mapErrorCount = 0;
    summary.mapSummary.errors.iterator().each((key, error) => {
        mapErrorCount++;
        log.error('Map Error', `Key ${key}: ${error}`);
        return true;
    });

    let reduceErrorCount = 0;
    summary.reduceSummary.errors.iterator().each((key, error) => {
        reduceErrorCount++;
        log.error('Reduce Error', `Key ${key}: ${error}`);
        return true;
    });

    log.audit('Error Summary', { mapErrors: mapErrorCount, reduceErrors: reduceErrorCount });
};

Performance Optimization

  1. Minimize getInputData Work: Return a search object rather than running it and returning results. Let NetSuite stream the data.
  2. Keep Map Functions Light: Each map invocation has only 1,000 units. Move heavy processing to reduce (5,000 units).
  3. Choose Keys Wisely: Keys determine grouping in reduce. Too few unique keys = underutilized parallelism. Too many = no aggregation benefit.
  4. Use submitFields Over record.save(): record.submitFields() uses 2 units vs 20+ for full record save.

Monitoring Progress

Check M/R Status from Another Script
const checkMapReduceStatus = (taskId) => {
    const status = task.checkStatus({ taskId });
    log.debug('M/R Status', {
        status: status.status,       // PENDING, PROCESSING, COMPLETE, FAILED
        stage: status.stage,         // GET_INPUT, MAP, REDUCE, SUMMARIZE
        percentComplete: status.getPercentageCompleted()
    });
    return status;
};

Industry Applications

👔 Professional Services
Professional Services Consideration
BOM Cost Rollup: Map: Calculate component costs. Reduce: Aggregate by assembly. Process thousands of assemblies in parallel to update standard costs.
👔 Professional Services
Professional Services Consideration
Mass Price Updates: Map: Calculate new prices per item. Reduce: Update items by vendor or category. Handle hundreds of thousands of items efficiently.
👔 Professional Services
Professional Services Consideration
Usage Billing: Map: Process usage events. Reduce: Aggregate by customer and billing period. Generate invoices for high-volume usage data.
👔 Professional Services
Professional Services Consideration
Time Entry Processing: Map: Validate time entries. Reduce: Aggregate by project/employee. Process month-end billing for thousands of consultants.
⚠️ Queue Priority
Map/Reduce Scripts share the same queue as Scheduled Scripts. During busy periods, scripts may wait in queue. Plan execution timing for large processes accordingly.

Map/Reduce Script Checklist

Chapter 7.6

Suitelets & Restlets

Build custom UI pages with Suitelets and RESTful APIs with Restlets for internal tools and external integrations.

Suitelets vs Restlets

Both script types create custom endpoints in NetSuite, but serve different purposes:

Aspect Suitelet Restlet
Primary UseCustom UI pagesRESTful API endpoints
AccessBrowser URL, iframes, linksHTTP clients, integrations
AuthenticationNetSuite login sessionOAuth, NLAuth, Token-Based
Response FormatHTML pages, files, JSONJSON, XML, plain text
Governance10,000 units5,000 units
HTTP MethodsGET, POSTGET, POST, PUT, DELETE

Suitelet Basics

Suitelets create custom pages within NetSuite, accessible via URL or as embedded content.

Suitelet Template
/**
 * @NApiVersion 2.1
 * @NScriptType Suitelet
 */
define(['N/ui/serverWidget', 'N/search', 'N/record', 'N/redirect'],
    (serverWidget, search, record, redirect) => {

    const onRequest = (context) => {
        if (context.request.method === 'GET') {
            showForm(context);
        } else {
            processSubmission(context);
        }
    };

    const showForm = (context) => {
        const form = serverWidget.createForm({ title: 'Custom Data Entry Form' });

        form.addField({
            id: 'custpage_customer',
            type: serverWidget.FieldType.SELECT,
            label: 'Customer',
            source: 'customer'
        });
        form.addField({
            id: 'custpage_memo',
            type: serverWidget.FieldType.TEXTAREA,
            label: 'Notes'
        });
        form.addField({
            id: 'custpage_amount',
            type: serverWidget.FieldType.CURRENCY,
            label: 'Amount'
        });
        form.addSubmitButton({ label: 'Save Record' });
        context.response.writePage(form);
    };

    const processSubmission = (context) => {
        const customerId = context.request.parameters.custpage_customer;
        const memo = context.request.parameters.custpage_memo;
        const amount = context.request.parameters.custpage_amount;

        const newRecord = record.create({ type: 'customrecord_my_record' });
        newRecord.setValue('custrecord_customer', customerId);
        newRecord.setValue('custrecord_memo', memo);
        newRecord.setValue('custrecord_amount', amount);
        const recordId = newRecord.save();

        redirect.toRecord({ type: 'customrecord_my_record', id: recordId });
    };

    return { onRequest };
});

Suitelet UI Components

The N/ui/serverWidget module provides rich UI building blocks:

Field Types

Available Field Types
// Text fields
form.addField({ id: 'name', type: serverWidget.FieldType.TEXT, label: 'Name' });
form.addField({ id: 'email', type: serverWidget.FieldType.EMAIL, label: 'Email' });
form.addField({ id: 'notes', type: serverWidget.FieldType.TEXTAREA, label: 'Notes' });
form.addField({ id: 'richtext', type: serverWidget.FieldType.RICHTEXT, label: 'Description' });

// Numeric fields
form.addField({ id: 'qty', type: serverWidget.FieldType.INTEGER, label: 'Quantity' });
form.addField({ id: 'rate', type: serverWidget.FieldType.FLOAT, label: 'Rate' });
form.addField({ id: 'amount', type: serverWidget.FieldType.CURRENCY, label: 'Amount' });
form.addField({ id: 'pct', type: serverWidget.FieldType.PERCENT, label: 'Discount %' });

// Date/Time fields
form.addField({ id: 'date', type: serverWidget.FieldType.DATE, label: 'Date' });
form.addField({ id: 'datetime', type: serverWidget.FieldType.DATETIMETZ, label: 'Date/Time' });

// Selection fields
form.addField({ id: 'customer', type: serverWidget.FieldType.SELECT, label: 'Customer', source: 'customer' });

// Other fields
form.addField({ id: 'check', type: serverWidget.FieldType.CHECKBOX, label: 'Active' });
form.addField({ id: 'file', type: serverWidget.FieldType.FILE, label: 'Upload File' });

Sublists

Adding Sublists to Suitelets
const sublist = form.addSublist({
    id: 'custpage_items',
    type: serverWidget.SublistType.INLINEEDITOR,
    label: 'Line Items'
});

sublist.addField({ id: 'item', type: serverWidget.FieldType.SELECT, label: 'Item', source: 'item' });
sublist.addField({ id: 'qty', type: serverWidget.FieldType.INTEGER, label: 'Quantity' });
sublist.addField({ id: 'rate', type: serverWidget.FieldType.CURRENCY, label: 'Rate' });

// Pre-populate sublist lines
sublist.setSublistValue({ id: 'item', line: 0, value: '123' });
sublist.setSublistValue({ id: 'qty', line: 0, value: '10' });

Tabs and Field Groups

Organizing Suitelet Layout
form.addTab({ id: 'maintab', label: 'Main Info' });
form.addTab({ id: 'detailstab', label: 'Details' });

form.addFieldGroup({ id: 'addressgroup', label: 'Address', tab: 'maintab' });

const cityField = form.addField({
    id: 'city',
    type: serverWidget.FieldType.TEXT,
    label: 'City',
    container: 'addressgroup'
});

Suitelet Access & Deployment

Generate Suitelet URLs programmatically:

URL Generation
const url = require('N/url');

// Internal URL (for logged-in users)
const internalUrl = url.resolveScript({
    scriptId: 'customscript_my_suitelet',
    deploymentId: 'customdeploy_my_suitelet',
    params: { customer: '123' }
});

// External URL (for external access)
const externalUrl = url.resolveScript({
    scriptId: 'customscript_my_suitelet',
    deploymentId: 'customdeploy_my_suitelet',
    params: { customer: '123' },
    returnExternalUrl: true
});
⚠️ Available Without Login
In the deployment record, checking "Available Without Login" makes the Suitelet publicly accessible. Use this carefully and implement your own authentication for sensitive data.

Restlet Basics

Restlets provide RESTful API endpoints for external integrations:

Restlet Template with All HTTP Methods
/**
 * @NApiVersion 2.1
 * @NScriptType Restlet
 */
define(['N/record', 'N/search', 'N/log'],
    (record, search, log) => {

    const get = (requestParams) => {
        const customerId = requestParams.id;
        if (!customerId) return { error: 'Customer ID required' };
        try {
            const customer = record.load({ type: record.Type.CUSTOMER, id: customerId });
            return {
                success: true,
                data: {
                    id: customer.id,
                    name: customer.getValue('companyname'),
                    email: customer.getValue('email'),
                    balance: customer.getValue('balance')
                }
            };
        } catch (e) {
            return { success: false, error: e.message };
        }
    };

    const post = (requestBody) => {
        try {
            const newCustomer = record.create({ type: record.Type.CUSTOMER });
            newCustomer.setValue('companyname', requestBody.name);
            newCustomer.setValue('email', requestBody.email);
            newCustomer.setValue('subsidiary', requestBody.subsidiary || 1);
            const customerId = newCustomer.save();
            return { success: true, id: customerId, message: 'Customer created' };
        } catch (e) {
            return { success: false, error: e.message };
        }
    };

    const put = (requestBody) => {
        if (!requestBody.id) return { error: 'Customer ID required for update' };
        try {
            const values = {};
            if (requestBody.name) values['companyname'] = requestBody.name;
            if (requestBody.email) values['email'] = requestBody.email;
            record.submitFields({ type: record.Type.CUSTOMER, id: requestBody.id, values });
            return { success: true, message: 'Customer updated' };
        } catch (e) {
            return { success: false, error: e.message };
        }
    };

    const doDelete = (requestParams) => {
        if (!requestParams.id) return { error: 'Record ID required' };
        try {
            record.delete({ type: 'customrecord_my_record', id: requestParams.id });
            return { success: true, message: 'Record deleted' };
        } catch (e) {
            return { success: false, error: e.message };
        }
    };

    return { get, post, put, delete: doDelete };
});

Restlet Authentication

Restlets require authentication. Common methods:

Method Use Case Header Format
Token-Based Auth (TBA)Recommended for productionOAuth 1.0 signature
OAuth 2.0Modern integrations, machine-to-machineBearer token
NLAuthLegacy/testing (not recommended)Account, email, password, role

Token-Based Authentication Setup

TBA Header Format
// Integration record settings:
// - Enable Token-Based Authentication
// - Create Access Token for user/role

// HTTP request headers for TBA:
// Authorization: OAuth realm="ACCOUNT_ID",
//   oauth_consumer_key="CONSUMER_KEY",
//   oauth_token="TOKEN_ID",
//   oauth_signature_method="HMAC-SHA256",
//   oauth_timestamp="TIMESTAMP",
//   oauth_nonce="NONCE",
//   oauth_version="1.0",
//   oauth_signature="SIGNATURE"

Calling Restlets

External and Internal Restlet Calls
// External: Calling from JavaScript
const restletUrl = 'https://ACCOUNT_ID.restlets.api.netsuite.com/app/site/hosting/restlet.nl';
const params = `?script=123&deploy=1&id=456`;

fetch(restletUrl + params, {
    method: 'GET',
    headers: {
        'Authorization': 'OAuth ...',
        'Content-Type': 'application/json'
    }
})
.then(response => response.json())
.then(data => console.log(data));

// Internal: From SuiteScript
const https = require('N/https');
const url = require('N/url');

const restletUrl = url.resolveScript({
    scriptId: 'customscript_my_restlet',
    deploymentId: 'customdeploy_my_restlet'
});

const response = https.post({
    url: restletUrl,
    body: JSON.stringify({ name: 'Test' }),
    headers: { 'Content-Type': 'application/json' }
});

Common Patterns

Pattern: Wizard-Style Suitelet

Multi-Step Suitelet
const onRequest = (context) => {
    const step = context.request.parameters.step || '1';
    switch (step) {
        case '1': renderStep1(context); break;
        case '2':
            if (context.request.method === 'POST') saveStep1Data(context);
            renderStep2(context);
            break;
        case '3':
            if (context.request.method === 'POST') {
                saveStep2Data(context);
                completeWizard(context);
            }
            break;
    }
};

Pattern: Bulk Action Restlet

Process Multiple Records
const post = (requestBody) => {
    const recordIds = requestBody.ids;
    const action = requestBody.action;
    const results = { success: [], failed: [] };

    recordIds.forEach(id => {
        try {
            if (action === 'approve') {
                record.submitFields({
                    type: 'salesorder', id: id,
                    values: { 'orderstatus': 'B' }
                });
            }
            results.success.push(id);
        } catch (e) {
            results.failed.push({ id, error: e.message });
        }
    });
    return results;
};

Industry Applications

👔 Professional Services
Professional Services Consideration
Customer Portal: Suitelets for self-service license management, usage dashboards, and support ticket submission. Restlets for product activation and usage reporting APIs.
👔 Professional Services
Professional Services Consideration
B2B Portal: Suitelet-based order entry forms for customers. Restlet APIs for real-time inventory checks and order status from partner systems.
👔 Professional Services
Professional Services Consideration
Shop Floor Integration: Restlets receiving production data from IoT devices and MES systems. Suitelets for work order kiosks on the floor.
👔 Professional Services
Professional Services Consideration
Time Entry Portal: Suitelet for consultants to enter time and expenses without full NetSuite access. Restlets for mobile app time submission.

Suitelet/Restlet Checklist

Chapter 7.7

Workflow Automation

Design and implement business process automation using SuiteFlow workflows without coding.

Workflow Fundamentals

SuiteFlow is NetSuite's visual workflow builder that automates business processes without coding. Workflows consist of states, transitions, and actions that execute based on triggers and conditions.

Workflows vs SuiteScript

Criteria Workflows (SuiteFlow) SuiteScript
Complexity Simple to moderate logic Complex, conditional logic
Maintenance Business users can modify Developers required
Debugging Workflow history visible Script logs and debugger
Performance Lightweight, efficient Full API access, more overhead
External Calls Limited (Send Email) Full HTTP/REST capabilities
Best For Approvals, notifications, field updates Integrations, complex calculations
Customization Workflow Workflows New

Workflow Components

Component Description Example
State A step in the workflow process Pending Approval, Manager Review, Approved
Transition Movement from one state to another Pending → Manager Review (on submit)
Action Work performed during a state Send email, set field, create record
Condition Logic that controls execution Amount > 1000 AND Department = Sales

Trigger Types

Trigger On When It Fires Common Use
Before Record Load Record opens in UI Set defaults, show/hide fields
Before Record Submit Before record saves Validation, calculated fields
After Record Submit After record saves Send emails, create related records
Scheduled On defined schedule Periodic updates, reminders
Entry When entering a state State-specific actions
Exit When leaving a state Cleanup, logging

Creating a Workflow

Customization Workflow Workflows New

Workflow Settings

Field Description Recommendation
NameWorkflow identifierUse format: [Record] - [Process] (e.g., "SO - Approval")
Record TypeRecord this workflow applies toCannot be changed after creation
Sub TypeTransaction subtype filterLeave blank for all subtypes
Release StatusNot Initiating, Testing, ReleasedUse Testing during development
InitiationEvent, Scheduled, or BothEvent is most common
Execute as AdminRun with full permissionsEnable for cross-department workflows
Keep Instance on Record UpdateContinue workflow on editsEnable for approval workflows
⚠️ Important Setting
"Keep Instance and Allow Re-initiate" — Enable this for approval workflows to ensure the workflow continues when records are edited during the approval process. Without this, editing a record cancels the workflow.

Workflow Actions

Field Actions

Action Purpose Trigger Options
Set Field ValueUpdate any field on the recordEntry, Exit, Before/After Submit
Set Field Display TypeMake field hidden, disabled, or normalBefore Load, Entry
Set Field Display LabelChange field label dynamicallyBefore Load, Entry
Set Field MandatoryMake field required or optionalBefore Load, Entry

Communication Actions

Action Purpose Key Settings
Send EmailSend email notificationsRecipient, template, attach record
Send Campaign EmailUse marketing templateCampaign, email template
Add NoteCreate note on recordTitle, note content

Record Actions

Action Purpose Notes
Create RecordCreate new related recordTask, phone call, event, any custom record
Transform RecordConvert to another record typeQuote → Sales Order, SO → Invoice
Remove ButtonHide UI buttonsBefore Load only
Lock RecordPrevent editingUseful for approved records

Advanced Actions

Action Purpose Use Case
Custom ActionExecute SuiteScriptComplex logic, external calls
Initiate WorkflowTrigger another workflowChained processes
Go to PageRedirect user to URLConfirmation pages
Return User ErrorShow error messageValidation failures

Approval Workflow Pattern

Approvals are the most common workflow use case. Here is the standard pattern:

States

A typical approval workflow moves through these states: DraftPending ApprovalManager Review (for higher amounts) → Approved, with a Rejected path available at each approval level.

State Configuration

State Entry Actions Exit Actions
Pending Approval Email approver, set approval status field None
Manager Review Email manager, create task for follow-up None
Approved Set status, email submitter, unlock record None
Rejected Set status, email submitter with reason None

Transition Conditions

Transition Trigger Condition
Draft → Pending Approval After Record Submit Status = Pending Approval
Pending → Manager Review After Record Submit Amount > $10,000
Pending → Approved After Record Submit Approval Status = Approved AND Amount ≤ $10,000
Manager Review → Approved After Record Submit Manager Approval = Approved
Any → Rejected After Record Submit Approval Status = Rejected
🎯 Approval Workflow Design Tips
  • Enable "Keep Instance on Record Update" so the workflow continues when records are edited during the approval process
  • Lock fields in approval states using Set Field Display Type actions to prevent changes during review
  • Include rejection reason fields so approvers can explain why they rejected
  • Add approval buttons using workflow button transitions for a clean user experience

Building Conditions

Condition Syntax

Operator Description Example
=Equals{status} = "Pending Approval"
!=Not equals{department} != "Executive"
> < >= <=Comparison{amount} > 1000
containsText contains{memo} contains "RUSH"
startswithText starts with{tranid} startswith "SO"
isemptyField has no value{custbody_approver} isempty
isnotemptyField has value{custbody_reason} isnotempty
AND / ORCombine conditions{amount} > 1000 AND {type} = "Invoice"

Common Condition Patterns

Condition Examples
// Approval threshold by department
({department} = "Engineering" AND {amount} > 5000) OR
({department} = "Marketing" AND {amount} > 2500) OR
({department} = "Sales" AND {amount} > 10000)

// Multi-level approval
{amount} > 10000 AND {custbody_mgr_approved} = T AND {custbody_dir_approved} != T

// Record change detection (use with Before Submit)
{amount} != {previousvalue.amount}

// Day of week (for scheduled workflows)
{today} = "Monday" OR {today} = "Wednesday"

Workflow Variables

Variables store values during workflow execution for use across states and actions.

Variable Types

Type Use Case Example
TextStore stringsRejection reason, notes
NumberStore numeric valuesOriginal amount, line count
DateStore datesApproval date, due date
Record ReferenceStore related record IDApprover employee record
BooleanTrue/False flagsHas been escalated

Setting Variables

Use "Set Field Value" action targeting workflow variables:

Variable Assignment
// In Set Field Value action
Field: [Workflow Variable] custworkflow_original_amount
Value: {total}
Value Type: Field

// Formula-based variable
Field: [Workflow Variable] custworkflow_days_pending
Value: ROUND(({today} - {datecreated}), 0)
Value Type: Formula

Email Notifications

Dynamic Email Content

Use merge fields in email templates:

Approval Email Template
Subject: ${record.type} ${record.tranid} Requires Your Approval

Body:
Dear ${record.salesrep.entityid},

A ${record.type} has been submitted for your approval:

Transaction: ${record.tranid}
Customer: ${record.entity}
Amount: ${record.total}
Submitted By: ${currentuser.firstname} ${currentuser.lastname}

Please review and approve: ${record.recordurl}

Recipient Options

Option Description Use Case
Specific RecipientFixed employee or emailAlways notify same person
Field on RecordEmployee field valueSales rep, project manager
RoleAll users with roleAll Administrators
GroupDistribution groupApproval committee
Current User's SupervisorManager hierarchyManager approvals

Industry Workflow Patterns

Manufacturing

  • Work Order Release: Engineering approval before production release
  • BOM Changes: Multi-level approval for bill of material modifications
  • Quality Hold: Automatic hold when QC failure recorded

Wholesale/Distribution

  • Credit Hold Release: Approval workflow when customer exceeds credit limit
  • Special Pricing: Margin approval for below-threshold pricing
  • Large Order Review: Manager notification for orders exceeding threshold

Professional Services

  • Time Entry Approval: Manager approval of weekly timesheets
  • Expense Report: Multi-tier approval based on amount
  • Project Budget Change: PM and sponsor approval required

Retail

  • Returns Authorization: Approval for returns over threshold
  • Price Override: Manager approval for discounts
  • Gift Card Refund: Finance approval required

Testing & Troubleshooting

Release Status Strategy

Status Behavior When to Use
Not InitiatingWorkflow never startsInitial development
TestingOnly initiates for Owner roleQA testing phase
ReleasedActive for all usersProduction use

Viewing Workflow History

Record System Information Workflow History subtab
  • Shows all states the record has passed through
  • Displays actions executed at each state
  • Includes timestamps and triggering users
  • Shows condition evaluation results

Common Issues

Issue Cause Solution
Workflow not startingWrong initiation conditionCheck base record filter and initiation settings
Transition not firingCondition not metReview condition logic, check field values
Email not sendingEmpty recipientEnsure field-based recipient has value
Multiple instancesRe-initiation allowedAdjust "Allow Re-initiate" setting
Workflow canceled on edit"Keep Instance" not enabledEnable "Keep Instance on Record Update"

Workflow Automation Checklist

Workflow Configuration Checklist

Chapter 7.8

SuiteFlow Advanced

Master advanced workflow patterns including multi-level approvals, conditional routing, and workflow actions.

Custom Workflow Actions

Custom actions extend workflows by executing SuiteScript code when standard actions aren't sufficient.

When to Use Custom Actions

  • Complex calculations requiring multiple field values
  • External API calls (web services, third-party integrations)
  • Advanced record creation with sublist lines
  • Cross-record lookups and updates
  • Custom validation logic

Custom Action Script Template

Workflow Action Script
/**
 * @NApiVersion 2.1
 * @NScriptType WorkflowActionScript
 */
define(['N/record', 'N/search', 'N/runtime', 'N/log'],
    (record, search, runtime, log) => {

    const onAction = (context) => {
        try {
            const rec = context.newRecord;
            const customerId = rec.getValue('entity');

            const creditLimit = lookupCreditLimit(customerId);
            const orderTotal = rec.getValue('total');

            if (orderTotal > creditLimit) {
                return 'OVER_LIMIT';
            }
            return 'APPROVED';
        } catch (e) {
            log.error('Workflow Action Error', e.message);
            return 'ERROR';
        }
    };

    const lookupCreditLimit = (customerId) => {
        const customerFields = search.lookupFields({
            type: search.Type.CUSTOMER,
            id: customerId,
            columns: ['creditlimit']
        });
        return parseFloat(customerFields.creditlimit) || 0;
    };

    return { onAction };
});

Deploying Custom Actions

Setting Value Notes
Script TypeWorkflow ActionAppears in workflow action dropdown
Deployment StatusReleasedMust be released to use in workflows
Execute As RoleAdministratorUse elevated role for cross-record access
Log LevelDebugSet to Error in production

Sublist Processing in Workflows

Workflows can process line items through scheduled triggers and custom actions.

Sublist Iteration Pattern

Custom Action to Process Item Lines
const onAction = (context) => {
    const rec = context.newRecord;
    const lineCount = rec.getLineCount({ sublistId: 'item' });

    let hasBackorder = false;
    let totalWeight = 0;

    for (let i = 0; i < lineCount; i++) {
        const qty = rec.getSublistValue({
            sublistId: 'item', fieldId: 'quantity', line: i
        });
        const qtyAvailable = rec.getSublistValue({
            sublistId: 'item', fieldId: 'quantityavailable', line: i
        });
        const weight = rec.getSublistValue({
            sublistId: 'item', fieldId: 'weight', line: i
        }) || 0;

        if (qty > qtyAvailable) hasBackorder = true;
        totalWeight += (weight * qty);
    }

    return hasBackorder ? 'BACKORDER' : 'AVAILABLE';
};

Sublist Modification in Custom Actions

Add Line Items via Custom Action
// NOTE: Must load record in dynamic mode
const onAction = (context) => {
    const recId = context.newRecord.id;
    const recType = context.newRecord.type;

    const rec = record.load({ type: recType, id: recId, isDynamic: true });

    rec.selectNewLine({ sublistId: 'item' });
    rec.setCurrentSublistValue({
        sublistId: 'item', fieldId: 'item', value: 123
    });
    rec.setCurrentSublistValue({
        sublistId: 'item', fieldId: 'quantity', value: 1
    });
    rec.commitLine({ sublistId: 'item' });
    rec.save();

    return 'LINE_ADDED';
};
⚠️ Sublist Limitations
Standard workflow actions (Set Field Value) cannot modify sublist fields directly. Use custom actions for any sublist modifications. The record must be loaded in dynamic mode for line operations.

Scheduled Workflows

Scheduled workflows execute on a defined schedule for records matching specified criteria.

Scheduled Workflow Settings

Setting Options Use Case
Initiation TypeScheduled (or Both)Enable scheduled execution
Repeat TypeDaily, Weekly, Monthly, YearlyFrequency of execution
Time of DaySpecific timeWhen to run daily
Day of WeekSun-Sat selectionsWhich days to run weekly
Day of Month1-31 or LastWhich day to run monthly

Common Scheduled Workflow Patterns

Scheduled Workflow Examples
// 1. Overdue Invoice Reminders
//    Schedule: Daily at 8:00 AM
//    Record: Invoice
//    Condition: {duedate} < {today} AND {status} != "Paid In Full"
//    Action: Send Email to customer contact

// 2. Stale Quote Follow-up
//    Schedule: Weekly on Monday
//    Record: Estimate
//    Condition: {trandate} < ({today} - 14) AND {status} = "Open"
//    Action: Create Task for sales rep

// 3. Month-end Accrual
//    Schedule: Monthly on Last Day
//    Record: Journal Entry
//    Condition: {custbody_accrual_type} = "Auto"
//    Action: Custom action to create reversal

// 4. Anniversary Notifications
//    Schedule: Daily
//    Record: Customer
//    Condition: MONTH({custentity_anniversary}) = MONTH({today})
//    Action: Send Campaign Email

Workflow Buttons

Add custom buttons to records that trigger workflow transitions.

Creating Workflow Buttons

  1. Create a state with desired button action
  2. Add transition from that state
  3. Set transition trigger to "Button"
  4. Configure button label and conditions

Button Configuration

Property Description Example
Button LabelText displayed on button"Approve", "Submit for Review"
Save Record FirstSave before transitionEnable for data capture
Button ConditionWhen to show buttonUser role, field values

Button Visibility by Role

Button Condition Examples
// Show "Approve" button only to managers
Condition: {role} = "Manager" OR {role} = "Administrator"

// Show "Escalate" button when pending > 3 days
Condition: ({today} - {custbody_submit_date}) > 3

// Show "Override" button for specific users
Condition: {currentuser.id} = 123 OR {currentuser.id} = 456

Workflow-Script Integration

Workflows and scripts can work together for comprehensive automation.

Initiating Workflows from SuiteScript

Trigger Workflow from User Event Script
define(['N/workflow'],
    (workflow) => {

    const afterSubmit = (context) => {
        if (context.type === context.UserEventType.CREATE) {
            workflow.initiate({
                recordType: context.newRecord.type,
                recordId: context.newRecord.id,
                workflowId: 'customworkflow_approval'
            });
        }
    };

    return { afterSubmit };
});

Triggering Workflow Transitions

Trigger Transition from Script
define(['N/workflow'],
    (workflow) => {

    const triggerApproval = (recordType, recordId) => {
        workflow.trigger({
            recordType: recordType,
            recordId: recordId,
            workflowId: 'customworkflow_approval',
            actionId: 'customworkflowaction_approve'
        });
    };

    return { triggerApproval };
});

Reading Workflow State in Scripts

Check Current Workflow State
define(['N/search'],
    (search) => {

    const getWorkflowState = (recordType, recordId, workflowId) => {
        const results = search.create({
            type: 'workflowinstance',
            filters: [
                ['record', 'is', recordId],
                'AND',
                ['recordtype', 'is', recordType],
                'AND',
                ['workflow', 'is', workflowId]
            ],
            columns: ['state', 'datecreated']
        }).run().getRange({ start: 0, end: 1 });

        if (results.length > 0) {
            return results[0].getValue('state');
        }
        return null;
    };

    return { getWorkflowState };
});

Parallel & Sequential Approvals

Sequential Approval Pattern

Submit → Manager Approval → Director Approval → VP Approval → Approved (with rejection paths at each level).

ℹ️ Threshold Transitions
Manager → Director: Amount > $10,000 | Director → VP: Amount > $50,000

Parallel Approval Pattern

Submit → Finance Approval + Legal Approval + Ops Approval → All Approved (when all approvals complete).

Parallel Approval Implementation
// Implementation using checkbox fields
Transition to "All Approved" when:
{custbody_finance_approved} = T AND
{custbody_legal_approved} = T AND
{custbody_ops_approved} = T
ℹ️ Parallel Approval Strategy
NetSuite workflows don't have native parallel states. Implement parallel approvals using custom checkbox fields for each approver. All approvers can edit the record to set their approval field, and a transition fires when all fields are checked.

Escalation & Delegation

Time-Based Escalation

Escalation Workflow (Scheduled)
// Scheduled workflow for escalation
// Runs daily at 9:00 AM

// Record: Sales Order
// Condition:
//   {custbody_approval_status} = "Pending" AND
//   ({today} - {custbody_submit_date}) > 3 AND
//   {custbody_escalated} != T

// Actions:
// 1. Set Field: custbody_escalated = T
// 2. Set Field: custbody_approver = {custbody_original_approver.supervisor}
// 3. Send Email: To new approver about escalation
// 4. Add Note: "Approval escalated due to 3-day SLA breach"

Delegation Configuration

Delegation Type Implementation Use Case
Permanent DelegateCustom field on employeeExecutive assistants
Temporary DelegateCustom record with date rangeVacation coverage
Category DelegateMultiple delegate fields by typeDifferent delegates for PO vs SO

Delegation Lookup Script

Custom Action to Find Delegate
const onAction = (context) => {
    const rec = context.newRecord;
    const originalApprover = rec.getValue('custbody_approver');

    const delegateSearch = search.create({
        type: 'customrecord_delegation',
        filters: [
            ['custrecord_delegate_from', 'is', originalApprover],
            'AND',
            ['custrecord_delegate_start', 'onorbefore', 'today'],
            'AND',
            ['custrecord_delegate_end', 'onorafter', 'today'],
            'AND',
            ['isinactive', 'is', false]
        ],
        columns: ['custrecord_delegate_to']
    }).run().getRange({ start: 0, end: 1 });

    if (delegateSearch.length > 0) {
        return delegateSearch[0].getValue('custrecord_delegate_to');
    }
    return originalApprover;
};

Advanced Best Practices

Performance Optimization

Practice Reason
Use specific initiation conditionsPrevents unnecessary workflow instances
Minimize states with multiple actionsReduces processing time per transition
Avoid lookups in conditionsUse stored field values instead
Use After Submit for emailsDoesn't block user save operation
Limit custom action complexityKeep under 1000 governance units

Governance in Custom Actions

Action Trigger Governance Units Notes
Before Record Load1,000UI display context
Before Record Submit1,000Validation context
After Record Submit1,000Post-save processing
Scheduled1,000Per record processed
💡 Workflow Debugging

Use the Workflow History tab on records to see workflow execution details. Navigate to: Record > Workflow > View History. This shows states entered, actions executed, and any errors.

SuiteFlow Advanced Checklist

Advanced Workflow Checklist

Chapter 7.9

Formula Fields & Expressions

Leverage formulas in saved searches, workflows, and custom fields for calculations without SuiteScript.

Formula Fundamentals

NetSuite formulas use SQL-like syntax for calculations in custom fields, saved searches, and workflows. Understanding formula context is critical for correct results.

Formula Contexts

Context Field Reference Example
Custom Field (Default){fieldid}{total} * 0.1
Saved Search{fieldid} or joined {table.fieldid}{customer.creditlimit}
Workflow Condition{fieldid}{amount} > 1000
Workflow Set Field{fieldid}{total} - {amountpaid}
Advanced PDF/HTML${fieldid}${record.total}

Formula Return Types

Type Use For Example
Formula (Text)String results, concatenation{firstname} || ' ' || {lastname}
Formula (Numeric)Calculations, counts{quantity} * {rate}
Formula (Date)Date calculations{trandate} + 30
Formula (Currency)Money values with currency format{amount} * 1.1
Formula (Percent)Percentage values{amountpaid} / {total}

Text Functions

String Manipulation

Function Description Example
|| (Concatenate)Combine strings{firstname} || ' ' || {lastname}
UPPER()Convert to uppercaseUPPER({companyname})
LOWER()Convert to lowercaseLOWER({email})
INITCAP()Title caseINITCAP({city})
SUBSTR()Extract portion of stringSUBSTR({phone}, 1, 3)
LENGTH()String lengthLENGTH({memo})
TRIM()Remove leading/trailing spacesTRIM({address})
REPLACE()Replace substringREPLACE({phone}, '-', '')
INSTR()Find position of substringINSTR({email}, '@')
LPAD() / RPAD()Pad string to lengthLPAD({tranid}, 10, '0')

Common Text Patterns

Text Formula Examples
// Extract domain from email
SUBSTR({email}, INSTR({email}, '@') + 1)

// Format phone as (XXX) XXX-XXXX
'(' || SUBSTR({phone}, 1, 3) || ') ' ||
SUBSTR({phone}, 4, 3) || '-' || SUBSTR({phone}, 7, 4)

// First initial + last name
SUBSTR({firstname}, 1, 1) || '. ' || {lastname}

// Handle null values
NVL({altname}, {entityid})

Numeric Functions

Math Functions

Function Description Example
ROUND()Round to decimal placesROUND({amount}, 2)
TRUNC()Truncate decimal placesTRUNC({rate}, 0)
CEIL()Round up to integerCEIL({quantity})
FLOOR()Round down to integerFLOOR({hours})
ABS()Absolute valueABS({variance})
MOD()Modulo (remainder)MOD({quantity}, 12)
POWER()Raise to powerPOWER(1 + {rate}, {periods})
GREATEST()Maximum of valuesGREATEST({qty1}, {qty2}, 0)
LEAST()Minimum of valuesLEAST({available}, {ordered})

Common Numeric Patterns

Numeric Formula Examples
// Margin calculation
ROUND(({amount} - {costestimate}) / NULLIF({amount}, 0) * 100, 2)

// Prevent division by zero
{numerator} / NULLIF({denominator}, 0)

// Quantity in cases (12 per case)
FLOOR({quantity} / 12) || ' cases, ' || MOD({quantity}, 12) || ' units'

// Weighted average
SUM({quantity} * {rate}) / NULLIF(SUM({quantity}), 0)

// Round to nearest 5
ROUND({amount} / 5) * 5

Date Functions

Date Manipulation

Function Description Example
SYSDATECurrent date/timeSYSDATE
TRUNC(date)Remove time portionTRUNC(SYSDATE)
ADD_MONTHS()Add/subtract monthsADD_MONTHS({startdate}, 3)
MONTHS_BETWEEN()Months between datesMONTHS_BETWEEN(SYSDATE, {startdate})
LAST_DAY()Last day of monthLAST_DAY({trandate})
NEXT_DAY()Next occurrence of dayNEXT_DAY({trandate}, 'MONDAY')
EXTRACT()Extract date componentEXTRACT(YEAR FROM {trandate})
TO_DATE()Convert string to dateTO_DATE('2024-01-01', 'YYYY-MM-DD')
TO_CHAR()Format date as stringTO_CHAR({trandate}, 'Mon DD, YYYY')

Date Format Codes

Code Description Example Output
YYYY4-digit year2024
MMMonth (01-12)03
MonMonth abbreviationMar
DDDay of month15
DYDay abbreviationFri
HH24Hour (00-23)14
MIMinutes30
QQuarter1
WWWeek of year11

Common Date Patterns

Date Formula Examples
// Days until due
{duedate} - TRUNC(SYSDATE)

// Days overdue (positive if past due)
TRUNC(SYSDATE) - {duedate}

// Age in years
FLOOR(MONTHS_BETWEEN(SYSDATE, {birthdate}) / 12)

// First day of current month
TRUNC(SYSDATE, 'MM')

// Last day of previous month
TRUNC(SYSDATE, 'MM') - 1

// Quarter name
'Q' || TO_CHAR({trandate}, 'Q') || ' ' || TO_CHAR({trandate}, 'YYYY')

Conditional Logic

CASE Expressions

CASE Statement Examples
// Simple CASE (compare to specific values)
CASE {status}
    WHEN 'Pending Fulfillment' THEN 'In Progress'
    WHEN 'Fulfilled' THEN 'Complete'
    WHEN 'Cancelled' THEN 'Void'
    ELSE 'Unknown'
END

// Searched CASE (evaluate conditions)
CASE
    WHEN {amount} >= 10000 THEN 'Large'
    WHEN {amount} >= 1000 THEN 'Medium'
    WHEN {amount} > 0 THEN 'Small'
    ELSE 'Zero'
END

// Nested CASE for complex logic
CASE
    WHEN {type} = 'Customer' THEN
        CASE
            WHEN {balance} > {creditlimit} THEN 'Over Credit'
            WHEN {balance} > {creditlimit} * 0.8 THEN 'Near Limit'
            ELSE 'Good Standing'
        END
    ELSE 'N/A'
END

NULL Handling Functions

Function Description Example
NVL()Replace null with valueNVL({memo}, 'No memo')
NVL2()Different values for null/not nullNVL2({email}, 'Has Email', 'No Email')
NULLIF()Return null if values equalNULLIF({quantity}, 0)
COALESCE()First non-null valueCOALESCE({phone}, {altphone}, 'N/A')
DECODE()IF-THEN-ELSE shorthandDECODE({type}, 'A', 1, 'B', 2, 0)

DECODE Function

DECODE Examples
// DECODE syntax: DECODE(expression, search1, result1, search2, result2, ..., default)

// Map status codes to labels
DECODE({custbody_status},
    1, 'New',
    2, 'In Progress',
    3, 'Completed',
    4, 'On Hold',
    'Unknown')

Aggregate Functions

Used in saved searches with Summary type columns.

Standard Aggregates

Function Description Example
SUM()Total of valuesSUM({amount})
COUNT()Count of recordsCOUNT({internalid})
COUNT(DISTINCT)Count of unique valuesCOUNT(DISTINCT {customer})
AVG()Average valueAVG({rate})
MIN()Minimum valueMIN({trandate})
MAX()Maximum valueMAX({amount})

Aggregate Formula Patterns

Aggregate Formula Examples
// Weighted average rate
SUM({quantity} * {rate}) / NULLIF(SUM({quantity}), 0)

// Count with condition
SUM(CASE WHEN {status} = 'Overdue' THEN 1 ELSE 0 END)

// Percentage of total
{amount} / SUM({amount}) * 100

// Conditional sum
SUM(CASE WHEN {type} = 'Invoice' THEN {amount} ELSE 0 END)

Saved Search Formula Patterns

Join Field Access

Accessing Related Record Fields
// Access parent record fields
{customer.companyname}
{customer.salesrep}
{item.displayname}

// Access custom fields on joined records
{customer.custentity_region}
{item.custitem_category}

// Multiple levels of joins
{customer.salesrep.email}

Common Search Formulas

Practical Search Formulas
// Days since last activity
TRUNC(SYSDATE) - {lastmodifieddate}

// Full address in one column
{shipaddress1} ||
NVL2({shipaddress2}, CHR(10) || {shipaddress2}, '') ||
CHR(10) || {shipcity} || ', ' || {shipstate} || ' ' || {shipzip}

// Age bucket
CASE
    WHEN TRUNC(SYSDATE) - {duedate} <= 0 THEN 'Current'
    WHEN TRUNC(SYSDATE) - {duedate} <= 30 THEN '1-30 Days'
    WHEN TRUNC(SYSDATE) - {duedate} <= 60 THEN '31-60 Days'
    WHEN TRUNC(SYSDATE) - {duedate} <= 90 THEN '61-90 Days'
    ELSE 'Over 90 Days'
END

Industry Formula Applications

Manufacturing

Manufacturing Formulas
// Scrap percentage
ROUND(({quantityscrapped} / NULLIF({quantity}, 0)) * 100, 2) || '%'

// Production efficiency
ROUND(({quantitycompleted} / NULLIF({quantityplanned}, 0)) * 100, 1)

// Yield rate
ROUND(({quantitycompleted} / NULLIF({quantitystarted}, 0)) * 100, 2)

Wholesale/Distribution

Wholesale Formulas
// Days of supply
{quantityonhand} / NULLIF({averagedailyusage}, 0)

// Reorder flag
CASE WHEN {quantityonhand} <= {reorderpoint} THEN 'REORDER' ELSE '' END

// Margin percentage
ROUND(({rate} - {costestimate}) / NULLIF({rate}, 0) * 100, 2)

Professional Services

Services Formulas
// Utilization rate
ROUND(({billablehours} / NULLIF({availablehours}, 0)) * 100, 1) || '%'

// Budget burn rate
ROUND(({actualcost} / NULLIF({budgetedcost}, 0)) * 100, 1)

// Project health
CASE
    WHEN {percentcomplete} >= {percenttimecomplete} THEN 'On Track'
    WHEN {percentcomplete} >= {percenttimecomplete} * 0.8 THEN 'At Risk'
    ELSE 'Behind'
END

Retail

Retail Formulas
// Average transaction value
{totalrevenue} / NULLIF({transactioncount}, 0)

// Discount percentage
ROUND((1 - ({actualprice} / NULLIF({listprice}, 0))) * 100, 1) || '%'

// Sell-through rate
ROUND(({quantitysold} / NULLIF({quantityreceived}, 0)) * 100, 1)

Formula Troubleshooting

Common Errors

Error Cause Solution
Invalid ExpressionSyntax error in formulaCheck parentheses, quotes, field names
Field Not FoundWrong field ID or unavailable joinVerify field ID in Records Browser
Type MismatchMixing incompatible typesUse TO_CHAR, TO_NUMBER, TO_DATE conversions
Division by ZeroDenominator can be zeroUse NULLIF({field}, 0) in denominator
Blank ResultsNULL values in calculationUse NVL() to handle nulls
Incorrect ResultsWrong formula contextCheck if summary function needed

Debugging Tips

  • Break down complex formulas — Test each part separately
  • Use Records Browser — Verify exact field IDs
  • Check data types — Ensure field types match expected formula types
  • Test with simple values — Use literals first, then replace with fields
  • Watch for null propagation — Any operation with NULL returns NULL
💡 Formula Performance

Formulas are calculated at runtime. Complex formulas on large result sets can slow searches significantly. For performance-critical reports, consider pre-calculating values via scheduled scripts and storing in custom fields.

Formula Fields Checklist

Formula Implementation Checklist