← Hub NetSuite Portal & Suitelet Starter
v10.0
GFS Design System v10.0
NetSuite
Starter
The definitive template for building GFS NetSuite portals, suitelets, dashboards, and operational tools.
Every pattern needed for production NetSuite development -- suitelet page layouts, record forms, saved search displays, dashboard portlets, approval workflows, print templates, email templates, SuiteQL queries, and SuiteScript patterns. Copy directly into your Suitelet.
24 Sections SuiteScript SuiteQL Print/Email Field Reference Best Practices
Document Metadata
Type NetSuite Starter
Version 9.0
Updated 2026-05-20
Author Design Systems
Status Production
Platform NetSuite 2025.2
02 Suitelet Page Layout Complete app shell with top bar, sidebar, breadcrumbs, and interactive navigation
Operations Center
ML
M. Levine
Home / Operations / Dashboard
GFS
Main
Dashboard
Records42
Searches
Data
Reports
Financial
Sales
Inventory
SuiteQL
Exports
System
Settings
Approvals3
■ Sidebar collapses to horizontal on mobile (< 768px). Active item uses inset border-left accent. Sub-menus toggle on click.
03 Record Form Complete NetSuite record entry form with validation, line items, and action bar
Sales Order
SO-24891 Pending Fulfillment
Created05/18/2026
Modified05/20/2026
Created ByM. Levine
Primary Information
Customer is required.
Order date is required.
Shipping / Billing
Line Items
#ItemQtyRateAmount
1Barrel Cheddar 640# - GFS-442012$2,847.50$34,170.00
2Process American 5# Loaf - GFS-2210200$14.25$2,850.00
3Mozzarella LMPS 6# - GFS-3310150$18.90$2,835.00
4Pepper Jack 40# Block - GFS-448024$128.50$3,084.00
SUBTOTAL$42,939.00
TAX (8.625%)$3,703.49
TOTAL$46,642.49
Extended Field Types
USD format, auto-calculated from line items
%
Gross margin percentage
Hold Ctrl/Cmd to select multiple zones
Editable Sublist with Recalculation
#ItemDescriptionQtyRateAmountTax Code
1 $34,170.00
2 $2,850.00
SUBTOTAL$37,020.00
Edit quantity or rate directly in cells. Amounts recalculate automatically. Required fields show red border on validation.
Notes & Comments
■ Form validates required fields on submit. "Add Line" appends a new row. All fields use .ns- prefixed classes. Currency fields right-aligned with Plex Mono.
04 Record List View NetSuite saved search / record list with filters, sorting, pagination, and bulk actions
10 results
Doc # Date Entity Amount StatusMemoActions
SO-2489105/20/2026Driscoll Foods$46,642.49Pending FulfillmentWeekly dairy standing order
SO-2489005/19/2026Sysco Northeast$32,180.50BilledFrozen foods reorder
SO-2488905/19/2026US Foods Philadelphia$27,445.00FulfilledDeli meats order #3
SO-2488805/18/2026Restaurant Depot NYC$19,830.75Partially FulfilledCheese blocks multi-ship
PO-1044705/18/2026Bongards Creameries$84,200.00OpenBarrel cheddar 640# x 30
INV-1884205/17/2026FreshMart Distribution$15,620.00BilledMonthly invoice - May
SO-2488705/17/2026Baldor Specialty Foods$42,910.25Credit HoldExceeds credit limit
SO-2488605/16/2026Performance Food Group$56,340.00Pending FulfillmentPrivate label co-pack
PO-1044605/16/2026Land O'Lakes$28,750.00ClosedButter 25# blocks received
INV-1884105/15/2026Jetro Cash & Carry$8,775.50BilledCOD - paid on delivery
Last exported: 05/19/2026 14:30

Saved Filter Presets
Row Expansion for Detail View
Doc #DateEntityAmountStatus
SO-2489105/20/2026Driscoll Foods$46,642.49Pending Fulfillment
Order Details
PO#: PO-DF-2026-0547
Terms: Net 30
Rep: Mike Reynolds
Dept: Dairy & Cheese
Ship To
Driscoll Foods - Warehouse
1 Distribution Way
Mount Vernon, NY 10550
Line Items (4)
Barrel Cheddar 640# x12
Process American 5# x200
Mozzarella LMPS 6# x150
Pepper Jack 40# x24
SO-2489005/19/2026Sysco Northeast$32,180.50Billed
Order Details
PO#: PO-SYS-2026-1184
Terms: Net 60
Rep: Sarah Chen
Dept: Frozen Foods
Ship To
Sysco NE Distribution
14 Sysco Court
Front Royal, VA 22630
Shipping
Carrier: Sysco Fleet
PRO#: PRO-4829880
Shipped: 05/19/2026
Temp: Frozen (-10F)
Inline Editing Mode
Doc #EntityMemoDepartmentAmountStatus
SO-24891 Driscoll Foods $46,642.49 Pending Fulfillment
SO-24890 Sysco Northeast $32,180.50 Billed
2 rows modified
■ Click column headers to sort. Select All checkbox toggles all rows. Hover rows to reveal action buttons. Pagination and page size are configurable. Row expansion shows detail without navigation. Inline editing highlights modified cells in alice blue.
05 Dashboard Portlet Grid NetSuite dashboard with KPI, chart, list, and reminder portlets
Key Metrics
Open Sales Orders
142
+8 today
Open Purchase Orders
37
-2 today
AR Aging > 60 Days
$186K
+$12K this week
Inventory Value
$4.2M
+$340K
Monthly Revenue
$1.8M
Dec
$2.0M
Jan
$2.2M
Feb
$2.1M
Mar
$2.5M
Apr
$2.9M
May
Recent Transactions
  • SO-24891 Driscoll Foods $46,642.49 - 10m ago
  • SO-24890 Sysco Northeast $32,180.50 - 2h ago
  • PO-10447 Bongards Creameries $84,200.00 - 4h ago
  • INV-18842 FreshMart Distribution $15,620.00 - 1d ago
  • SO-24887 Baldor Specialty Foods $42,910.25 - 1d ago
Reminders
PO approvals pending review
3
Customers on credit hold
2
Contracts expiring within 30 days
5
■ Portlets use .ns-portlet with expand/collapse toggle. KPI metrics use .ns-kpi. Bar chart is pure CSS. Grid is 2x2 on desktop, 1-col on mobile.
06 Approval Workflow Status timeline, approver cards, approve/reject with confirmation, workflow history
Approval Status Timeline
Submitted
M. Levine
05/18 09:30
Level 1
S. Chen, Ops Mgr
05/18 14:15
2
Level 2
T. Richards, VP
Pending
Approved
Final
--
Current Approver
TR
Tom Richards
VP Operations | Level 2 Approver
Workflow History
Date/TimeUserRoleActionComment
05/18/2026 09:30M. LevineAdministratorSubmittedPO-10447 for Bongards barrel cheddar. CME+0.0350 pricing approved by purchasing.
05/18/2026 14:15S. ChenOps ManagerApprovedQuantities verified against inventory forecast. Approved for processing.
05/20/2026 --:--T. RichardsVP OperationsPending--
■ Approval chain uses .ns-approval-step with .completed / .current / .pending states. Rejection requires a comment (validated in JS).
07 Customer / Vendor Portal External-facing portal with login, quick actions, orders table, document downloads, and support
Login Screen

Portal Dashboard
Welcome, Dave Jordan
Driscoll Foods | Account #CUST-1001 | Net 30 Terms
AR Balance: $86,420.00
Credit Available: $163,580.00
Recent Orders
Order #DateAmountStatusTracking
SO-2489105/20/2026$46,642.49Pending--
SO-2487805/13/2026$38,240.00ShippedPRO-4829174
SO-2486505/06/2026$42,180.00BilledPRO-4828901
Document Library
📄
Certificate of Analysis
COA-2026-0547.pdf
📄
Spec Sheets
SPEC-GFS-4420.pdf
📄
Safety Data Sheets
SDS-Dairy-2026.pdf
Submit Support Ticket
Contact Information
Global Food Solutions, Inc.
131 Heartland Blvd, Edgewood, NY 11717
(877) 728-5550
orders@GlobalFoodSolutions.co

Your Account Rep: Mike Reynolds
mreynolds@GlobalFoodSolutions.co
■ Portal login is centered card with cobalt header. Quick actions use 4-col grid. Document downloads link to file cabinet. Support ticket submits to case record.
08 NetSuite Print Templates Invoice, Packing Slip, Purchase Order, and Bill of Lading HTML print templates
■ Print templates use @media print styles for proper margins and page breaks. Each includes 3px cobalt top border, company header, and structured layout.
09 Email Templates Order confirmation, invoice, shipping notification, and past due reminder email patterns

Dear Dave Jordan,

Thank you for your order. We have received SO-24891 and it is being processed. Below is a summary:

Order #: SO-24891
Date: May 20, 2026
Items: 4 line items
Total: $46,642.49
Terms: Net 30
Est. Ship: May 22, 2026

Questions? Contact Mike Reynolds at mreynolds@GlobalFoodSolutions.co.

Dear Accounts Payable,

Please find attached Invoice #INV-18842 in the amount of $15,620.02, due by June 16, 2026.

For ACH/Wire: Chase Bank, ABA 021000021, Account 8847291034. Reference INV-18842.

Dear Dave Jordan,

Your order SO-24891 has shipped:

Carrier: Heartland Express
PRO #: PRO-4830012
Ship Date: May 20, 2026
Est. Delivery: May 21, 2026
Temp Range: 34-38F (Refrigerated)

Dear Accounts Payable,

This is a reminder that the following invoice is past due:

Invoice #: INV-18790
Amount Due: $24,380.00
Due Date: April 28, 2026
Days Past Due: 22
Late Fee: $365.70 (1.5% monthly)

Please remit payment at your earliest convenience.

Contact ar@GlobalFoodSolutions.co or (877) 728-5550 ext. 204 for payment questions.

■ Email templates use inline-safe styles. Past due email uses warning-colored header. All include CTA button and footer. Test in Litmus before deploy.
10 SuiteQL & Search Patterns Query builder UI, common GFS queries reference, saved search display, CSV export
SuiteQL Query Builder
SuiteQL Editor
Common GFS SuiteQL Queries
Query NameDescriptionFrequency
Top 10 Customers by RevenueYTD revenue by customer, ranked descendingDaily
Open AR > 30 DaysInvoices past due over 30 days with aging bucketsDaily
Inventory Below Reorder PointItems where available qty < reorder pointReal-time
Open POs by VendorOutstanding purchase orders grouped by vendor with ETADaily
Sales by Rep - MTDMonth-to-date sales totals by sales repWeekly
Margin Analysis by ItemCost vs. revenue per item with margin percentageMonthly
Credit Hold CustomersCustomers currently on credit hold with outstanding balanceDaily
Lot TraceabilityTrace lot numbers from vendor receipt through customer shipmentOn demand

Production GFS Queries with Field IDs
Top 20 Customers by YTD Revenue
SELECT
  e.companyname AS "Customer",
  e.id AS "Internal ID",
  SUM(tl.netamount) AS "YTD Revenue",
  COUNT(DISTINCT t.id) AS "Order Count",
  ROUND(AVG(tl.netamount), 2) AS "Avg Line Amount"
FROM transaction t
  JOIN transactionline tl ON t.id = tl.transaction
  JOIN entity e ON t.entity = e.id
WHERE t.type = 'SalesOrd'
  AND t.trandate >= TO_DATE('2026-01-01','YYYY-MM-DD')
  AND tl.mainline = 'F'
  AND tl.itemtype != 'Discount'
GROUP BY e.companyname, e.id
ORDER BY "YTD Revenue" DESC
FETCH FIRST 20 ROWS ONLY
AR Aging by Bucket (30/60/90/120)
SELECT
  e.companyname AS "Customer",
  SUM(CASE WHEN CURRENT_DATE - t.duedate BETWEEN 1 AND 30 THEN t.foreignamountremaining ELSE 0 END) AS "1-30",
  SUM(CASE WHEN CURRENT_DATE - t.duedate BETWEEN 31 AND 60 THEN t.foreignamountremaining ELSE 0 END) AS "31-60",
  SUM(CASE WHEN CURRENT_DATE - t.duedate BETWEEN 61 AND 90 THEN t.foreignamountremaining ELSE 0 END) AS "61-90",
  SUM(CASE WHEN CURRENT_DATE - t.duedate > 90 THEN t.foreignamountremaining ELSE 0 END) AS "90+",
  SUM(t.foreignamountremaining) AS "Total AR"
FROM transaction t
  JOIN entity e ON t.entity = e.id
WHERE t.type = 'CustInvc'
  AND t.status = 'CustInvc:A'  -- Open
  AND t.foreignamountremaining > 0
GROUP BY e.companyname
ORDER BY "Total AR" DESC
JOIN Pattern: Transaction + Line + Item
SELECT
  t.tranid AS "Doc #",
  t.trandate AS "Date",
  BUILTIN.DF(t.entity) AS "Customer",
  i.itemid AS "Item",
  i.displayname AS "Description",
  tl.quantity AS "Qty",
  tl.rate AS "Rate",
  tl.netamount AS "Amount",
  tl.custcol_gfs_lot AS "Lot #",
  tl.custcol_gfs_temp AS "Temp F"
FROM transaction t
  JOIN transactionline tl ON t.id = tl.transaction
  JOIN item i ON tl.item = i.id
WHERE t.type = 'SalesOrd'
  AND tl.mainline = 'F'
  AND t.trandate >= TO_DATE('2026-05-01','YYYY-MM-DD')
ORDER BY t.trandate DESC, tl.linesequencenumber ASC
Aggregate: Revenue by Brand (custbody_gfs_brand)
SELECT
  BUILTIN.DF(t.custbody_gfs_brand) AS "Brand",
  COUNT(DISTINCT t.id) AS "Orders",
  SUM(tl.netamount) AS "Revenue",
  ROUND(SUM(tl.netamount) / NULLIF(SUM(SUM(tl.netamount)) OVER (), 0) * 100, 1) AS "% of Total"
FROM transaction t
  JOIN transactionline tl ON t.id = tl.transaction
WHERE t.type IN ('SalesOrd','CustInvc')
  AND tl.mainline = 'F'
  AND t.trandate >= TO_DATE('2026-01-01','YYYY-MM-DD')
GROUP BY BUILTIN.DF(t.custbody_gfs_brand)
ORDER BY "Revenue" DESC
Date Functions: Monthly Trend with BUILTIN.DF
SELECT
  TO_CHAR(t.trandate, 'YYYY-MM') AS "Month",
  COUNT(*) AS "Transactions",
  SUM(t.foreigntotal) AS "Total Revenue",
  ROUND(AVG(t.foreigntotal), 2) AS "Avg Order Value"
FROM transaction t
WHERE t.type = 'SalesOrd'
  AND t.trandate >= ADD_MONTHS(CURRENT_DATE, -12)
  AND t.trandate < CURRENT_DATE
GROUP BY TO_CHAR(t.trandate, 'YYYY-MM')
ORDER BY "Month" DESC
CASE WHEN: Classify Orders by Size
SELECT
  t.tranid AS "Doc #",
  BUILTIN.DF(t.entity) AS "Customer",
  t.foreigntotal AS "Total",
  CASE
    WHEN t.foreigntotal >= 50000 THEN 'LARGE'
    WHEN t.foreigntotal >= 25000 THEN 'MEDIUM'
    WHEN t.foreigntotal >= 5000 THEN 'STANDARD'
    ELSE 'SMALL'
  END AS "Order Tier",
  CASE
    WHEN t.custbody_gfs_temp_zone = 1 THEN 'Refrigerated'
    WHEN t.custbody_gfs_temp_zone = 2 THEN 'Frozen'
    WHEN t.custbody_gfs_temp_zone = 3 THEN 'Ambient'
    ELSE 'Unknown'
  END AS "Temp Zone"
FROM transaction t
WHERE t.type = 'SalesOrd'
  AND t.trandate >= TO_DATE('2026-05-01','YYYY-MM-DD')
ORDER BY t.foreigntotal DESC
Inventory Below Reorder Point
SELECT
  i.itemid AS "Item ID",
  i.displayname AS "Item Name",
  ib.quantityavailable AS "Available",
  ib.quantityonhand AS "On Hand",
  ib.quantityonorder AS "On Order",
  i.reorderpoint AS "Reorder Point",
  i.preferredstocklevel AS "Preferred Level",
  (i.reorderpoint - ib.quantityavailable) AS "Shortfall"
FROM item i
  JOIN inventorybalance ib ON i.id = ib.item
WHERE i.isinactive = 'F'
  AND ib.quantityavailable < i.reorderpoint
  AND i.reorderpoint IS NOT NULL
ORDER BY "Shortfall" DESC
Vendor Scorecard: Delivery Performance
SELECT
  BUILTIN.DF(t.entity) AS "Vendor",
  COUNT(*) AS "Total POs",
  SUM(CASE WHEN ir.trandate <= t.expectedreceiptdate THEN 1 ELSE 0 END) AS "On Time",
  ROUND(SUM(CASE WHEN ir.trandate <= t.expectedreceiptdate THEN 1 ELSE 0 END) * 100.0 / COUNT(*), 1) AS "OTD %",
  SUM(t.foreigntotal) AS "Total Spend"
FROM transaction t
  LEFT JOIN transaction ir ON ir.createdfrom = t.id AND ir.type = 'ItemRcpt'
WHERE t.type = 'PurchOrd'
  AND t.trandate >= TO_DATE('2026-01-01','YYYY-MM-DD')
GROUP BY BUILTIN.DF(t.entity)
HAVING COUNT(*) >= 3
ORDER BY "OTD %" ASC
Lot Traceability: Receipt to Shipment
SELECT
  il.inventorynumber AS "Lot #",
  i.itemid AS "Item",
  -- Receipt info
  po.tranid AS "PO #",
  BUILTIN.DF(po.entity) AS "Vendor",
  ir.trandate AS "Received Date",
  -- Shipment info
  so.tranid AS "SO #",
  BUILTIN.DF(so.entity) AS "Customer",
  ifull.trandate AS "Ship Date"
FROM inventorynumber il
  JOIN item i ON il.item = i.id
  LEFT JOIN transactionline rl ON rl.item = i.id
  LEFT JOIN transaction ir ON rl.transaction = ir.id AND ir.type = 'ItemRcpt'
  LEFT JOIN transaction po ON ir.createdfrom = po.id
  LEFT JOIN transactionline sl ON sl.item = i.id
  LEFT JOIN transaction ifull ON sl.transaction = ifull.id AND ifull.type = 'ItemShip'
  LEFT JOIN transaction so ON ifull.createdfrom = so.id
WHERE il.inventorynumber = 'LOT-BC-0520A'
ORDER BY ir.trandate ASC
Margin Analysis by Item
SELECT
  i.itemid AS "Item ID",
  i.displayname AS "Description",
  SUM(tl.quantity) AS "Units Sold",
  SUM(tl.netamount) AS "Revenue",
  SUM(tl.costestimate) AS "COGS",
  SUM(tl.netamount) - SUM(tl.costestimate) AS "Gross Profit",
  ROUND((SUM(tl.netamount) - SUM(tl.costestimate)) / NULLIF(SUM(tl.netamount), 0) * 100, 1) AS "Margin %"
FROM transaction t
  JOIN transactionline tl ON t.id = tl.transaction
  JOIN item i ON tl.item = i.id
WHERE t.type = 'CustInvc'
  AND tl.mainline = 'F'
  AND t.trandate >= TO_DATE('2026-01-01','YYYY-MM-DD')
GROUP BY i.itemid, i.displayname
HAVING SUM(tl.netamount) > 0
ORDER BY "Margin %" ASC
■ SuiteQL editor runs query on button click. Results display in standard .ns-table. Export CSV extracts table data. All queries use SuiteQL 2.0 syntax. JOIN patterns show transaction+line+item relationships. CASE WHEN enables classification logic.
11 Script Patterns & Utilities SuiteScript 2.x entry point patterns, client scripts, RESTlets, Map/Reduce, and GFS utilities
Client Script -- Form Validation
/**
 * @NApiVersion 2.1
 * @NScriptType ClientScript
 * @NModuleScope SameAccount
 */
define(['N/currentRecord', 'N/ui/dialog'], (cr, dialog) => {

  function saveRecord(context) {
    const rec = cr.get();
    const customer = rec.getValue({ fieldId: 'entity' });
    const total = rec.getValue({ fieldId: 'total' });

    if (!customer) {
      dialog.alert({ title: 'Validation', message: 'Customer is required.' });
      return false;
    }
    if (total <= 0) {
      dialog.alert({ title: 'Validation', message: 'Order total must be greater than zero.' });
      return false;
    }
    return true;
  }

  function fieldChanged(context) {
    if (context.fieldId === 'quantity' || context.fieldId === 'rate') {
      const rec = cr.get();
      const qty = rec.getCurrentSublistValue({ sublistId: 'item', fieldId: 'quantity' });
      const rate = rec.getCurrentSublistValue({ sublistId: 'item', fieldId: 'rate' });
      rec.setCurrentSublistValue({ sublistId: 'item', fieldId: 'amount', value: qty * rate });
    }
  }

  return { saveRecord, fieldChanged };
});
User Event -- Before/After Submit
/**
 * @NApiVersion 2.1
 * @NScriptType UserEventScript
 */
define(['N/record', 'N/log'], (record, log) => {

  function beforeSubmit(context) {
    if (context.type !== context.UserEventType.CREATE) return;
    const rec = context.newRecord;
    rec.setValue({ fieldId: 'department', value: 1 }); // Dairy & Cheese
    log.audit('GFS-UE', 'Set department for ' + rec.type + ' ' + rec.id);
  }

  function afterSubmit(context) {
    if (context.type === context.UserEventType.DELETE) return;
    const rec = record.load({ type: context.newRecord.type, id: context.newRecord.id });
    const total = rec.getValue({ fieldId: 'total' });
    if (total > 25000) {
      log.audit('GFS-UE', 'Order ' + rec.id + ' requires L2 approval: $' + total);
    }
  }

  return { beforeSubmit, afterSubmit };
});
Suitelet -- Entry Point
/**
 * @NApiVersion 2.1
 * @NScriptType Suitelet
 */
define(['N/ui/serverWidget', 'N/query'], (sw, query) => {

  function onRequest(context) {
    if (context.request.method === 'GET') {
      const form = sw.createForm({ title: 'GFS Operations Dashboard' });
      form.clientScriptModulePath = './gfs_cs_dashboard.js';

      const sublist = form.addSublist({
        id: 'custpage_results', type: sw.SublistType.LIST,
        label: 'Search Results'
      });
      sublist.addField({ id: 'custpage_doc', type: sw.FieldType.TEXT, label: 'Doc #' });
      sublist.addField({ id: 'custpage_entity', type: sw.FieldType.TEXT, label: 'Customer' });
      sublist.addField({ id: 'custpage_amount', type: sw.FieldType.CURRENCY, label: 'Amount' });

      const sql = "SELECT tranid, entity, foreigntotal FROM transaction " +
                  "WHERE type = 'SalesOrd' ORDER BY trandate DESC FETCH FIRST 50 ROWS ONLY";
      const results = query.runSuiteQL({ query: sql }).asMappedResults();
      results.forEach((row, i) => {
        sublist.setSublistValue({ id: 'custpage_doc', line: i, value: row.tranid });
        sublist.setSublistValue({ id: 'custpage_amount', line: i, value: row.foreigntotal });
      });

      context.response.writePage(form);
    }
  }

  return { onRequest };
});
RESTlet -- API Endpoint
/**
 * @NApiVersion 2.1
 * @NScriptType Restlet
 */
define(['N/record', 'N/log'], (record, log) => {

  function get(context) {
    const { type, id } = context;
    if (!type || !id) return { error: 'Missing type or id parameter' };
    try {
      const rec = record.load({ type, id, isDynamic: false });
      return {
        id: rec.id, tranid: rec.getValue('tranid'),
        entity: rec.getText('entity'), total: rec.getValue('total'),
        status: rec.getText('status')
      };
    } catch (e) {
      log.error('GFS-RESTlet', e.message);
      return { error: e.message };
    }
  }

  function post(context) {
    const rec = record.create({ type: context.recordType, isDynamic: true });
    Object.keys(context.fields).forEach(fld => {
      rec.setValue({ fieldId: fld, value: context.fields[fld] });
    });
    return { success: true, id: rec.save() };
  }

  return { get, post };
});
Map/Reduce -- Batch Processing
/**
 * @NApiVersion 2.1
 * @NScriptType MapReduceScript
 */
define(['N/search', 'N/record', 'N/log'], (search, record, log) => {

  function getInputData() {
    return search.create({
      type: 'salesorder',
      filters: [
        ['status', 'anyof', 'SalesOrd:B'],
        'AND', ['trandate', 'onorbefore', 'daysago7']
      ],
      columns: ['tranid', 'entity', 'total']
    });
  }

  function map(context) {
    const result = JSON.parse(context.value);
    context.write({
      key: result.values.entity.value,
      value: { tranid: result.values.tranid, total: result.values.total }
    });
  }

  function reduce(context) {
    const orders = context.values.map(v => JSON.parse(v));
    log.audit('GFS-MR', 'Entity ' + context.key + ': ' + orders.length + ' stale orders');
  }

  function summarize(summary) {
    log.audit('GFS-MR Complete', 'Map errors: ' + summary.mapSummary.errors.length +
      ', Reduce errors: ' + summary.reduceSummary.errors.length);
  }

  return { getInputData, map, reduce, summarize };
});
Scheduled Script -- Nightly Processing
/**
 * @NApiVersion 2.1
 * @NScriptType ScheduledScript
 */
define(['N/search', 'N/record', 'N/email', 'N/runtime', 'N/log'], (search, record, email, runtime, log) => {

  function execute(context) {
    const script = runtime.getCurrentScript();
    log.audit('GFS-Sched', 'Start: ' + context.type + ' | Gov remaining: ' + script.getRemainingUsage());

    // Find invoices past due > 30 days
    const results = search.create({
      type: 'invoice',
      filters: [
        ['status', 'anyof', 'CustInvc:A'],
        'AND', ['duedate', 'before', 'daysago30'],
        'AND', ['amountremaining', 'greaterthan', 0]
      ],
      columns: ['tranid', 'entity', 'amountremaining', 'duedate']
    }).run().getRange({ start: 0, end: 100 });

    results.forEach(r => {
      // Check governance before each operation
      if (script.getRemainingUsage() < 100) {
        log.audit('GFS-Sched', 'Rescheduling - low governance');
        return;
      }
      log.audit('GFS-Sched', 'Past due: ' + r.getValue('tranid') +
        ' | $' + r.getValue('amountremaining'));
    });

    log.audit('GFS-Sched', 'Complete. Processed ' + results.length + ' records.');
  }

  return { execute };
});
Portlet Script -- Dashboard Widget
/**
 * @NApiVersion 2.1
 * @NScriptType Portlet
 */
define(['N/query', 'N/ui/serverWidget'], (query, sw) => {

  function render(params) {
    const portlet = params.portlet;
    portlet.title = 'GFS Open Orders Summary';
    portlet.addColumn({ id: 'custpage_metric', type: 'text', label: 'Metric', align: sw.LayoutJustification.LEFT });
    portlet.addColumn({ id: 'custpage_value', type: 'text', label: 'Value', align: sw.LayoutJustification.RIGHT });

    const sql = `SELECT
      COUNT(*) AS cnt,
      SUM(foreigntotal) AS total
    FROM transaction
    WHERE type = 'SalesOrd' AND status IN ('SalesOrd:A','SalesOrd:B')`;

    const result = query.runSuiteQL({ query: sql }).asMappedResults()[0];

    portlet.addRow({ custpage_metric: 'Open Sales Orders', custpage_value: String(result.cnt) });
    portlet.addRow({ custpage_metric: 'Total Value', custpage_value: '$' + Number(result.total).toLocaleString() });
  }

  return { render };
});
Mass Update Script -- Bulk Field Update
/**
 * @NApiVersion 2.1
 * @NScriptType MassUpdateScript
 */
define(['N/record', 'N/log'], (record, log) => {

  function each(params) {
    try {
      const rec = record.load({ type: params.type, id: params.id });
      const currentDept = rec.getValue({ fieldId: 'department' });

      if (!currentDept) {
        rec.setValue({ fieldId: 'department', value: 1 }); // Default: Dairy & Cheese
        rec.setValue({ fieldId: 'custbody_gfs_brand', value: 'GFS' });
        rec.save();
        log.audit('GFS-MassUpdate', 'Updated record ' + params.id);
      }
    } catch (e) {
      log.error('GFS-MassUpdate', 'Error on ' + params.id + ': ' + e.message);
    }
  }

  return { each };
});
Bundle Installation Script
/**
 * @NApiVersion 2.1
 * @NScriptType BundleInstallationScript
 */
define(['N/log', 'N/runtime'], (log, runtime) => {

  function beforeInstall(params) {
    log.audit('GFS-Bundle', 'Pre-install check. Version: ' + params.version);
    // Verify prerequisites
    const account = runtime.accountId;
    log.audit('GFS-Bundle', 'Installing to account: ' + account);
  }

  function afterInstall(params) {
    log.audit('GFS-Bundle', 'Bundle installed. Version: ' + params.version);
    // Initialize custom records, default values, etc.
  }

  function beforeUpdate(params) {
    log.audit('GFS-Bundle', 'Updating from ' + params.fromVersion + ' to ' + params.toVersion);
  }

  function afterUpdate(params) {
    log.audit('GFS-Bundle', 'Update complete. Running migrations...');
    // Data migration logic for version upgrades
  }

  function beforeUninstall(params) {
    log.audit('GFS-Bundle', 'Uninstall requested. Cleaning up...');
  }

  return { beforeInstall, afterInstall, beforeUpdate, afterUpdate, beforeUninstall };
});
Workflow Action Script -- Approval Logic
/**
 * @NApiVersion 2.1
 * @NScriptType WorkflowActionScript
 */
define(['N/record', 'N/email', 'N/runtime', 'N/log'], (record, email, runtime, log) => {

  function onAction(context) {
    const rec = context.newRecord;
    const total = rec.getValue({ fieldId: 'total' });
    const dept = rec.getText({ fieldId: 'department' });

    // Determine approval level based on amount
    let approverRole;
    if (total < 5000) {
      approverRole = 'auto-approved';
    } else if (total < 25000) {
      approverRole = 'L1-manager';
    } else {
      approverRole = 'L2-vp';
    }

    rec.setValue({ fieldId: 'custbody_gfs_approval_level', value: approverRole });

    // Send notification
    email.send({
      author: runtime.getCurrentUser().id,
      recipients: getApprover(approverRole),
      subject: 'GFS Approval Required: ' + rec.getValue('tranid') + ' ($' + total + ')',
      body: 'PO requires ' + approverRole + ' approval. Dept: ' + dept
    });

    log.audit('GFS-WF', rec.getValue('tranid') + ' routed to ' + approverRole);
    return approverRole;
  }

  function getApprover(role) {
    // Map roles to employee IDs
    const map = { 'L1-manager': 142, 'L2-vp': 87 };
    return map[role] || 1; // fallback to admin
  }

  return { onAction };
});

GFS Utility Functions Reference
FunctionModuleDescription
gfs.formatCurrency(val)gfs_utilFormat number as USD currency string
gfs.getSubsidiary()gfs_utilReturn current user's subsidiary (Edgewood/Hermitage)
gfs.validateItem(itemId)gfs_itemCheck item is active, in-stock, and not discontinued
gfs.getCMEPrice(date)gfs_pricingFetch CME barrel cheddar price for trailing week
gfs.calcMargin(cost, price)gfs_pricingCalculate gross margin percentage
gfs.sendNotification(opts)gfs_notifySend email/SMS notification with GFS template
gfs.checkCreditHold(custId)gfs_creditReturn boolean if customer is on credit hold
gfs.lotTrace(lotNum)gfs_qualityFull lot traceability from receipt through shipment
gfs.getDeliveryWindow(custId)gfs_deliveryReturn customer's preferred delivery window (AM/PM/anytime)
gfs.validateTemp(actual, zone)gfs_qualityValidate temperature reading against zone requirements
gfs.getRoute(locationId)gfs_deliveryReturn optimal delivery route for a location
gfs.calcRebate(custId, period)gfs_pricingCalculate rebate amount for customer in a period
gfs.convertWeight(val, from, to)gfs_utilConvert between lbs, kg, oz, cases
gfs.formatDate(d, fmt)gfs_utilFormat date to MM/DD/YYYY, YYYY-MM-DD, or ISO
gfs.checkGovernance(min)gfs_utilReturn true if remaining governance > min threshold
gfs.runSuiteQL(sql, params)gfs_dataExecute parameterized SuiteQL and return mapped results
gfs.paginate(search, pageSize)gfs_dataRun paged search execution with configurable page size
gfs.batchSave(records)gfs_dataSave array of records with governance check between each
gfs.logPerformance(label, fn)gfs_utilWrap function with timing and governance logging
■ All SuiteScript patterns use 2.1 API. Click "Copy" to copy any code block. Utility functions are in the gfs_* module library deployed as SuiteApp.
12 NetSuite Copy-Paste Snippets Ready-to-use HTML and JS blocks for rapid NetSuite development
Suitelet Page Skeleton (HTML)
<div class="ns-app-shell">
  <div class="ns-topbar">
    <div class="ns-topbar-left">
      <span class="ns-topbar-logo">&#9632; GFS</span>
      <div class="ns-topbar-divider"></div>
      <span class="ns-topbar-portal">[Portal Name]</span>
    </div>
    <div class="ns-topbar-right">
      <div class="ns-topbar-user">
        <div class="ns-topbar-avatar">ML</div>
        <span class="ns-topbar-name">M. Levine</span>
      </div>
    </div>
  </div>
  <div class="ns-breadcrumb">
    <a href="#">Home</a>
    <span class="ns-bc-sep">/</span>
    <span class="ns-bc-current">[Page]</span>
  </div>
  <div class="ns-app-body">
    <div class="ns-sidebar"><!-- Nav items --></div>
    <div class="ns-content"><!-- Page content --></div>
  </div>
  <div class="ns-app-footer">
    <span>Global Food Solutions, Inc.</span>
    <span>GFS Suitelet Framework v10.0</span>
  </div>
</div>
Record Form HTML
<div class="ns-record-header">
  <div>
    <div class="ns-record-type">Sales Order</div>
    <div class="ns-record-id">SO-XXXXX
      <span class="ns-badge ns-badge-pending">Pending</span>
    </div>
  </div>
</div>
<div class="ns-form-section" style="padding:0 24px;">
  <div class="ns-form-section-title">Primary Information</div>
  <div class="ns-2col">
    <div class="ns-field">
      <label class="ns-field-label">Customer <span class="ns-required">*</span></label>
      <select class="ns-input" required><option value="">-- Select --</option></select>
    </div>
  </div>
</div>
<div class="ns-form-actions" style="padding:0 24px 24px;">
  <button class="ns-btn ns-btn-primary">Save</button>
  <button class="ns-btn">Cancel</button>
</div>
Search Results Table HTML
Dashboard Portlet HTML
<div class="ns-portlet">
  <div class="ns-portlet-header">
    <span class="ns-portlet-title">Portlet Title</span>
    <div class="ns-portlet-actions">
      <button class="ns-btn ns-btn-ghost ns-btn-sm">&#9881;</button>
    </div>
  </div>
  <div class="ns-portlet-body">
    <div class="ns-kpi">
      <div class="ns-kpi-label">Metric Name</div>
      <div class="ns-kpi-value">142</div>
      <div class="ns-kpi-trend ns-up">+8 today</div>
    </div>
  </div>
</div>
Approval Workflow HTML
<div class="ns-approval-chain">
  <div class="ns-approval-step completed">
    <div class="ns-approval-node">&#10003;</div>
    <div class="ns-approval-name">Submitted</div>
    <div class="ns-approval-role">Requester</div>
  </div>
  <div class="ns-approval-step current">
    <div class="ns-approval-node">2</div>
    <div class="ns-approval-name">Level 1</div>
    <div class="ns-approval-role">Manager</div>
  </div>
  <div class="ns-approval-step pending">
    <div class="ns-approval-node">3</div>
    <div class="ns-approval-name">Final</div>
    <div class="ns-approval-role">VP</div>
  </div>
</div>
Print Template HTML
<div class="ns-print-template">
  <div class="ns-print-header">
    <div class="ns-print-co">
      <div class="ns-print-co-name">GLOBAL FOOD SOLUTIONS, INC.</div>
      <div class="ns-print-co-addr">131 Heartland Blvd, Edgewood, NY 11717</div>
    </div>
    <div class="ns-print-doc-type">
      <div class="ns-print-doc-type-name">[DOC TYPE]</div>
      <div class="ns-print-doc-type-id">#[ID]</div>
    </div>
  </div>
  <div class="ns-print-addr-row">
    <div class="ns-print-addr-box">
      <div class="ns-print-addr-label">Bill To</div>
      <div>[Address]</div>
    </div>
    <div class="ns-print-addr-box">
      <div class="ns-print-addr-label">Ship To</div>
      <div>[Address]</div>
    </div>
  </div>
  <div class="ns-print-footer">
    Global Food Solutions, Inc. | 131 Heartland Blvd, Edgewood, NY 11717
  </div>
</div>
Email Template HTML
<div class="ns-email-template">
  <div class="ns-email-header">
    <div class="ns-email-logo">&#9632; GLOBAL FOOD SOLUTIONS</div>
  </div>
  <div class="ns-email-body">
    <p>Dear [Name],</p>
    <p>[Message body]</p>
    <a href="#" class="ns-email-cta">[CTA Text]</a>
  </div>
  <div class="ns-email-footer">
    Global Food Solutions, Inc.<br>
    131 Heartland Blvd, Edgewood, NY 11717<br>
    (877) 728-5550 | globalfoodsolutions.co
  </div>
</div>
SuiteScript Client Validation
/**
 * @NApiVersion 2.1
 * @NScriptType ClientScript
 */
define(['N/currentRecord', 'N/ui/dialog'], (cr, dialog) => {
  function saveRecord() {
    const rec = cr.get();
    const required = ['entity', 'trandate', 'department'];
    for (const fld of required) {
      if (!rec.getValue({ fieldId: fld })) {
        dialog.alert({ title: 'Required', message: fld + ' is required.' });
        return false;
      }
    }
    return true;
  }
  return { saveRecord };
});
■ Click "Copy" on any snippet to copy to clipboard. Paste directly into NetSuite Suitelet HTML or SuiteScript file. All use GFS .ns- class conventions.
13 NetSuite Field Reference Complete field mapping reference for transactions, entities, items, and addresses
Transaction Body Fields
Internal IDTypeLabelReqGFS Usage Notes
tranidTextDocument NumberYAuto-generated. Format: SO-XXXXX, PO-XXXXX, INV-XXXXX
trandateDateDateYTransaction date. Defaults to today. Used in all reporting periods
entityList/RecordCustomer/VendorYPrimary entity on transaction. Drives pricing, terms, tax
subsidiaryList/RecordSubsidiaryYGFS Edgewood (1) or GFS Hermitage (2). Drives GL posting
departmentList/RecordDepartmentYDairy & Cheese, Frozen Foods, Deli & Prepared, Dry Goods
classList/RecordClassNProduct classification for P&L segmentation
locationList/RecordLocationYWarehouse location. Affects inventory and fulfillment
termsList/RecordTermsNNet 30, Net 60, Due on Receipt, Prepay. Defaults from customer
memoText AreaMemoNInternal notes. Include delivery instructions, temp requirements
statusList/RecordStatusNSystem-managed. Pending Approval, Pending Fulfillment, etc.
currencyList/RecordCurrencyYUSD (1). Multi-currency enabled but GFS primarily USD
exchangerateCurrencyExchange RateN1.0 for USD. Auto-set for foreign currency transactions
salesrepList/RecordSales RepNUsed for commission calculation and territory reporting
otherrefnumTextPO/Reference #NCustomer PO number. Required for invoicing certain accounts
shipdateDateShip DateNExpected ship date. Drives warehouse pick schedule
shipmethodList/RecordShip ViaNHeartland Express, Sysco Fleet, Customer Pickup, LTL
totalCurrencyTotalNCalculated. Sum of line amounts + tax + shipping
custbody_gfs_brandList/RecordBrandYGFS product brand. Maps to department field for reporting
custbody_gfs_temp_zoneList/RecordTemperature ZoneYRefrigerated (1), Frozen (2), Ambient (3). Drives carrier selection
custbody_gfs_routeList/RecordDelivery RouteNRoute assignment for delivery scheduling optimization
custbody_gfs_delivery_windowList/RecordDelivery WindowNAM (6-12), PM (12-6), Evening (6-10), Anytime
custbody_gfs_approval_levelTextApproval LevelNSet by workflow: auto-approved, L1-manager, L2-vp
custbody_gfs_priorityList/RecordPriorityNStandard, Rush, Emergency. Rush adds $150 surcharge
duedateDateDue DateNCalculated from terms. Used in AR aging and dunning
expectedreceiptdateDateExpected ReceiptNPO field. Vendor's promised delivery date
startdateDateStart DateNUsed on contracts and recurring orders
enddateDateEnd DateNContract end date. Triggers renewal notification 30 days prior
approvalstatusList/RecordApproval StatusN1=Pending, 2=Approved, 3=Rejected. Workflow managed
nextapproverList/RecordNext ApproverNEmployee ID of next approver in chain
createdfromList/RecordCreated FromNLinks to source transaction (SO to Invoice, PO to Receipt)
Line Item Fields
Internal IDTypeLabelReqGFS Usage Notes
itemList/RecordItemYItem internal ID. Lookup via itemid (GFS-4420) or barcode
quantityIntegerQuantityYOrder quantity in base units. Validates against available inventory
rateCurrencyRateYUnit price. May be CME-based, contract, or list price
amountCurrencyAmountNCalculated: quantity x rate. Can be overridden
taxcodeList/RecordTax CodeN-Not Taxable-, NY-8.625%, NJ-6.625%, PA-6%
departmentList/RecordDepartmentNLine-level department override. Inherits from header if blank
classList/RecordClassNLine-level classification for GL segment reporting
locationList/RecordLocationNShip-from warehouse. Multi-location fulfillment supported
descriptionTextDescriptionNDefaults from item. Can override for custom descriptions
unitsList/RecordUnitsNUnit of measure: Each, Case, Pallet, Lb
priceList/RecordPrice LevelNBase Price, Contract, Volume Discount, Promotional
costestimateCurrencyEst. CostNSystem-calculated COGS estimate. Used for margin analysis
custcol_gfs_lotTextLot NumberNLot assignment. Format: LOT-XX-MMDD[A-Z]. Required for USDA items
custcol_gfs_tempDecimalTemperature (F)NRecorded temperature at pick. Validated against zone requirements
custcol_gfs_weightDecimalActual Weight (lbs)NActual weight for catch-weight items. Used for billing adjustment
custcol_gfs_expiryDateExpiration DateNProduct expiration. Alerts if < 30 days remaining at ship
custcol_gfs_production_dateDateProduction DateNManufacturing date. Used for FIFO allocation
grossamtCurrencyGross AmountNAmount before discounts. Used in rebate calculations
lineuniquekeyIntegerLine Unique KeyNSystem-generated. Stable reference for line modifications
isclosedCheckboxClosedNClose individual lines. Prevents further fulfillment
Address Fields (addressbook subrecord)
Internal IDTypeLabelReqGFS Usage Notes
addr1TextAddress Line 1YStreet address. Required for all shipping/billing
addr2TextAddress Line 2NSuite, dock number, building. Include dock # for deliveries
cityTextCityYCity name for tax jurisdiction determination
stateList/RecordStateYTwo-letter code. Drives tax calculation and compliance
zipTextZip CodeY5 or 9 digit. Used for route planning and tax nexus
countryList/RecordCountryY_unitedStates default. Triggers export docs if international
attentionTextAttentionNContact name at delivery location
addresseeTextAddresseeNCompany name for delivery label
phonePhonePhoneNDelivery contact phone. Required for refrigerated shipments
Entity Fields (Customer/Vendor)
Internal IDTypeLabelReqGFS Usage Notes
companynameTextCompany NameYLegal entity name. Appears on invoices and legal docs
emailEmailEmailNPrimary contact email. Used for order confirmations
phonePhonePhoneNMain phone. Format: (XXX) XXX-XXXX
categoryList/RecordCategoryNDistributor, Restaurant, Retail, Institutional, Government
termsList/RecordTermsNDefault payment terms. Inherited by new transactions
creditlimitCurrencyCredit LimitNMaximum AR balance. Triggers credit hold when exceeded
balanceCurrencyBalanceNCurrent AR balance. System-calculated
overduebalanceCurrencyOverdue BalanceNPast-due AR. Used in dunning workflow
custentity_gfs_tierList/RecordCustomer TierNPlatinum, Gold, Silver, Bronze. Drives pricing and priority
custentity_gfs_buyerTextPrimary BuyerNBuyer contact name for order communications
custentity_gfs_licenseTextLicense NumberNUSDA/FDA facility license. Required for compliance
Item Fields
Internal IDTypeLabelReqGFS Usage Notes
itemidTextName/NumberYGFS item code. Format: GFS-XXXX (e.g., GFS-4420)
displaynameTextDisplay NameNFriendly name: "Barrel Cheddar 640#"
salesdescriptionTextSales DescriptionNDescription for invoices and quotes
purchasedescriptionTextPurchase DescriptionNDescription for purchase orders to vendors
costCurrencyCostNAverage, FIFO, or standard cost depending on method
basepriceCurrencyBase PriceNDefault selling price. Price level 1
weightDecimalWeightNShipping weight in lbs. Used for BOL and freight calculations
reorderpointIntegerReorder PointNMinimum stock level before reorder alert triggers
custitem_gfs_shelf_lifeIntegerShelf Life (days)NProduct shelf life in days from production
custitem_gfs_case_countIntegerCase CountNUnits per case. Used for pallet calculations
custitem_gfs_temp_reqList/RecordTemp RequirementYStorage/transport temperature zone requirement
custitem_gfs_allergensMulti-SelectAllergensNMilk, Soy, Wheat, Eggs, etc. For labeling compliance
■ Field IDs are case-sensitive in SuiteScript. Custom fields use custbody_ (transaction), custcol_ (line), custentity_ (entity), custitem_ (item) prefixes. Required fields validated in beforeSubmit user event.
14 Record Type Reference Every NetSuite record type GFS uses with internal IDs, status values, required fields, and workflows
Record TypeInternal IDStatus ValuesRequired FieldsGFS Workflows
Sales OrdersalesorderSalesOrd:A (Pending Approval)
SalesOrd:B (Pending Fulfillment)
SalesOrd:D (Partially Fulfilled)
SalesOrd:C (Pending Billing)
SalesOrd:F (Billed)
SalesOrd:H (Closed)
entity, trandate, subsidiary, department, itemCredit hold check, approval routing, fulfillment notification
Purchase OrderpurchaseorderPurchOrd:A (Pending Supervisor)
PurchOrd:B (Pending Receipt)
PurchOrd:D (Partially Received)
PurchOrd:C (Pending Bill)
PurchOrd:F (Fully Billed)
PurchOrd:H (Closed)
entity, trandate, subsidiary, department, itemMulti-level approval (5K/25K thresholds), vendor notification
InvoiceinvoiceCustInvc:A (Open)
CustInvc:B (Paid In Full)
entity, trandate, subsidiary, itemAuto-email on creation, dunning at 30/60/90 days
Vendor BillvendorbillVendBill:A (Open)
VendBill:B (Paid In Full)
entity, trandate, subsidiary, item3-way match validation (PO/Receipt/Bill)
Item FulfillmentitemfulfillmentItemShip:A (Picked)
ItemShip:B (Packed)
ItemShip:C (Shipped)
createdfrom, item, quantityShipping notification email, BOL generation
Item ReceiptitemreceiptItemRcpt:A (Received)createdfrom, item, quantityQC inspection trigger, lot assignment
Credit MemocreditmemoCustCred:A (Open)
CustCred:B (Fully Applied)
entity, trandate, itemApproval required > $5K. Auto-apply to oldest invoice
Return AuthorizationreturnauthorizationRtnAuth:A (Pending Approval)
RtnAuth:B (Pending Receipt)
RtnAuth:C (Pending Refund)
RtnAuth:F (Refunded)
RtnAuth:H (Closed)
entity, trandate, item, custbody_gfs_rma_reasonApproval required. Quality hold if food safety reason
Work OrderworkorderWorkOrd:A (Planned)
WorkOrd:B (Released)
WorkOrd:D (In Process)
WorkOrd:C (Built)
WorkOrd:H (Closed)
subsidiary, assemblyitem, quantityProduction schedule notification, BOM explosion
Transfer OrdertransferorderTrnfrOrd:A (Pending Approval)
TrnfrOrd:B (Pending Fulfillment)
TrnfrOrd:D (Partially Fulfilled)
TrnfrOrd:E (Pending Receipt)
TrnfrOrd:F (Received)
TrnfrOrd:H (Closed)
subsidiary, transferlocation, itemInter-warehouse transfer approval, temp logging
Vendor PaymentvendorpaymentVendPymt:A (Voided)
VendPymt:C (Online Bill Pay Pending)
entity, account, totalCheck run scheduling, ACH batch processing
Customer PaymentcustomerpaymentCustPymt:A (Deposited)
CustPymt:C (Not Deposited)
entity, payment, accountAuto-apply to invoices, deposit creation
EstimateestimateEstimate:A (Open)
Estimate:B (Processed)
Estimate:C (Closed)
Estimate:V (Voided)
entity, trandate, item30-day expiration, convert to SO on accept
OpportunityopportunityOpprtnty:A (In Progress)
Opprtnty:B (Issued Estimate)
Opprtnty:C (Closed - Won)
Opprtnty:D (Closed - Lost)
entity, title, probabilityPipeline reporting, sales rep notification
Customercustomer--companyname, subsidiaryCredit review on create, territory assignment
Vendorvendor--companyname, subsidiaryInsurance verification, W-9 compliance check
Employeeemployee--firstname, lastname, subsidiaryOnboarding checklist, role assignment
Inventory AdjustmentinventoryadjustmentInvAdjst:A (Approved)subsidiary, account, item, adjustqtybyApproval required. Audit trail for USDA compliance
Assembly BuildassemblybuildBuild:A (Built)item, quantity, subsidiaryBOM consumption, lot assignment, QC hold check
Journal EntryjournalentryJournal:A (Pending Approval)
Journal:B (Approved)
subsidiary, line(account, debit/credit)Approval required > $10K. Month-end close process
DepositdepositDeposit:A (Deposited)account, paymentBank reconciliation matching
Bill of Materialsbom--name, subsidiary, bomrevisionRevision control, production order BOM explosion
CasesupportcaseCase:1 (Not Started)
Case:2 (In Progress)
Case:3 (Escalated)
Case:4 (Re-Opened)
Case:5 (Closed)
company, title, statusAuto-assign by territory, SLA escalation at 24h
Tasktask--title, assignedFollow-up task auto-creation from workflows
Vendor Return AuthvendorreturnauthorizationVendAuth:A (Pending)
VendAuth:B (Returned)
VendAuth:H (Closed)
entity, itemVendor credit tracking, quality claim documentation

Record Type Quick Reference by Category
Sales Cycle
Opportunity > Estimate > Sales Order > Item Fulfillment > Invoice > Customer Payment > Deposit
Purchase Cycle
Purchase Order > Item Receipt > Vendor Bill > Vendor Payment
Returns Cycle
Return Authorization > Item Receipt > Credit Memo (or Replacement SO)
Manufacturing
Bill of Materials > Work Order > Assembly Build > Inventory Adjustment
Inventory
Transfer Order > Inventory Adjustment > Inventory Count > Bin Transfer
Financial
Journal Entry > Deposit > Bank Transfer > Intercompany Journal
■ Record type internal IDs are case-sensitive in SuiteScript. Status internal IDs use the format Type:Letter. Use record.Type enum in 2.1 scripts for type-safe references. Transaction cycles flow left-to-right; each step creates from the previous.
15 NetSuite Status Flows Visual status flow diagrams for each transaction type showing progression from creation to close
Sales Order Status Flow
Pending Approval
Pending Fulfillment
Partially Fulfilled
Fulfilled
Pending Billing
Billed
Closed
Purchase Order Status Flow
Pending Supervisor
Pending Receipt
Partially Received
Received
Pending Bill
Billed
Closed
Invoice Status Flow
Open
Paid In Full
Vendor Bill Status Flow
Open
Paid In Full
Work Order Status Flow
Planned
Released
In Process
Built
Closed
Transfer Order Status Flow
Pending Approval
Pending Fulfillment
Partially Fulfilled
Pending Receipt
Received
Closed
Return Authorization Status Flow
Pending Approval
Pending Receipt
Received
Pending Refund
Refunded
Closed
Item Fulfillment Status Flow
Picked
Packed
Shipped
■ Status flows show the standard progression. Records can skip statuses (e.g., direct to Billed). Closed status is terminal -- records cannot be reopened. Use status internal IDs (SalesOrd:B) in search filters and scripts.
17 Suitelet Best Practices Architecture patterns, HTML injection, session management, governance, and deployment checklists
Architecture Patterns
PatternWhen to UseGFS ExampleComplexity
Single PageSimple data display, read-only dashboards, portlet-style viewsGFS Pricing Dashboard, CME Price DisplayLow
Multi-TabRelated views in one Suitelet, settings panels, record detail with tabsGFS Operations Center, Customer 360 ViewMedium
Wizard (Multi-Step)Complex data entry, guided workflows, import toolsGFS New Customer Onboarding, Price UploadHigh
DashboardKPIs, charts, real-time metrics, executive viewsGFS Executive Dashboard, Delivery ScorecardMedium
API GatewaySuitelet as intermediary for external API callsGFS CME Price Fetcher, Webhook ReceiverMedium
HTML Injection vs serverWidget Forms
HTML Injection (form.addField inlinehtml)
  • + Full design control -- GFS brand consistency
  • + Modern UI: grids, charts, interactive elements
  • + Client-side interactivity with embedded JS
  • + Print-optimized templates
  • - No built-in NS form submission handling
  • - Must handle CSRF tokens manually
  • - Accessibility is developer responsibility
serverWidget Form (N/ui/serverWidget)
  • + Native NS form handling and submission
  • + Built-in CSRF protection
  • + Sublists with pagination out of the box
  • + Familiar NS look for internal users
  • - Limited styling -- looks like NetSuite
  • - No custom charts or complex layouts
  • - Tab/field type limitations
URL Parameter Handling
Suitelet -- URL Parameters and Routing
function onRequest(context) {
  const params = context.request.parameters;
  const page = params.page || 'dashboard';
  const recordId = params.id;
  const action = params.action; // view, edit, delete

  // Route to appropriate handler
  switch (page) {
    case 'dashboard': return renderDashboard(context);
    case 'record':    return renderRecord(context, recordId, action);
    case 'search':    return renderSearch(context, params);
    case 'export':    return handleExport(context, params);
    default:          return render404(context);
  }
}

// Generate internal Suitelet URL with parameters
const url = require('N/url');
const suiteletUrl = url.resolveScript({
  scriptId: 'customscript_gfs_ops',
  deploymentId: 'customdeploy_gfs_ops',
  params: { page: 'record', id: 12345, action: 'view' }
});
Governance Usage Table
API CallGovernance CostScript TypesNotes
record.load()5AllUse lookupFields for 1-2 fields instead
record.save()10AllBatch saves with governance check between each
record.create()5AllDynamic mode adds overhead but enables validation
record.delete()10AllRarely needed -- use inactivate instead
record.copy()5AllFull record copy including sublists
search.create().run()5AllInitial creation. Each getRange adds 10
search.lookupFields()1AllPreferred for reading 1-5 fields from a record
query.runSuiteQL()10AllPowerful but costs more. Cache results when possible
email.send()10AllBatch emails when possible. Daily limit: 10 per trigger
http.get() / .post()10AllExternal API calls. Cache responses in session/record
file.load()1AllLoad from File Cabinet. Use for templates, configs
file.create()5AllCreate in File Cabinet. CSV exports, PDF generation
render.xmlToPdf()10AllPDF generation from XML/HTML template
record.submitFields()2AllUpdate specific fields without loading full record
record.attach() / .detach()5AllLink/unlink records (e.g., file to transaction)
Governance limits per script type: Client=1,000 | User Event=1,000 | Suitelet=1,000 | RESTlet=5,000 | Scheduled=10,000 | Map/Reduce=10,000 per stage
Deployment Checklist
Testing Checklist
■ Use HTML injection for customer-facing portals (brand consistency). Use serverWidget for internal tools (faster dev). Always validate URL parameters server-side. Cache expensive operations.
18 RESTlet & API Patterns Authentication, request/response formats, pagination, batch operations, and endpoint inventory
Authentication Methods
MethodUse CaseSecurityGFS Status
Token-Based Auth (TBA)Production API access. Server-to-server. Cloudflare Workers.RecommendedActive - all endpoints
NLAuthLegacy scripts. Manual testing only.DeprecatedMigrating to TBA
OAuth 2.0User-context integrations. Web app delegated access.AvailablePlanned for portal
Request / Response Format Standard
GFS API Request Format
// REQUEST (POST /restlet?script=xxx&deploy=xxx)
{
  "action": "getRecord",           // Required: operation name
  "recordType": "salesorder",      // Required: NS record type
  "recordId": 12345,               // Required for get/update/delete
  "fields": {                      // Required for create/update
    "entity": 1001,
    "trandate": "2026-05-20",
    "department": 1,
    "custbody_gfs_brand": "GFS",
    "custbody_gfs_temp_zone": 1
  },
  "lines": [                       // Optional: sublist lines
    { "item": 4420, "quantity": 12, "rate": 2847.50 }
  ],
  "options": {                     // Optional: pagination, filters
    "page": 1,
    "pageSize": 50,
    "sortBy": "trandate",
    "sortDir": "DESC"
  }
}
GFS API Response Format
// SUCCESS RESPONSE
{
  "success": true,
  "data": {
    "id": 12345,
    "tranid": "SO-24891",
    "entity": { "id": 1001, "name": "Driscoll Foods" },
    "total": 46642.49,
    "status": { "id": "SalesOrd:B", "name": "Pending Fulfillment" }
  },
  "meta": {
    "timestamp": "2026-05-20T14:30:00Z",
    "governance": { "used": 15, "remaining": 4985 },
    "pagination": { "page": 1, "pageSize": 50, "totalResults": 247, "totalPages": 5 }
  }
}

// ERROR RESPONSE
{
  "success": false,
  "error": {
    "code": "RECORD_NOT_FOUND",
    "message": "Sales Order 99999 does not exist or you do not have permission.",
    "details": { "type": "salesorder", "id": 99999 }
  },
  "meta": {
    "timestamp": "2026-05-20T14:30:00Z"
  }
}
Pagination Pattern
RESTlet -- Paginated Search Results
function get(context) {
  const page = parseInt(context.page) || 1;
  const pageSize = Math.min(parseInt(context.pageSize) || 50, 100); // Max 100
  const start = (page - 1) * pageSize;

  const srch = search.create({
    type: 'salesorder',
    filters: [['status','anyof','SalesOrd:B']],
    columns: ['tranid','entity','total','trandate']
  });

  const totalCount = srch.runPaged({ pageSize: 1000 }).count;
  const results = srch.run().getRange({ start, end: start + pageSize });

  return {
    success: true,
    data: results.map(r => ({
      id: r.id,
      tranid: r.getValue('tranid'),
      entity: r.getText('entity'),
      total: r.getValue('total'),
      date: r.getValue('trandate')
    })),
    meta: {
      pagination: {
        page, pageSize,
        totalResults: totalCount,
        totalPages: Math.ceil(totalCount / pageSize),
        hasNext: start + pageSize < totalCount,
        hasPrev: page > 1
      }
    }
  };
}
Batch Operations Pattern
RESTlet -- Batch Record Updates
function post(context) {
  const { action, records } = context;
  if (action !== 'batchUpdate' || !Array.isArray(records)) {
    return { success: false, error: { code: 'INVALID_REQUEST', message: 'Expected batchUpdate action with records array' }};
  }

  const results = [];
  const script = runtime.getCurrentScript();

  for (const rec of records) {
    // Governance check before each operation
    if (script.getRemainingUsage() < 50) {
      results.push({ id: rec.id, success: false, error: 'GOVERNANCE_LIMIT' });
      break;
    }

    try {
      record.submitFields({
        type: rec.type,
        id: rec.id,
        values: rec.fields
      });
      results.push({ id: rec.id, success: true });
    } catch (e) {
      results.push({ id: rec.id, success: false, error: e.message });
    }
  }

  return {
    success: true,
    data: { processed: results.length, succeeded: results.filter(r => r.success).length, results },
    meta: { governance: { remaining: script.getRemainingUsage() }}
  };
}
GFS API Endpoint Inventory
EndpointScript IDMethodDescriptionAuth
Record CRUDcustomscript_gfs_api_recordGET/POST/PUT/DELETEUniversal record operationsTBA
Searchcustomscript_gfs_api_searchPOSTExecute saved searches with parametersTBA
SuiteQLcustomscript_gfs_api_queryPOSTRun SuiteQL queries with paginationTBA
File Uploadcustomscript_gfs_api_uploadPOSTUpload files to File Cabinet (SuiteAttach)TBA
Pricingcustomscript_gfs_api_pricingGETRetrieve item pricing and CME ratesTBA
Inventorycustomscript_gfs_api_inventoryGETReal-time inventory availability checkTBA
Customercustomscript_gfs_api_customerGET/POSTCustomer data and credit statusTBA
Webhook Receivercustomscript_gfs_api_webhookPOSTReceive external webhooks (Stripe, shipping)HMAC
Reportscustomscript_gfs_api_reportsGETPre-built report data (AR aging, revenue)TBA
Approvalcustomscript_gfs_api_approvalPOSTApprove/reject transactions via APITBA

Webhook Receiver Pattern
RESTlet -- Webhook Receiver with HMAC Validation
/**
 * @NApiVersion 2.1
 * @NScriptType Restlet
 * Receives webhooks from external services (Stripe, ShipStation, etc.)
 */
define(['N/record', 'N/log', 'N/crypto', 'N/encode'], (record, log, crypto, encode) => {

  function post(context) {
    const payload = JSON.stringify(context);
    log.audit('GFS-Webhook', 'Received: ' + payload.substring(0, 200));

    // Validate HMAC signature (if applicable)
    // const signature = context.headers['x-webhook-signature'];
    // const isValid = validateHMAC(payload, signature, SECRET_KEY);
    // if (!isValid) return { success: false, error: 'INVALID_SIGNATURE' };

    // Route by event type
    const eventType = context.event || context.type;
    switch (eventType) {
      case 'payment.received':
        return handlePaymentReceived(context.data);
      case 'shipment.delivered':
        return handleShipmentDelivered(context.data);
      case 'order.updated':
        return handleOrderUpdated(context.data);
      default:
        log.audit('GFS-Webhook', 'Unhandled event: ' + eventType);
        return { success: true, message: 'Event acknowledged but not processed' };
    }
  }

  function handlePaymentReceived(data) {
    try {
      record.submitFields({
        type: 'invoice',
        id: data.invoiceId,
        values: { custbody_gfs_payment_ref: data.paymentRef }
      });
      return { success: true, message: 'Payment recorded' };
    } catch (e) {
      return { success: false, error: e.message };
    }
  }

  return { post };
});
Rate Limiting Considerations
Limit TypeValueScopeWhat Happens
Concurrent Requests10 per deploymentPer script deploymentAdditional requests queue, then timeout at 30s
Governance per Execution5,000 units (RESTlet)Per single requestSSS_USAGE_LIMIT_EXCEEDED error
Request Size10 MBPer request bodyREQUEST_SIZE_EXCEEDED error
Response Size10 MBPer response bodyTruncation or timeout
Daily Email Limit10 per trigger typePer script executionEmails silently dropped after limit
Integration Requests/HourVaries by account tierAccount-wide429 Too Many Requests
■ All GFS RESTlets use TBA authentication. Standard response envelope includes success, data, and meta. Pagination maxes at 100 per page. Rate limit: 10 concurrent requests per deployment. Webhook receivers should validate signatures before processing.
19 Custom Record Reference All GFS custom records with field definitions, usage patterns, and linked record relationships
Temperature Log (custrecord_gfs_temp_log)
Field IDTypeLabelDescription
custrecord_gfs_temp_dateDate/TimeLog Date/TimeTimestamp of temperature reading
custrecord_gfs_temp_locationList/RecordLocationWarehouse, truck, or dock location
custrecord_gfs_temp_itemList/RecordItemItem being monitored
custrecord_gfs_temp_lotTextLot NumberLot being monitored
custrecord_gfs_temp_readingDecimalTemperature (F)Actual temperature reading in Fahrenheit
custrecord_gfs_temp_zoneList/RecordRequired ZoneRequired zone: Refrigerated (34-38F), Frozen (-10-0F), Ambient
custrecord_gfs_temp_statusList/RecordStatusNormal, Warning, Exception, Critical
custrecord_gfs_temp_deviationDecimalDeviation (F)Degrees outside acceptable range. 0 = normal
custrecord_gfs_temp_employeeList/RecordLogged ByEmployee who recorded the reading
custrecord_gfs_temp_transactionList/RecordRelated TransactionLink to SO, PO, or fulfillment record
custrecord_gfs_temp_actionText AreaAction TakenCorrective action if exception
Vendor Scorecard (custrecord_gfs_vendor_score)
Field IDTypeLabelDescription
custrecord_gfs_vs_vendorList/RecordVendorVendor entity reference
custrecord_gfs_vs_periodList/RecordPeriodAccounting period for the scorecard
custrecord_gfs_vs_otd_pctPercentOn-Time Delivery %Percentage of POs received on or before expected date
custrecord_gfs_vs_quality_pctPercentQuality Score %Percentage of receipts passing QC inspection
custrecord_gfs_vs_price_varCurrencyPrice VarianceActual vs. contracted price variance
custrecord_gfs_vs_total_spendCurrencyTotal SpendTotal PO value for the period
custrecord_gfs_vs_overallList/RecordOverall RatingA (Excellent), B (Good), C (Needs Improvement), D (At Risk), F (Unacceptable)
custrecord_gfs_vs_notesText AreaNotesReviewer comments and action items
Lot Trace (custrecord_gfs_lot_trace)
Field IDTypeLabelDescription
custrecord_gfs_lt_lot_numberTextLot NumberGFS lot identifier. Format: LOT-XX-MMDD[A-Z]
custrecord_gfs_lt_itemList/RecordItemProduct item reference
custrecord_gfs_lt_vendorList/RecordSource VendorVendor who supplied this lot
custrecord_gfs_lt_vendor_lotTextVendor Lot #Vendor's own lot number for traceability
custrecord_gfs_lt_receivedDateReceived DateDate lot was received at GFS warehouse
custrecord_gfs_lt_productionDateProduction DateManufacturer's production date
custrecord_gfs_lt_expiryDateExpiration DateProduct expiration. Triggers alerts at 30/15/7 days
custrecord_gfs_lt_qtyIntegerQuantityTotal quantity in lot (base units)
custrecord_gfs_lt_parent_poList/RecordSource POPurchase order that received this lot
custrecord_gfs_lt_statusList/RecordStatusAvailable, On Hold, Allocated, Shipped, Expired
Delivery Route (custrecord_gfs_route)
Field IDTypeLabelDescription
custrecord_gfs_rt_nameTextRoute Namee.g., "NE-Metro-AM", "LI-East-PM"
custrecord_gfs_rt_driverList/RecordAssigned DriverEmployee assigned to route
custrecord_gfs_rt_vehicleTextVehicle IDTruck/trailer identifier
custrecord_gfs_rt_dayMulti-SelectDelivery DaysMon, Tue, Wed, Thu, Fri delivery schedule
custrecord_gfs_rt_windowList/RecordWindowAM (6-12), PM (12-6), Evening (6-10)
custrecord_gfs_rt_stopsIntegerMax StopsMaximum delivery stops per route run
custrecord_gfs_rt_capacityDecimalCapacity (lbs)Maximum weight capacity for the vehicle
custrecord_gfs_rt_tempList/RecordTemp CapabilityRefrigerated, Frozen, Dual-Temp, Ambient
Rebate Program (custrecord_gfs_rebate)
Field IDTypeLabelDescription
custrecord_gfs_reb_customerList/RecordCustomerCustomer entity eligible for rebate
custrecord_gfs_reb_typeList/RecordRebate TypeVolume, Growth, Promotional, Year-End
custrecord_gfs_reb_ratePercentRebate Rate %Percentage of qualifying purchases rebated
custrecord_gfs_reb_thresholdCurrencyVolume ThresholdMinimum purchase volume to qualify
custrecord_gfs_reb_startDateStart DateProgram effective start
custrecord_gfs_reb_endDateEnd DateProgram expiration. Triggers renewal review
custrecord_gfs_reb_accruedCurrencyAccrued AmountCurrent period accrued rebate. Updated by scheduled script
custrecord_gfs_reb_statusList/RecordStatusActive, Pending Approval, Expired, Paid Out
Production Run (custrecord_gfs_production)
Field IDTypeLabelDescription
custrecord_gfs_pr_work_orderList/RecordWork OrderLink to NetSuite work order
custrecord_gfs_pr_itemList/RecordAssembly ItemFinished product being produced
custrecord_gfs_pr_qty_plannedIntegerPlanned QtyTarget production quantity
custrecord_gfs_pr_qty_actualIntegerActual QtyActual produced quantity
custrecord_gfs_pr_yield_pctPercentYield %Actual / Planned percentage
custrecord_gfs_pr_startDate/TimeStart TimeProduction run start
custrecord_gfs_pr_endDate/TimeEnd TimeProduction run completion
custrecord_gfs_pr_lotTextLot AssignedLot number assigned to finished goods
custrecord_gfs_pr_statusList/RecordStatusScheduled, In Progress, Complete, QC Hold
Quality Hold (custrecord_gfs_quality_hold)
Field IDTypeLabelDescription
custrecord_gfs_qh_itemList/RecordItemItem placed on quality hold
custrecord_gfs_qh_lotTextLot NumberAffected lot number
custrecord_gfs_qh_reasonList/RecordHold ReasonTemperature Excursion, Foreign Material, Label Error, Customer Complaint, Recall
custrecord_gfs_qh_dateDateHold DateDate hold was placed
custrecord_gfs_qh_placed_byList/RecordPlaced ByEmployee who initiated hold
custrecord_gfs_qh_qtyIntegerQuantity HeldQuantity of product on hold
custrecord_gfs_qh_dispositionList/RecordDispositionPending, Released, Destroyed, Returned to Vendor, Reworked
custrecord_gfs_qh_released_byList/RecordReleased ByQA manager who approved release
custrecord_gfs_qh_released_dateDateReleased DateDate hold was released
custrecord_gfs_qh_notesText AreaInvestigation NotesRoot cause analysis and corrective actions
■ Custom records use custrecord_ prefix. All custom records have Include Name Field enabled for search display. Parent/child relationships use List/Record fields with Record Type sourcing. Permissions set at record type level.
20 Print Template FreeMarker NetSuite Advanced PDF/HTML template patterns with FreeMarker syntax, conditionals, and loops
FreeMarker Syntax Reference
SyntaxDescriptionExample
${record.tranid}Output field valueSO-24891
${record.entity}Output entity display nameDriscoll Foods
${record.trandate?string("MM/dd/yyyy")}Format date05/20/2026
${record.total?string.currency}Format as currency$46,642.49
<#list record.item as line>Loop over sublist linesIterate each line item
<#if record.total > 25000>Conditional displayShow if total exceeds threshold
${line_index + 1}Line number (0-based index)1, 2, 3...
<#assign total = 0>Assign variableRunning total accumulator
${record.custbody_gfs_brand}Custom body fieldGFS, Private Label
${line.custcol_gfs_lot}Custom column fieldLOT-BC-0520A
GFS Invoice Template (FreeMarker)
Advanced PDF -- Invoice Template
<?xml version="1.0"?>
<!DOCTYPE pdf PUBLIC "-//big.faceless.org//report" "report-1.1.dtd">
<pdf>
<head>
  <style>
    body { font-family: sans-serif; font-size: 10pt; color: #092F64; }
    .header { border-top: 3px solid #092F64; padding: 12px 0; }
    .co-name { font-size: 14pt; font-weight: bold; letter-spacing: 0.5px; }
    table.items { width: 100%; border-collapse: collapse; margin: 12px 0; }
    table.items th { background: #092F64; color: #fff; padding: 6px 8px;
      font-size: 8pt; text-transform: uppercase; text-align: left; }
    table.items td { padding: 6px 8px; border-bottom: 1px solid #D9E1EA; }
    .total-row { background: #092F64; color: #fff; font-weight: bold; }
  </style>
</head>
<body>
  <div class="header">
    <table width="100%"><tr>
      <td><span class="co-name">GLOBAL FOOD SOLUTIONS, INC.</span><br/>
        131 Heartland Blvd, Edgewood, NY 11717<br/>(877) 728-5550</td>
      <td align="right"><span style="font-size:18pt;font-weight:bold">INVOICE</span><br/>
        #${record.tranid}</td>
    </tr></table>
  </div>

  <table width="100%"><tr>
    <td width="50%" style="border:1px solid #D9E1EA;padding:8px">
      <b style="font-size:8pt;text-transform:uppercase">Bill To</b><br/>
      ${record.billaddress}</td>
    <td width="50%" style="border:1px solid #D9E1EA;padding:8px">
      <b style="font-size:8pt;text-transform:uppercase">Ship To</b><br/>
      ${record.shipaddress}</td>
  </tr></table>

  <table><tr>
    <td>Date: ${record.trandate?string("MM/dd/yyyy")}</td>
    <td>Due: ${record.duedate?string("MM/dd/yyyy")}</td>
    <td>Terms: ${record.terms}</td>
    <td>Rep: ${record.salesrep}</td>
  </tr></table>

  <table class="items">
    <tr><th>Item</th><th>Description</th><th align="right">Qty</th>
      <th align="right">Rate</th><th align="right">Amount</th></tr>
    <#list record.item as line>
    <tr>
      <td>${line.item}</td>
      <td>${line.description}</td>
      <td align="right">${line.quantity}</td>
      <td align="right">${line.rate?string.currency}</td>
      <td align="right">${line.amount?string.currency}</td>
    </tr>
    </#list>
  </table>

  <table align="right" width="200px">
    <tr><td>Subtotal</td><td align="right">${record.subtotal?string.currency}</td></tr>
    <tr><td>Tax</td><td align="right">${record.taxtotal?string.currency}</td></tr>
    <tr><td>Shipping</td><td align="right">${record.shippingcost?string.currency}</td></tr>
    <tr class="total-row"><td>Total Due</td><td align="right">${record.total?string.currency}</td></tr>
  </table>

  <#if record.total > 50000>
  <p style="color:#DC2626;font-weight:bold">NOTE: This invoice exceeds $50,000. Wire transfer preferred.</p>
  </#if>
</body>
</pdf>
GFS Packing Slip with Lot Numbers
Advanced PDF -- Packing Slip with Lots
<!-- Key section: Line items with lot tracking -->
<table class="items">
  <tr><th>Item</th><th>Description</th><th align="right">Ordered</th>
    <th align="right">Shipped</th><th>Lot #</th><th>Expiry</th><th>Temp</th></tr>
  <#list record.item as line>
  <tr>
    <td>${line.item}</td>
    <td>${line.description}</td>
    <td align="right">${line.quantityordered!""}</td>
    <td align="right">${line.quantity}</td>
    <td>${line.custcol_gfs_lot!""}</td>
    <td><#if line.custcol_gfs_expiry?has_content>${line.custcol_gfs_expiry?string("MM/dd/yyyy")}</#if></td>
    <td><#if line.custcol_gfs_temp?has_content>${line.custcol_gfs_temp}F</#if></td>
  </tr>
  </#list>
</table>

<!-- Cold chain compliance note -->
<#if record.custbody_gfs_temp_zone?has_content>
  <#if record.custbody_gfs_temp_zone == "1">
    <p style="border:1px solid #0F766E;background:#F0FDF4;padding:6px;font-size:9pt">
      COLD CHAIN: Maintain 34-38F. Do not break cold chain. Inspect upon delivery.</p>
  <#elseif record.custbody_gfs_temp_zone == "2">
    <p style="border:1px solid #092F64;background:#E9F5FF;padding:6px;font-size:9pt">
      FROZEN: Maintain -10 to 0F. Do not refreeze if thawed.</p>
  </#if>
</#if>

<!-- Signature block -->
<table width="100%" style="margin-top:20px"><tr>
  <td width="25%">Received by: ____________</td>
  <td width="25%">Date: ____________</td>
  <td width="25%">Condition: ____________</td>
  <td width="25%">Temp at delivery: ____F</td>
</tr></table>
Conditional Display Patterns
FreeMarker -- Conditional Patterns
<!-- Show credit memo notice -->
<#if record.type == "creditmemo">
  <p style="color:#DC2626;font-weight:bold">CREDIT MEMO - This document reduces your account balance.</p>
</#if>

<!-- International shipping notice -->
<#if record.shipcountry != "US">
  <p>INTERNATIONAL SHIPMENT - Additional customs documentation required.
  HS Code and country of origin included on commercial invoice.</p>
</#if>

<!-- Multi-currency handling -->
<#if record.currency != "1">
  <p>Currency: ${record.currency} | Exchange Rate: ${record.exchangerate}
  | USD Equivalent: ${(record.total * record.exchangerate)?string.currency}</p>
</#if>

<!-- Null-safe field access -->
${record.memo!"No memo provided"}
${record.custbody_gfs_route!"Route not assigned"}
<#if record.otherrefnum?has_content>PO#: ${record.otherrefnum}</#if>

<!-- Line-level conditionals -->
<#list record.item as line>
  <tr style="<#if line.amount > 10000>font-weight:bold;background:#FFF7ED</#if>">
    <td>${line.item}</td>
    <td align="right">${line.amount?string.currency}</td>
    <td><#if line.custcol_gfs_lot?has_content>${line.custcol_gfs_lot}<#else>N/A</#if></td>
  </tr>
</#list>

<!-- Barcode generation -->
<barcode codetype="code128" showtext="true" value="${record.tranid}"/>
Page Header/Footer with Company Branding
FreeMarker -- Recurring Header/Footer
<macrolist>
  <macro id="nlheader">
    <table width="100%"><tr>
      <td style="font-size:8pt;color:#092F64">GLOBAL FOOD SOLUTIONS, INC.</td>
      <td align="right" style="font-size:8pt;color:#6B7280">
        ${record.type?upper_case} #${record.tranid} | Page <pagenumber/> of <totalpages/>
      </td>
    </tr></table>
    <hr style="border-top:1px solid #D9E1EA" />
  </macro>

  <macro id="nlfooter">
    <hr style="border-top:1px solid #D9E1EA" />
    <table width="100%"><tr>
      <td style="font-size:7pt;color:#6B7280">
        Global Food Solutions, Inc. | 131 Heartland Blvd, Edgewood, NY 11717 | (877) 728-5550
      </td>
      <td align="right" style="font-size:7pt;color:#6B7280">
        Printed: ${.now?string("MM/dd/yyyy HH:mm")}
      </td>
    </tr></table>
  </macro>
</macrolist>
■ FreeMarker templates use ${} for output and <#> for directives. Use ?has_content for null checks and ! for defaults. Barcode generation requires codetype attribute. Test in PDF Preview before deploying to production.
21 Workflow & Approval Patterns Multi-level approval matrix, conditional routing, escalation rules, and workflow state diagrams
GFS Approval Matrix
Amount RangeApproval LevelApproverSLAEscalation
$0 - $4,999Auto-ApprovedSystem (no human)InstantN/A
$5,000 - $24,999Level 1Department Manager24 hoursAuto-escalate to L2 after 48h
$25,000 - $99,999Level 2VP Operations + CFO48 hoursEmail reminder at 24h, escalate to CEO at 72h
$100,000+Level 3VP + CFO + CEO72 hoursBoard notification for > $250K
Conditional Routing Rules
ConditionRoute ToAdditional Action
Department = ProductionProduction Manager (before standard chain)Add QC checklist requirement
Department = Dairy & CheeseDairy Operations LeadVerify USDA compliance fields
Vendor = new (first PO)Purchasing DirectorRequire W-9 and insurance verification
Item = hazmat classifiedSafety Compliance OfficerAttach SDS documentation
Ship Country != USExport ComplianceGenerate customs documentation
Customer on credit holdCredit ManagerBlock fulfillment until released
Rush order (priority = Rush)Skip L1, direct to L2Add $150 surcharge, alert warehouse
PO Approval Workflow State Diagram
Draft
Submitted
L1 Review
L2 Review
Approved
Reject at any stage -- returns to Draft with rejection reason
Delegate -- forward to alternate approver, preserves SLA clock
Credit Hold Release Workflow
Credit Hold
Payment Received
Credit Review
Released
Resume Fulfillment
Email Notifications at Each Step
TriggerRecipientsSubject TemplateIncludes
PO SubmittedNext approverGFS Approval Required: ${tranid} ($${total})Record link, vendor, amount, line items
PO Approved (L1)Submitter + next approverGFS L1 Approved: ${tranid} -- Routing to L2Approval comment, approver name, timestamp
PO Approved (Final)Submitter + purchasing + vendorGFS PO Approved: ${tranid} Ready for ProcessingFull PO details, delivery instructions
PO RejectedSubmitterGFS PO Rejected: ${tranid} -- Action RequiredRejection reason, rejector comment, resubmit link
Escalation (48h)Original approver + managerGFS OVERDUE: ${tranid} awaiting approval (48h+)Time elapsed, direct approval link
Credit Hold PlacedCredit manager + sales repGFS Credit Hold: ${entity} -- Orders blockedCurrent balance, credit limit, overdue amount
Credit Hold ReleasedSales rep + warehouseGFS Credit Released: ${entity} -- Resume fulfillmentPending orders list, new credit limit
Custom Action Buttons
User Event -- Add Custom Buttons to Record
/**
 * @NApiVersion 2.1
 * @NScriptType UserEventScript
 */
define(['N/runtime', 'N/url'], (runtime, url) => {

  function beforeLoad(context) {
    if (context.type !== context.UserEventType.VIEW) return;
    const form = context.form;
    const rec = context.newRecord;
    const status = rec.getValue('approvalstatus');

    // Show approve/reject buttons only for current approver
    const currentUser = runtime.getCurrentUser().id;
    const nextApprover = rec.getValue('nextapprover');

    if (status === '1' && currentUser === nextApprover) {
      form.addButton({
        id: 'custpage_approve',
        label: 'Approve',
        functionName: 'gfsApprove'
      });
      form.addButton({
        id: 'custpage_reject',
        label: 'Reject',
        functionName: 'gfsReject'
      });
    }

    // Always show audit trail button
    const auditUrl = url.resolveScript({
      scriptId: 'customscript_gfs_audit',
      deploymentId: 'customdeploy_gfs_audit',
      params: { recordId: rec.id, recordType: rec.type }
    });
    form.addButton({
      id: 'custpage_audit',
      label: 'View Audit Trail',
      functionName: 'window.open("' + auditUrl + '")'
    });

    form.clientScriptModulePath = './gfs_cs_approval.js';
  }

  return { beforeLoad };
});
■ Approval workflows use SuiteFlow (Customization > Workflows). Amount thresholds configured in the workflow itself. Escalation uses scheduled workflow actions. All approval events logged for audit trail. Delegation preserves the original SLA clock.
22 Error Handling & Validation Client script validation, error codes, try/catch patterns, governance limits, and conflict handling
Client Script Validation Patterns
Complete Client Script -- All Entry Points
/**
 * @NApiVersion 2.1
 * @NScriptType ClientScript
 */
define(['N/currentRecord', 'N/ui/dialog', 'N/ui/message'], (cr, dialog, message) => {

  function pageInit(context) {
    // Show warning banner if record is on credit hold
    const rec = cr.get();
    if (rec.getValue('custbody_gfs_credit_hold')) {
      message.create({
        type: message.Type.WARNING,
        title: 'Credit Hold',
        message: 'This customer is on credit hold. Order will not be fulfilled until released.'
      }).show();
    }
  }

  function saveRecord(context) {
    const rec = cr.get();
    const errors = [];

    // Required field validation
    if (!rec.getValue('entity')) errors.push('Customer is required.');
    if (!rec.getValue('trandate')) errors.push('Date is required.');
    if (!rec.getValue('department')) errors.push('Department is required.');
    if (!rec.getValue('custbody_gfs_temp_zone')) errors.push('Temperature Zone is required.');

    // Line item validation
    const lineCount = rec.getLineCount({ sublistId: 'item' });
    if (lineCount === 0) errors.push('At least one line item is required.');

    for (let i = 0; i < lineCount; i++) {
      const qty = rec.getSublistValue({ sublistId: 'item', fieldId: 'quantity', line: i });
      const rate = rec.getSublistValue({ sublistId: 'item', fieldId: 'rate', line: i });
      if (qty <= 0) errors.push('Line ' + (i + 1) + ': Quantity must be greater than 0.');
      if (rate <= 0) errors.push('Line ' + (i + 1) + ': Rate must be greater than 0.');
    }

    // Business rule: minimum order value
    const total = rec.getValue('total');
    if (total < 100) errors.push('Minimum order value is $100.00.');

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

  function fieldChanged(context) {
    const rec = cr.get();
    if (context.fieldId === 'entity') {
      // Auto-populate terms, rep, and delivery window from customer defaults
      const custId = rec.getValue('entity');
      if (custId) {
        // Use lookupFields in a real implementation
        console.log('Customer changed to: ' + custId);
      }
    }
    if (context.sublistId === 'item' &&
        (context.fieldId === 'quantity' || context.fieldId === 'rate')) {
      const qty = rec.getCurrentSublistValue({ sublistId: 'item', fieldId: 'quantity' }) || 0;
      const rate = rec.getCurrentSublistValue({ sublistId: 'item', fieldId: 'rate' }) || 0;
      rec.setCurrentSublistValue({ sublistId: 'item', fieldId: 'amount', value: qty * rate });
    }
  }

  function validateLine(context) {
    if (context.sublistId !== 'item') return true;
    const rec = cr.get();
    const item = rec.getCurrentSublistValue({ sublistId: 'item', fieldId: 'item' });
    if (!item) {
      dialog.alert({ title: 'Validation', message: 'Please select an item before adding the line.' });
      return false;
    }
    return true;
  }

  function sublistChanged(context) {
    if (context.sublistId === 'item') {
      // Recalculate totals, update UI elements
      const rec = cr.get();
      const lineCount = rec.getLineCount({ sublistId: 'item' });
      console.log('Sublist changed. Lines: ' + lineCount);
    }
  }

  return { pageInit, saveRecord, fieldChanged, validateLine, sublistChanged };
});
Common NS Error Codes and User-Friendly Messages
Error CodeCauseUser-Friendly MessageResolution
RCRD_DSNT_EXISTRecord not found or deleted"This record no longer exists. It may have been deleted."Refresh search, verify ID
INSUFFICIENT_PERMISSIONRole lacks permission"You don't have permission to perform this action. Contact admin."Check role permissions
RCRD_HAS_BEEN_CHANGEDConcurrent edit conflict"Another user modified this record. Please refresh and try again."Reload record, re-apply changes
SSS_USAGE_LIMIT_EXCEEDEDGovernance limit hit"System is processing too many records. Please try again shortly."Optimize script, use Map/Reduce
UNIQUE_CUST_ID_REQDDuplicate entity ID"A customer with this ID already exists."Use unique identifier
INVALID_FLD_VALUEWrong field value type"Invalid value for this field. Please check the format."Validate data type before set
SSS_REQUEST_LIMIT_EXCEEDEDToo many concurrent requests"Server is busy. Please wait a moment and retry."Implement retry with backoff
FIELD_REQDRequired field missing"Please fill in all required fields before saving."Check required fields list
CC_PROCESSOR_ERRORCredit card processing failed"Payment processing failed. Please verify payment details."Check CC details, retry
SSS_TIME_LIMIT_EXCEEDEDScript execution timeout"Operation timed out. Large operations may need to run overnight."Break into smaller batches
Try/Catch Pattern for SuiteScript
Error Handling -- Best Practice Pattern
function safeRecordOperation(recordType, recordId, operation) {
  try {
    const rec = record.load({ type: recordType, id: recordId, isDynamic: true });
    const result = operation(rec);
    const savedId = rec.save({ enableSourcing: true, ignoreMandatoryFields: false });
    log.audit('GFS-Op', 'Success: ' + recordType + ' ' + savedId);
    return { success: true, id: savedId, data: result };

  } catch (e) {
    // Categorize errors for appropriate handling
    if (e.name === 'RCRD_HAS_BEEN_CHANGED') {
      log.audit('GFS-Op', 'Conflict on ' + recordId + '. Retrying...');
      // Retry once after conflict
      return safeRecordOperation(recordType, recordId, operation);
    }

    if (e.name === 'SSS_USAGE_LIMIT_EXCEEDED') {
      log.error('GFS-Op', 'Governance exceeded. Remaining: ' +
        runtime.getCurrentScript().getRemainingUsage());
      return { success: false, error: 'GOVERNANCE_LIMIT', message: 'Script governance limit reached.' };
    }

    if (e.name === 'RCRD_DSNT_EXIST') {
      log.audit('GFS-Op', 'Record not found: ' + recordType + ' ' + recordId);
      return { success: false, error: 'NOT_FOUND', message: 'Record does not exist.' };
    }

    // Unknown error -- log full details
    log.error('GFS-Op', JSON.stringify({
      name: e.name, message: e.message, stack: e.stack,
      recordType, recordId
    }));
    return { success: false, error: e.name, message: e.message };
  }
}
Error Message UI Patterns
User-Facing Error Display Methods
// 1. Inline field error (highlight field + message below)
function showFieldError(fieldId, msg) {
  const field = document.getElementById(fieldId);
  if (field) {
    field.classList.add('ns-input-error');
    const errEl = document.getElementById('err-' + fieldId);
    if (errEl) { errEl.textContent = msg; errEl.style.display = 'block'; }
  }
}

// 2. Banner warning (top of form, dismissible)
// Uses N/ui/message module in SuiteScript
const banner = message.create({
  type: message.Type.ERROR,
  title: 'Save Failed',
  message: 'Unable to save record. ' + errorDetails,
  duration: 10000  // auto-dismiss after 10s
});
banner.show();

// 3. Modal alert (blocking, requires user action)
dialog.alert({
  title: 'Critical Error',
  message: 'This operation cannot be completed.\n\n' + errorDetails
}).then(() => {
  // User clicked OK -- redirect or reload
  window.location.reload();
});

// 4. Toast notification (non-blocking, auto-dismiss)
// Using GFS custom toast (from this design system)
nsShowToast('Record saved successfully.', 'ns-toast-success');
nsShowToast('Unable to save. Try again.', 'ns-toast-error');
nsShowToast('Unsaved changes detected.', 'ns-toast-warning');
■ Always wrap record operations in try/catch. Use specific error code checks before generic catch. Log full error details (name, message, stack) for debugging. Show user-friendly messages -- never expose raw error objects.
23 Deployment & Governance Script deployment checklist, sandbox promotion, rollback, version control, and go-live procedures
Script Deployment Checklist
Governance Budget Table (All Operations)
OperationUnitsCategoryOptimization Tip
record.load()5RecordUse lookupFields() for 1-5 fields (cost: 1)
record.save()10RecordUse submitFields() for field-only updates (cost: 2)
record.create() + save()15RecordBatch creates in Map/Reduce for volume
record.delete()10RecordConsider inactivation instead
record.submitFields()2RecordBest for updating specific fields
search.lookupFields()1SearchCheapest way to read record data
search.create().run()5SearchEach getRange(start,end) adds 10 units
search.run().getRange()10SearchMax 1000 results per getRange call
query.runSuiteQL()10QueryPowerful but expensive -- cache results
email.send()10Email10 emails per script trigger max. Batch if possible
http.get() / .post()10HTTPCache external API responses
file.load()1FileCache file contents in variables
file.create() + save()5FileWrite to File Cabinet for large outputs
render.xmlToPdf()10RenderGenerate PDFs only when needed
record.transform()10RecordSO to Invoice, PO to Receipt transforms
N/log (any level)1SystemReduce debug logging in production
Governance limits: Client=1,000 | User Event=1,000 | Suitelet=1,000 | RESTlet=5,000 | Scheduled=10,000 | Map/Reduce=10,000/stage | Workflow Action=1,000
Sandbox to Production Promotion Steps
1.Complete all testing in Sandbox (see testing checklist)
2.Document all script files, deployment IDs, and configuration changes
3.Create SDF (SuiteCloud Development Framework) project or manual promotion list
4.Back up existing production scripts to /SuiteScripts/archive/YYYYMMDD/
5.Upload new script files to production File Cabinet
6.Update script records to point to new file versions
7.Set deployment status to "Testing" first -- verify with one user
8.Change deployment status to "Released" for full audience
9.Monitor execution logs for first 24 hours
10.Confirm with stakeholders and close deployment ticket
Rollback Procedure
Emergency Rollback Steps
1. Set deployment status to "Not Released" (immediately stops execution)
2. Restore previous script file from /SuiteScripts/archive/
3. Update script record to point to restored file
4. Set deployment back to "Released"
5. Verify functionality with test transaction
6. Notify affected users of rollback and estimated fix timeline
7. Document root cause and create fix ticket
Go-Live Checklist
■ Always deploy to Testing status first. Monitor execution logs for 24h after release. Keep archived versions in File Cabinet for instant rollback. Governance budgets are per-execution, not cumulative.
24 GFS NetSuite Do / Don't 30+ NetSuite-specific best practice pairs covering scripting, search, workflow, deployment, and security

Do

Use dynamic mode for interactive records (forms, suitelets with user input)
Use SuiteQL (N/query) for complex queries with 3+ table joins
Cache search results in Map/Reduce getInputData stage
Use log.audit for business events (approvals, record changes)
Handle all error paths in RESTlets with typed error responses
Use field internal IDs (not labels) in all scripts
Batch record operations with governance checks between each
Use N/query for readonly data retrieval (reporting, dashboards)
Use custbody_ / custcol_ / custentity_ for custom fields
Test with all affected roles before deployment
Use record.submitFields() for simple field updates (cost: 2)
Use search.lookupFields() to read 1-5 fields from a record (cost: 1)
Use paged execution for searches returning 4000+ results
Validate URL parameters server-side in Suitelets
Use N/runtime to check governance before expensive operations
Use Map/Reduce for batch processing of 100+ records
Log full error objects: name, message, stack trace
Use Promise patterns in client scripts for async operations
Archive previous script versions before overwriting
Use the N/format module for date and number formatting
Use N/email with templates for consistent branded communications
Set deployment status to Testing before full release
Use N/record.Type enum for type-safe record type references
Implement retry logic for RCRD_HAS_BEEN_CHANGED errors
Use FETCH FIRST N ROWS ONLY in SuiteQL for pagination
Use FreeMarker ?has_content for null-safe field access in templates
Set concurrency limits appropriate for the script's workload
Use N/url.resolveScript() for dynamic Suitelet URLs
Include version comments in script headers (@version 1.0.0)
Use TBA authentication for all API integrations
Use comments (not messages) for record communication notes
Monitor execution logs for 48h after any production deployment

Don't

Use standard mode for user-facing forms (loses field validation)
Use N/search for joins across 4+ tables (use SuiteQL instead)
Run searches inside map() -- they execute per record (use getInputData)
Use log.debug in production (clutters logs, wastes governance)
Return raw NetSuite error objects to external clients
Hardcode internal IDs -- use script parameters or custom records
Save records in a loop without checking remaining governance
Load full records just to read 1-2 field values
Repurpose standard fields for custom data (use custom fields)
Assume admin access -- always test with restricted roles
Use record.load() + save() when submitFields() would suffice
Build search result arrays with run().each() for large datasets
Use getRange(0, 1000) without checking if more results exist
Trust client-side URL parameters without server validation
Run expensive operations without checking governance budget first
Use Scheduled Scripts for batch work that Map/Reduce handles better
Catch errors silently -- always log or surface the issue
Use synchronous HTTP calls in client scripts (blocks the UI)
Overwrite production scripts without backing up the previous version
Use string concatenation for dates (use N/format module)
Send emails with hardcoded HTML -- use NS email templates
Deploy directly to Released without testing with a single user first
Use string literals for record types ('salesorder' -- use record.Type)
Fail permanently on concurrent edit errors without retrying
Use SELECT * or unbounded queries in SuiteQL (always limit results)
Assume field values exist in templates -- always use null checks
Set concurrency to max for all scripts (causes queue contention)
Hardcode Suitelet URLs (they change between environments)
Ship scripts without version tracking or change documentation
Use NLAuth for production integrations (deprecated, less secure)
Use the messages tab for internal notes (use comments instead)
Assume deployment success -- always verify with execution log review
■ These rules are GFS-specific and reflect our NetSuite 2025.2 environment. Print this page and post near your workstation. When in doubt, check governance first, cache second, log third.
Confirm Delete
Are you sure you want to delete record SO-24891? This action cannot be undone. All line items, shipping records, and related transactions will be permanently removed.
← Back to Hub · GFS Design System v10.0 · Global Food Solutions, Inc. · 131 Heartland Blvd, Edgewood, NY 11717 · (877) 728-5550