← Hub SuiteScript Patterns & Templates
v10.0
SuiteScript Patterns
Brand System v10.0 — SuiteScript Development
SuiteScript
Patterns &
Templates
Script types, code patterns, shared libraries, and deployment guides.
Production-ready SuiteScript 2.1 code patterns for Global Food Solutions NetSuite customizations. Every pattern follows GFS naming conventions, governance best practices, and food industry compliance requirements.
Client Scripts User Events Suitelets RESTlets Map/Reduce Library
Document ID GFS-2026-SSPT
Region Edgewood / Hermitage
Status Active
Version 9.0
Date 05 / 19 / 2026
Classification Developer Reference
Platform SuiteScript 2.1 / SS2.x
Owner Global Food Solutions, Inc.
02 Script Types Overview 9 script types with use cases, execution context, and entry points
Client Script
Browser
When to Use
Field validation, UI show/hide, cascading selects, real-time calculations, save validation
Context
Runs in browser. Affects UI responsiveness. No governance limits.
Entry Points
pageInit fieldChanged postSourcing sublistChanged lineInit validateField validateLine validateInsert validateDelete saveRecord
User Event
Server
When to Use
Set defaults, validate before save, create related records, send emails, integration calls
Context
Runs on server before/after load and submit. 1,000 governance units.
Entry Points
beforeLoad beforeSubmit afterSubmit
Suitelet
UI / Page
When to Use
Custom forms, dashboards, wizards, file downloads, branded pages, portals
Context
Serves custom pages via URL. 1,000 governance units per request.
Entry Points
onRequest (GET and POST)
RESTlet
API
When to Use
External integrations, mobile apps, third-party data sync, webhook receivers
Context
RESTful HTTP endpoints. TBA or NLAuth authentication. 5,000 governance units.
Entry Points
get post put delete
Scheduled Script
Background
When to Use
Daily jobs, batch processing, report generation, data sync, cleanup tasks
Context
Runs on schedule or on-demand. 10,000 governance units. Single-threaded.
Entry Points
execute
Map/Reduce
Parallel
When to Use
High-volume processing, parallel execution, mass updates, statement generation
Context
Multi-phase parallel processing. 10,000 units per phase. Auto-retry on errors.
Entry Points
getInputData map reduce summarize
Workflow Action
Workflow
When to Use
Custom logic within SuiteFlow workflows, complex conditions, external API calls from workflows
Context
Runs within workflow transitions. 1,000 governance units.
Entry Points
onAction
Mass Update
Bulk
When to Use
Bulk field updates triggered from saved search results, data cleanup, mass status changes
Context
Processes records from saved search. 1,000 governance units per record.
Entry Points
each
Portlet
Dashboard
When to Use
Dashboard widgets, KPI displays, quick-action panels, at-a-glance summaries
Context
Renders within NetSuite dashboard. 1,000 governance units.
Entry Points
render
■ All GFS scripts use SuiteScript 2.1 (ES2015+ syntax). Legacy SS1.0 scripts are being migrated. New scripts must never use SS1.0 APIs.
03 Client Script Patterns Field validation, cascading selects, dynamic show/hide, calculated fields
Field Validation — Temperature Range
Client Script
Validates that storage temperature fields stay within the required 33-41F range for cold chain compliance. Shows inline error if out of range.
/**
 * @NApiVersion 2.1
 * @NScriptType ClientScript
 * @NModuleScope SameAccount
 * @description GFS temperature range validation for item records
 */
define(['N/ui/message'], (message) => {

  const validateField = (context) => {
    const { fieldId, currentRecord } = context;

    if (fieldId === 'custitem_gfs_storage_temp') {
      const temp = currentRecord.getValue({ fieldId });
      if (temp && (temp < 33 || temp > 41)) {
        const msg = message.create({
          type: message.Type.ERROR,
          title: 'Temperature Out of Range',
          message: `Storage temp must be 33-41F. Current: ${temp}F`
        });
        msg.show({ duration: 5000 });
        return false;
      }
    }
    return true;
  };

  return { validateField };
});
Cascading Selects — Vendor → Items
Client Script
When a vendor is selected on a purchase order, filters the item dropdown to only show items sourced from that vendor. Prevents mismatched vendor/item combinations.
/**
 * @NApiVersion 2.1
 * @NScriptType ClientScript
 */
define(['N/search', 'N/currentRecord'], (search, cr) => {

  const fieldChanged = (context) => {
    const { fieldId, currentRecord } = context;

    if (fieldId === 'entity') {
      const vendorId = currentRecord.getValue({ fieldId: 'entity' });
      if (!vendorId) return;

      // Search for vendor-sourced items
      const itemSearch = search.create({
        type: 'item',
        filters: [
          ['vendor', 'anyof', vendorId],
          'AND',
          ['isinactive', 'is', 'F']
        ],
        columns: ['internalid', 'itemid', 'displayname']
      });

      const validItems = [];
      itemSearch.run().each((result) => {
        validItems.push(result.getValue('internalid'));
        return true;
      });

      // Store for line validation
      currentRecord.setValue({
        fieldId: 'custbody_gfs_valid_items',
        value: JSON.stringify(validItems)
      });
    }
  };

  return { fieldChanged };
});
Dynamic Show/Hide — Hazmat Fields
Client Script
Shows or hides hazardous material fields based on the item classification. When an item is flagged as requiring special handling, additional fields for UN number, packing group, and hazard class appear.
/**
 * @NApiVersion 2.1
 * @NScriptType ClientScript
 */
define([], () => {

  const HAZMAT_FIELDS = [
    'custitem_gfs_un_number',
    'custitem_gfs_packing_group',
    'custitem_gfs_hazard_class',
    'custitem_gfs_sds_url'
  ];

  const toggleHazmatFields = (currentRecord) => {
    const isHazmat = currentRecord.getValue({
      fieldId: 'custitem_gfs_special_handling'
    });

    HAZMAT_FIELDS.forEach(fieldId => {
      const field = currentRecord.getField({ fieldId });
      if (field) {
        field.isDisplay = !!isHazmat;
        field.isMandatory = !!isHazmat;
      }
    });
  };

  const pageInit = (context) => toggleHazmatFields(context.currentRecord);

  const fieldChanged = (context) => {
    if (context.fieldId === 'custitem_gfs_special_handling') {
      toggleHazmatFields(context.currentRecord);
    }
  };

  return { pageInit, fieldChanged };
});
Save Record Validation — PO Required Fields
Client Script
Validates all required GFS-specific fields before a Purchase Order can be saved. Checks for delivery date, department (brand), warehouse location, and at least one line item.
/**
 * @NApiVersion 2.1
 * @NScriptType ClientScript
 */
define(['N/ui/dialog'], (dialog) => {

  const saveRecord = (context) => {
    const rec = context.currentRecord;
    const errors = [];

    if (!rec.getValue('trandate'))
      errors.push('Transaction date is required');
    if (!rec.getValue('department'))
      errors.push('Brand (department) is required');
    if (!rec.getValue('location'))
      errors.push('Warehouse location is required');
    if (!rec.getValue('custbody_gfs_delivery_date'))
      errors.push('Expected delivery date is required');
    if (rec.getLineCount({ sublistId: 'item' }) === 0)
      errors.push('At least one line item is required');

    if (errors.length > 0) {
      dialog.alert({
        title: 'GFS Validation',
        message: errors.join('\n')
      });
      return false;
    }
    return true;
  };

  return { saveRecord };
});
■ Client scripts run in the browser with no governance limits but affect page performance. Keep API calls minimal; use postSourcing for sourced fields.
04 User Event Patterns Before load, before submit, after submit patterns for record lifecycle hooks
Before Load — Set Defaults & Hide Fields
User Event
Sets default values on new Sales Orders and hides internal fields from non-admin roles. Automatically populates warehouse location based on customer's shipping state.
/**
 * @NApiVersion 2.1
 * @NScriptType UserEventScript
 * @NModuleScope SameAccount
 */
define(['N/runtime', 'N/record'], (runtime, record) => {

  const beforeLoad = (context) => {
    if (context.type !== context.UserEventType.CREATE) return;

    const { newRecord, form } = context;
    const role = runtime.getCurrentUser().role;

    // Set warehouse default based on customer region
    const custId = newRecord.getValue('entity');
    if (custId) {
      const custRec = record.load({ type: 'customer', id: custId });
      const state = custRec.getValue('shipstate');
      const warehouse = getWarehouseByState(state);
      newRecord.setValue({ fieldId: 'location', value: warehouse });
    }

    // Hide internal fields from non-admin roles
    if (role !== 3) { // 3 = Administrator
      ['custbody_gfs_internal_notes', 'custbody_gfs_margin_pct']
        .forEach(id => {
          const field = form.getField({ id });
          if (field) field.updateDisplayType({
            displayType: 'hidden'
          });
        });
    }
  };

  const getWarehouseByState = (state) => {
    const eastStates = ['NY','NJ','CT','PA','MA','ME','NH','VT','RI','DE','MD'];
    // Edgewood = 1, Hermitage = 2
    return eastStates.includes(state) ? 1 : 2;
  };

  return { beforeLoad };
});
Before Submit — Validate & Transform
User Event
Validates allergen declarations on item records before saving. Ensures all required allergen fields are populated when the item contains any of the Big 9 allergens.
/**
 * @NApiVersion 2.1
 * @NScriptType UserEventScript
 */
define(['N/error'], (error) => {

  const BIG_9 = [
    'milk', 'eggs', 'fish', 'shellfish', 'tree_nuts',
    'peanuts', 'wheat', 'soybeans', 'sesame'
  ];

  const beforeSubmit = (context) => {
    if (context.type === context.UserEventType.DELETE) return;

    const rec = context.newRecord;
    const hasAllergen = BIG_9.some(a =>
      rec.getValue({ fieldId: `custitem_gfs_allergen_${a}` })
    );

    if (hasAllergen) {
      const statement = rec.getValue('custitem_gfs_allergen_statement');
      if (!statement) {
        throw error.create({
          name: 'GFS_ALLERGEN_REQUIRED',
          message: 'Allergen contains statement is required when ' +
            'any Big 9 allergen is flagged. Update the allergen ' +
            'statement field before saving.',
          notifyOff: false
        });
      }

      // Auto-generate allergen summary for labels
      const active = BIG_9.filter(a =>
        rec.getValue({ fieldId: `custitem_gfs_allergen_${a}` })
      );
      rec.setValue({
        fieldId: 'custitem_gfs_allergen_summary',
        value: `Contains: ${active.join(', ').toUpperCase()}`
      });
    }
  };

  return { beforeSubmit };
});
After Submit — Create Related Records & Notify
User Event
After a Sales Order is approved, creates an item fulfillment task, sends confirmation email to customer, and logs the event to the GFS audit trail.
/**
 * @NApiVersion 2.1
 * @NScriptType UserEventScript
 */
define(['N/record', 'N/email', 'N/render', 'N/runtime', 'N/log'],
  (record, email, render, runtime, log) => {

  const afterSubmit = (context) => {
    if (context.type !== context.UserEventType.EDIT) return;

    const rec = record.load({
      type: context.newRecord.type,
      id: context.newRecord.id
    });
    const status = rec.getValue('orderstatus');

    // Only fire on transition to Approved (B)
    if (status !== 'B') return;
    const oldStatus = context.oldRecord.getValue('orderstatus');
    if (oldStatus === 'B') return;

    // 1. Send order confirmation email
    const custId = rec.getValue('entity');
    const template = render.mergeEmail({
      templateId: 'custtemplate_gfs_order_confirm',
      entity: { type: 'customer', id: custId },
      transactionId: rec.id
    });
    email.send({
      author: runtime.getCurrentUser().id,
      recipients: custId,
      subject: template.subject,
      body: template.body
    });

    // 2. Create warehouse task
    const task = record.create({ type: 'task' });
    task.setValue({ fieldId: 'title',
      value: `Pick/Pack SO#${rec.getValue('tranid')}` });
    task.setValue({ fieldId: 'assigned',
      value: getWarehouseLead(rec.getValue('location')) });
    task.setValue({ fieldId: 'transaction', value: rec.id });
    task.save();

    // 3. Audit log
    log.audit({
      title: '[GFS] Sales | Order Approved',
      details: `SO ${rec.getValue('tranid')} approved by ` +
        `${runtime.getCurrentUser().name}`
    });
  };

  const getWarehouseLead = (locationId) => {
    // Edgewood warehouse lead = 142, Hermitage = 156
    return locationId === 1 ? 142 : 156;
  };

  return { afterSubmit };
});
■ User Event scripts have 1,000 governance units. Use afterSubmit for record creation and email; use beforeSubmit for validation only. Never load the same record in beforeSubmit.
05 Suitelet Patterns Branded forms, list views, chart dashboards, file downloads, redirect handlers
Form Builder — GFS Branded Suitelet
Suitelet
Creates a custom GFS-branded form for vendor onboarding. Includes company information, certifications, and compliance fields with the GFS cobalt header and branding.
/**
 * @NApiVersion 2.1
 * @NScriptType Suitelet
 */
define(['N/ui/serverWidget', 'N/record', 'N/redirect'],
  (ui, record, redirect) => {

  const onRequest = (context) => {
    if (context.request.method === 'GET') {
      const form = ui.createForm({
        title: 'GFS Vendor Onboarding',
        hideNavBar: false
      });

      // GFS branded header via inline HTML
      form.addField({
        id: 'custpage_header', type: 'inlinehtml', label: ' '
      }).defaultValue = `
        <div style="background:#092F64;color:#fff;padding:16px 24px;
          margin:-10px -10px 20px;font-family:Inter,sans-serif;">
          <div style="font-size:9px;letter-spacing:.12em;
            text-transform:uppercase;opacity:.7;">
            Global Food Solutions, Inc.</div>
          <div style="font-size:20px;font-weight:700;margin-top:4px;">
            New Vendor Registration</div>
        </div>`;

      // Company info group
      const grp1 = form.addFieldGroup({
        id: 'grp_company', label: 'Company Information'
      });
      form.addField({
        id: 'custpage_company', type: 'text',
        label: 'Company Name', container: 'grp_company'
      }).isMandatory = true;
      form.addField({
        id: 'custpage_ein', type: 'text',
        label: 'EIN / Tax ID', container: 'grp_company'
      });

      // Certifications group
      const grp2 = form.addFieldGroup({
        id: 'grp_certs', label: 'Certifications'
      });
      ['USDA', 'FDA', 'SQF', 'Organic', 'Kosher'].forEach(cert => {
        form.addField({
          id: `custpage_cert_${cert.toLowerCase()}`,
          type: 'checkbox',
          label: cert, container: 'grp_certs'
        });
      });

      form.addSubmitButton({ label: 'Submit Application' });
      context.response.writePage(form);

    } else {
      // POST: create vendor record
      const params = context.request.parameters;
      const vendor = record.create({ type: 'vendor' });
      vendor.setValue({
        fieldId: 'companyname',
        value: params.custpage_company
      });
      vendor.setValue({
        fieldId: 'custentity_gfs_ein',
        value: params.custpage_ein
      });
      const vendorId = vendor.save();

      redirect.toRecord({
        type: 'vendor', id: vendorId
      });
    }
  };

  return { onRequest };
});
List View — Open PO Dashboard
Suitelet
Displays a filterable list of open purchase orders with vendor, amount, status, and expected delivery date. Includes action buttons for approval and export.
/**
 * @NApiVersion 2.1
 * @NScriptType Suitelet
 */
define(['N/ui/serverWidget', 'N/search'], (ui, search) => {

  const onRequest = (context) => {
    const form = ui.createForm({ title: 'GFS Open Purchase Orders' });
    const list = form.addSublist({
      id: 'custpage_po_list',
      type: ui.SublistType.LIST,
      label: 'Open POs'
    });

    list.addField({ id: 'po_num', type: 'text', label: 'PO #' });
    list.addField({ id: 'vendor', type: 'text', label: 'Vendor' });
    list.addField({ id: 'amount', type: 'currency', label: 'Amount' });
    list.addField({ id: 'status', type: 'text', label: 'Status' });
    list.addField({ id: 'expected', type: 'date', label: 'Expected' });

    const poSearch = search.create({
      type: 'purchaseorder',
      filters: [['mainline', 'is', 'T'], 'AND',
                ['status', 'anyof', 'PurchOrd:A', 'PurchOrd:B']],
      columns: ['tranid', 'entity', 'total', 'statusref',
                 'custbody_gfs_delivery_date']
    });

    let line = 0;
    poSearch.run().each((result) => {
      list.setSublistValue({ id: 'po_num', line, value: result.getValue('tranid') });
      list.setSublistValue({ id: 'vendor', line, value: result.getText('entity') });
      list.setSublistValue({ id: 'amount', line, value: result.getValue('total') });
      list.setSublistValue({ id: 'status', line, value: result.getText('statusref') });
      list.setSublistValue({ id: 'expected', line,
        value: result.getValue('custbody_gfs_delivery_date') || '' });
      line++;
      return line < 500;
    });

    context.response.writePage(form);
  };

  return { onRequest };
});
■ Suitelets have 1,000 governance units per request. For complex dashboards, use inline HTML with the GFS design tokens. Always include the cobalt header bar for brand consistency.
06 RESTlet Patterns GET, POST, PUT, DELETE with authentication, error handling, and rate limiting
Complete RESTlet — Item CRUD with Auth & Error Handling
RESTlet
Full CRUD RESTlet for GFS item records. Includes TBA authentication validation, structured error responses, input sanitization, and rate limiting via custom usage tracking.
/**
 * @NApiVersion 2.1
 * @NScriptType Restlet
 * @NModuleScope SameAccount
 * @description GFS Item API - CRUD operations with TBA auth
 */
define(['N/record', 'N/search', 'N/log', 'N/error', 'N/runtime'],
  (record, search, log, error, runtime) => {

  // ── Helpers ──
  const response = (status, data) =>
    JSON.stringify({ status, timestamp: new Date().toISOString(), data });

  const errorResponse = (code, message) =>
    response('error', { code, message });

  const logRequest = (method, detail) =>
    log.audit({
      title: `[GFS] RESTlet | ${method}`,
      details: `User: ${runtime.getCurrentUser().name} | ${detail}`
    });

  // ── GET: Record lookup ──
  const get = (params) => {
    logRequest('GET', `Item ID: ${params.id}`);

    if (!params.id) return errorResponse('MISSING_ID', 'Item ID required');

    try {
      const item = record.load({
        type: 'inventoryitem', id: params.id
      });
      return response('ok', {
        id: item.id,
        sku: item.getValue('itemid'),
        name: item.getValue('displayname'),
        upc: item.getValue('upccode'),
        price: item.getValue('baseprice'),
        onHand: item.getValue('quantityonhand'),
        allergens: item.getValue('custitem_gfs_allergen_summary')
      });
    } catch (e) {
      return errorResponse('NOT_FOUND', `Item ${params.id} not found`);
    }
  };

  // ── POST: Record create ──
  const post = (body) => {
    logRequest('POST', `Creating item: ${body.sku}`);

    if (!body.sku || !body.name)
      return errorResponse('VALIDATION', 'sku and name are required');

    try {
      const item = record.create({ type: 'inventoryitem' });
      item.setValue({ fieldId: 'itemid', value: body.sku });
      item.setValue({ fieldId: 'displayname', value: body.name });
      if (body.upc) item.setValue({ fieldId: 'upccode', value: body.upc });
      if (body.price) item.setValue({ fieldId: 'baseprice', value: body.price });
      const id = item.save();

      return response('created', { id, sku: body.sku });
    } catch (e) {
      return errorResponse('CREATE_FAILED', e.message);
    }
  };

  // ── PUT: Record update ──
  const put = (body) => {
    logRequest('PUT', `Updating item: ${body.id}`);

    if (!body.id) return errorResponse('MISSING_ID', 'Item ID required');

    try {
      const fields = {};
      if (body.name) fields.displayname = body.name;
      if (body.price) fields.baseprice = body.price;
      if (body.upc) fields.upccode = body.upc;

      record.submitFields({
        type: 'inventoryitem',
        id: body.id,
        values: fields
      });
      return response('updated', { id: body.id });
    } catch (e) {
      return errorResponse('UPDATE_FAILED', e.message);
    }
  };

  // ── DELETE: Record deactivate (never hard delete) ──
  const doDelete = (params) => {
    logRequest('DELETE', `Deactivating item: ${params.id}`);

    if (!params.id) return errorResponse('MISSING_ID', 'Item ID required');

    try {
      record.submitFields({
        type: 'inventoryitem',
        id: params.id,
        values: { isinactive: true }
      });
      return response('deactivated', { id: params.id });
    } catch (e) {
      return errorResponse('DELETE_FAILED', e.message);
    }
  };

  return { get, post, put, 'delete': doDelete };
});
Authentication Notes
All GFS RESTlets use Token-Based Authentication (TBA). OAuth 1.0 header signing with consumer key/secret + token key/secret. Never use NLAuth in production. Integration records are scoped to specific script deployments. Token pairs rotate quarterly. See SuiteAttach project for the reference TBA implementation.
■ RESTlets have 5,000 governance units. Always return structured JSON. Never hard-delete records; use isinactive flag. Log all requests for audit trail.
07 Scheduled Script Patterns Daily jobs, batch processing, governance handling, recovery, and logging
Daily Job with Governance & Recovery
Scheduled
Scheduled script pattern for the daily AR aging email job. Demonstrates governance checking, checkpoint/resume for long runs, error handling per batch, and completion notification.
/**
 * @NApiVersion 2.1
 * @NScriptType ScheduledScript
 * @description GFS Daily AR Aging Email Report
 */
define(['N/search', 'N/email', 'N/render', 'N/runtime', 'N/log', 'N/task'],
  (search, email, render, runtime, log, task) => {

  const GOVERNANCE_THRESHOLD = 500;
  const FINANCE_ROLE = 1032;

  const execute = (context) => {
    const script = runtime.getCurrentScript();
    let lastProcessed = script.getParameter({
      name: 'custscript_gfs_ar_last_id'
    }) || 0;

    log.audit({
      title: '[GFS] AR Aging | Start',
      details: `Resuming from ID: ${lastProcessed}`
    });

    const agingSearch = search.create({
      type: 'invoice',
      filters: [
        ['mainline', 'is', 'T'], 'AND',
        ['status', 'anyof', 'CustInvc:A'], 'AND',
        ['internalidnumber', 'greaterthan', lastProcessed]
      ],
      columns: [
        search.createColumn({ name: 'internalid', sort: search.Sort.ASC }),
        'entity', 'tranid', 'total', 'daysoverdue'
      ]
    });

    const buckets = { current: 0, d30: 0, d60: 0, d90: 0, d90plus: 0 };
    let processedCount = 0;

    agingSearch.run().each((result) => {
      // Governance check
      if (script.getRemainingUsage() < GOVERNANCE_THRESHOLD) {
        log.audit({
          title: '[GFS] AR Aging | Checkpoint',
          details: `Saving at ID ${result.id}, processed ${processedCount}`
        });
        reschedule(result.id);
        return false;
      }

      const days = parseInt(result.getValue('daysoverdue')) || 0;
      const amount = parseFloat(result.getValue('total')) || 0;

      if (days <= 0) buckets.current += amount;
      else if (days <= 30) buckets.d30 += amount;
      else if (days <= 60) buckets.d60 += amount;
      else if (days <= 90) buckets.d90 += amount;
      else buckets.d90plus += amount;

      lastProcessed = result.id;
      processedCount++;
      return true;
    });

    // Send summary email
    if (processedCount > 0) {
      email.send({
        author: -5, // System
        recipients: FINANCE_ROLE,
        subject: `GFS AR Aging Summary - ${new Date().toLocaleDateString()}`,
        body: formatAgingEmail(buckets, processedCount)
      });
    }

    log.audit({
      title: '[GFS] AR Aging | Complete',
      details: `Processed ${processedCount} invoices`
    });
  };

  const reschedule = (lastId) => {
    const scriptTask = task.create({ taskType: task.TaskType.SCHEDULED_SCRIPT });
    scriptTask.scriptId = runtime.getCurrentScript().id;
    scriptTask.deploymentId = runtime.getCurrentScript().deploymentId;
    scriptTask.params = { custscript_gfs_ar_last_id: lastId };
    scriptTask.submit();
  };

  const formatAgingEmail = (b, count) =>
    `AR Aging Report\n` +
    `Current: $${b.current.toFixed(2)}\n` +
    `1-30 Days: $${b.d30.toFixed(2)}\n` +
    `31-60 Days: $${b.d60.toFixed(2)}\n` +
    `61-90 Days: $${b.d90.toFixed(2)}\n` +
    `90+ Days: $${b.d90plus.toFixed(2)}\n` +
    `Total Invoices: ${count}`;

  return { execute };
});
■ Scheduled scripts have 10,000 governance units. Always implement checkpoint/resume for data sets over 1,000 records. Use the reschedule pattern to chain script executions.
08 Map/Reduce Patterns High-volume parallel processing with error handling and summary logging
Mass Pricing Update — CME-Based Repricing
Map/Reduce
Updates pricing on all CME-linked cheese items based on the latest trailing week CME average. Processes items in parallel, handles per-item errors gracefully, and logs a complete summary with success/failure counts.
/**
 * @NApiVersion 2.1
 * @NScriptType MapReduceScript
 * @description GFS CME-based cheese pricing update (weekly)
 */
define(['N/search', 'N/record', 'N/log', 'N/email', 'N/runtime'],
  (search, record, log, email, runtime) => {

  // ── Phase 1: Get Input Data ──
  const getInputData = () => {
    return search.create({
      type: 'inventoryitem',
      filters: [
        ['custitem_gfs_cme_linked', 'is', 'T'], 'AND',
        ['isinactive', 'is', 'F']
      ],
      columns: [
        'internalid', 'itemid', 'displayname',
        'custitem_gfs_cme_basis', 'custitem_gfs_moisture_bracket',
        'baseprice'
      ]
    });
  };

  // ── Phase 2: Map (per item) ──
  const map = (context) => {
    const data = JSON.parse(context.value);
    const itemId = data.id;
    const basis = parseFloat(data.values.custitem_gfs_cme_basis) || 0;
    const bracket = data.values.custitem_gfs_moisture_bracket || 'standard';

    // CME trailing week average (from GFS config record)
    const cmePrice = getCMETrailingWeek();
    const premiumPct = bracket === 'high_moisture' ? 0.35 : 0;
    const newPrice = (cmePrice + basis) * (1 + premiumPct);

    // Emit to reduce phase grouped by price change direction
    const direction = newPrice > parseFloat(data.values.baseprice)
      ? 'increase' : 'decrease';

    context.write({
      key: direction,
      value: JSON.stringify({
        itemId, sku: data.values.itemid,
        oldPrice: data.values.baseprice,
        newPrice: newPrice.toFixed(4)
      })
    });
  };

  // ── Phase 3: Reduce (batch by direction) ──
  const reduce = (context) => {
    context.values.forEach(val => {
      const item = JSON.parse(val);
      try {
        record.submitFields({
          type: 'inventoryitem',
          id: item.itemId,
          values: { baseprice: item.newPrice }
        });
        log.audit({
          title: '[GFS] Pricing | Updated',
          details: `${item.sku}: $${item.oldPrice} -> $${item.newPrice}`
        });
      } catch (e) {
        log.error({
          title: '[GFS] Pricing | Failed',
          details: `${item.sku}: ${e.message}`
        });
      }
    });
  };

  // ── Phase 4: Summarize ──
  const summarize = (summary) => {
    let updated = 0, failed = 0;

    summary.reduceSummary.errors.iterator().each((key, error) => {
      log.error({ title: `Reduce Error [${key}]`, details: error });
      failed++;
      return true;
    });

    summary.reduceSummary.keys.iterator().each(() => {
      updated++;
      return true;
    });

    const msg = `GFS CME Pricing Update Complete\n` +
      `Updated: ${updated}\nFailed: ${failed}\n` +
      `Duration: ${summary.seconds}s\n` +
      `Governance Used: ${summary.usage}`;

    log.audit({ title: '[GFS] Pricing | Summary', details: msg });

    email.send({
      author: -5,
      recipients: runtime.getCurrentUser().id,
      subject: 'GFS CME Pricing Update Complete',
      body: msg
    });
  };

  // ── Helper ──
  const getCMETrailingWeek = () => {
    const config = search.lookupFields({
      type: 'customrecord_gfs_config',
      id: 1,
      columns: ['custrecord_gfs_cme_trailing_avg']
    });
    return parseFloat(config.custrecord_gfs_cme_trailing_avg) || 1.85;
  };

  return { getInputData, map, reduce, summarize };
});
■ Map/Reduce scripts get 10,000 governance units per phase and auto-retry failed map/reduce invocations. Use summarize phase for completion notifications and error reporting.
09 GFS Script Library Shared utility functions used across all GFS SuiteScript customizations
Module Path
SuiteScripts/gfs/lib/gfs_utils.js — Import via: define(['./lib/gfs_utils'], (gfs) => { ... })
getGFSConfig()
Loads the GFS configuration custom record with caching. Returns all system-wide settings.
configId
number
Config record ID (default: 1)
Returns: Object — { cmePrice, defaultWarehouse, alertEmail, ... }
const getGFSConfig = (configId = 1) => {
  const key = `gfs_config_${configId}`;
  let config = runtime.getCurrentSession().get({ name: key });
  if (config) return JSON.parse(config);

  config = search.lookupFields({
    type: 'customrecord_gfs_config', id: configId,
    columns: ['custrecord_gfs_cme_trailing_avg',
              'custrecord_gfs_default_warehouse',
              'custrecord_gfs_alert_email']
  });
  runtime.getCurrentSession().set({ name: key, value: JSON.stringify(config) });
  return config;
};
formatCurrency(amount, currency)
Formats a number as currency string with proper symbol, thousands separator, and decimal places.
amount
number
Numeric amount to format
currency
string
Currency code (default: 'USD')
Returns: string — e.g., "$14,240.00"
const formatCurrency = (amount, currency = 'USD') => {
  const symbols = { USD: '$', CAD: 'C$', EUR: '\u20AC', GBP: '\u00A3' };
  const sym = symbols[currency] || '$';
  const num = parseFloat(amount) || 0;
  return sym + num.toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ',');
};
sendFrostyNotification(opts)
Sends a GFS-branded notification email with the Frosty mascot header. Used for all system notifications.
opts.to
number|array
Recipient employee/contact ID(s)
opts.subject
string
Email subject line
opts.body
string
HTML body content
opts.level
string
'info' | 'warning' | 'error'
Returns: void
const sendFrostyNotification = (opts) => {
  const colors = { info: '#092F64', warning: '#B45309', error: '#DC2626' };
  const color = colors[opts.level] || colors.info;
  const html = `<div style="max-width:600px;font-family:Inter,sans-serif;">
    <div style="background:${color};color:#fff;padding:16px 24px;">
      <strong>Global Food Solutions</strong></div>
    <div style="padding:24px;border:1px solid #D9E1EA;">${opts.body}</div>
    <div style="padding:12px 24px;font-size:11px;color:#6B7280;">
      131 Heartland Blvd, Edgewood, NY 11717 | (877) 728-5550</div></div>`;
  email.send({ author: -5, recipients: opts.to,
    subject: opts.subject, body: html });
};
logAuditTrail(module, action, detail)
Writes a structured audit log entry to the GFS custom audit trail record and the execution log.
module
string
Module name (e.g., 'Sales', 'Pricing')
action
string
Action taken (e.g., 'Order Approved')
detail
string
Additional context or JSON payload
Returns: number — Audit record internal ID
const logAuditTrail = (module, action, detail) => {
  log.audit({ title: `[GFS] ${module} | ${action}`, details: detail });
  const rec = record.create({ type: 'customrecord_gfs_audit' });
  rec.setValue({ fieldId: 'custrecord_gfs_audit_module', value: module });
  rec.setValue({ fieldId: 'custrecord_gfs_audit_action', value: action });
  rec.setValue({ fieldId: 'custrecord_gfs_audit_detail', value: detail });
  rec.setValue({ fieldId: 'custrecord_gfs_audit_user',
    value: runtime.getCurrentUser().id });
  return rec.save();
};
validateAllergens(itemRecord)
Validates Big 9 allergen declarations on an item record. Returns validation result with errors array.
itemRecord
Record
NetSuite item record object
Returns: Object — { valid: boolean, errors: string[], allergens: string[] }
const BIG_9 = ['milk','eggs','fish','shellfish','tree_nuts',
               'peanuts','wheat','soybeans','sesame'];

const validateAllergens = (rec) => {
  const active = BIG_9.filter(a =>
    rec.getValue({ fieldId: `custitem_gfs_allergen_${a}` }));
  const errors = [];
  if (active.length > 0) {
    if (!rec.getValue('custitem_gfs_allergen_statement'))
      errors.push('Allergen contains statement required');
    if (!rec.getValue('custitem_gfs_allergen_reviewed'))
      errors.push('Allergen review checkbox must be checked');
  }
  return { valid: errors.length === 0, errors, allergens: active };
};
calculateMargin(cost, price)
Calculates gross margin percentage. Returns formatted percentage string with color indicator.
cost
number
Item cost or COGS
price
number
Selling price
Returns: Object — { pct: number, formatted: string, healthy: boolean }
const calculateMargin = (cost, price) => {
  const c = parseFloat(cost) || 0;
  const p = parseFloat(price) || 0;
  if (p === 0) return { pct: 0, formatted: '0.00%', healthy: false };
  const pct = ((p - c) / p) * 100;
  return {
    pct: Math.round(pct * 100) / 100,
    formatted: pct.toFixed(2) + '%',
    healthy: pct >= 15 // GFS minimum healthy margin
  };
};
getCMEPrice(commodity, days)
Retrieves the CME trailing average price for a given commodity over the specified number of days.
commodity
string
'barrel_cheddar' | 'block_cheddar' | 'butter' | 'nfdm'
days
number
Trailing days (default: 5 = one week)
Returns: number — Average price per pound in USD
const getCMEPrice = (commodity = 'barrel_cheddar', days = 5) => {
  const fieldMap = {
    barrel_cheddar: 'custrecord_gfs_cme_barrel',
    block_cheddar: 'custrecord_gfs_cme_block',
    butter: 'custrecord_gfs_cme_butter',
    nfdm: 'custrecord_gfs_cme_nfdm'
  };
  const field = fieldMap[commodity];
  if (!field) throw error.create({
    name: 'GFS_INVALID_COMMODITY',
    message: `Unknown commodity: ${commodity}`
  });

  const results = search.create({
    type: 'customrecord_gfs_cme_prices',
    filters: [['created', 'within', `daysago${days}`, 'today']],
    columns: [search.createColumn({ name: field, summary: 'AVG' })]
  }).run().getRange({ start: 0, end: 1 });

  return parseFloat(results[0]?.getValue({
    name: field, summary: 'AVG'
  })) || 0;
};
■ The GFS utility library is loaded as a SuiteScript 2.1 AMD module. All functions include JSDoc annotations. Keep this library lean; each function should serve multiple scripts.
10 Deployment Checklist Script record setup, testing, role restrictions, governance budgeting
Script Record Setup
Script ID follows naming convention: customscript_gfs_{module}_{type}
Script file uploaded to correct SuiteScripts/gfs/ folder
API version set to 2.1 (not 2.0 or 1.0)
Description field populated with purpose and owner
All library dependencies listed in define() block
Deployment Record
Deployment ID: customdeploy_gfs_{module}_{action}
Status set to "Testing" (not "Released") for initial deploy
Applied To correct record type(s)
Execute As Role configured (typically Administrator)
Log Level set to "Debug" for sandbox, "Audit" for production
Testing in Sandbox
All entry points tested (create, edit, view, delete, copy)
Tested with each affected role (Admin, Sales, Warehouse, Finance)
Edge cases validated (empty fields, max values, special characters)
Bulk operations tested (CSV import, mass update, web services)
Execution log reviewed for errors and warnings
Role Restrictions
Audience tab configured: only roles that need the script
RESTlets: integration role limited to specific IP ranges
Suitelets: available without login only if intentionally public
Scheduled: execute-as-role has minimum required permissions
No script runs as Administrator in production without IT approval
Execution Log Monitoring
Verify no unexpected EMERGENCY or ERROR entries post-deploy
Confirm governance usage is within budget (check peak vs. average)
Set up saved search alert for script errors over threshold
Monitor for first 48 hours after production release
Document expected governance per execution for capacity planning
Governance Budgeting
Client Script: no limit (but minimize API calls for UX)
User Event: 1,000 units — plan for worst-case line count
Suitelet / RESTlet: 1,000 / 5,000 units — paginate large queries
Scheduled / Map-Reduce: 10,000 per phase — implement checkpoints
Record.load = 5 units, search.run = 10 units, record.save = 20 units
Production Release Protocol
1. Complete all sandbox testing with sign-off from module owner.
2. Create change log entry in GFS customization registry.
3. Schedule deployment for off-hours (after 8 PM ET, before 6 AM ET).
4. Deploy script to production with status = "Testing".
5. Verify with one test transaction in production.
6. Change status to "Released" and monitor for 48 hours.
7. Keep previous version inactive (not deleted) for 30-day rollback.
■ Never deploy directly to production without sandbox validation. All GFS scripts follow the testing-first protocol. Rollback plan must exist before every release.
← Back to Hub · GFS Design System v10.0 · Global Food Solutions, Inc. · Edgewood, NY