← Files NetSuite SuiteCloudARCHIVED FILE

skills/netsuite-sdf-safe-guide/references/01-understand-netsuite-features.md

22.9 KB · Oct 4, 2026 · 12:30 UTC

↓ Download file

# Principle 1: Understand NetSuite Features and Data Schema

> Source: Oracle NetSuite SAFE Guide (SuiteApp Architectural Fundamentals & Examples) Version 2025.2
> Author: Oracle NetSuite

## Overview

As a SuiteApp developer or architect, you must invest time learning about the core NetSuite ERP/CRM features you want to extend. Understanding NetSuite business processes is imperative to identifying SuiteApp integration points. Feature knowledge allows you to focus on building value-added SuiteApps that extend the platform's features without duplicating them.

## Key Concepts

### 1.1 Solutions Must Work With Existing Processes

- Reuse the data schema provided by the NetSuite platform.
- Ensure your SuiteApp works well with built-in NetSuite business logic.
- Honor NetSuite as the system of record by using the platform's transaction and item records as the backbone.

### 1.2 Data and Record Types Considerations

- Use standard NetSuite records when it makes sense; extend them with custom fields when necessary.
- Thoroughly identify records or fields that are already available before creating custom ones.
- Reference the **SuiteScript Records Browser** and **SuiteTalk Schema Browser** in NetSuite Help Center.

#### Overextending Standard Records
Do NOT "stretch" the use of standard NetSuite records. This may cause problems with other SuiteApps or integrations. Create custom records for specific business needs instead.

#### Creating Custom Records
Create and use custom records to represent unique business objects not available with standard objects.

### 1.3 SuiteTalk Web Services: Choosing the Right Technology

| Interface | Best For | Pros | Cons |
|-----------|----------|------|------|
| **REST Web Services** | Modern integrations, mobile platforms | Lightweight, OAuth 2.0 support, SuiteQL support, **required for new SuiteApps** | Legacy tax not supported |
| **SOAP Web Services** | Legacy integrations only | Most mature platform | No OAuth 2.0, higher latency, deprecated — all endpoints disabled 2028.2 |
| **RESTlets** | Custom REST APIs | Built-in HTTPS, can be packaged via SACC | Limited execution time/governance |
| **SuiteQL** | Complex queries | SQL-like syntax, flexible joins | Read-only access |

**SOAP Deprecation:** SOAP will not be permitted for new SuiteApps starting in 2024.2. The last new SOAP endpoint was added in 2025.2. All SOAP endpoints will be permanently disabled in 2028.2 — plan migration well before this deadline.

#### 2026.1 REST API New Features

| Feature | Description | Notes |
|---------|-------------|-------|
| **Attach/Detach** | `POST .../record/v1/{type1}/{id1}/!attach/{type2}/{id2}` — returns HTTP 204 | Contact and File record types only |
| **Batch Operations** | Process multiple records of the same type in one async request (HTTP 202) | Poll job status for completion; consult Oracle REST docs for exact format |
| **create-form** | Retrieve the form structure for a record type | Consult Oracle REST docs for exact request format |
| **selectOptions** | Role-aware metadata for select field options | Respects role-based visibility rules |
| **Support Case records** | 4 support-related record types now accessible via REST | Consult Oracle REST docs for exact record type names |

See [Appendix: SuiteTalk REST API](appendices/appendix-suitetalk-rest.md) for a comprehensive REST reference including OAuth 2.0 quick reference and SOAP migration guidance.

### 1.4 REST API Data Retrieval Methods

1. **REST Record Endpoint** – Simple data fetching by ID or ExternalId.
2. **Query Endpoint (SuiteQL)** – Complex queries with joins.
3. **Query Endpoint (Datasets)** – Execute requests against saved datasets.

#### REST API Limitations
- Concurrency limits based on account tier and SuiteCloud Plus licenses
- Standard Tier: base limit of 5 concurrent requests (15 with SuiteCloud Plus)
- Maximum 1,000 results per page, up to 1,000 pages

### 1.5 Prefer Native Record Type Enums

Always use `record.Type.*` and `search.Type.*` enum constants for standard NetSuite record types instead of string literals. This provides compile-time safety, IDE auto-completion, and protects against typos that would silently fail at runtime.

#### Quick Reference: Common Mappings

| String Literal | `record.Type.*` | `search.Type.*` |
|----------------|-----------------|-----------------|
| `'customer'` | `record.Type.CUSTOMER` | `search.Type.CUSTOMER` |
| `'salesorder'` | `record.Type.SALES_ORDER` | `search.Type.SALES_ORDER` |
| `'invoice'` | `record.Type.INVOICE` | `search.Type.INVOICE` |
| `'vendor'` | `record.Type.VENDOR` | `search.Type.VENDOR` |
| `'purchaseorder'` | `record.Type.PURCHASE_ORDER` | `search.Type.PURCHASE_ORDER` |
| `'transaction'` | — | `search.Type.TRANSACTION` |
| `'inventoryitem'` | `record.Type.INVENTORY_ITEM` | `search.Type.INVENTORY_ITEM` |
| `'employee'` | `record.Type.EMPLOYEE` | `search.Type.EMPLOYEE` |

#### When String Literals Are Acceptable

String literals should **only** be used for custom record types (`customrecord_*`) since they are not part of the standard enum:

```javascript
// GOOD: Custom record; string literal required.
record.load({ type: 'customrecord_batch_controller', id: controllerId });

// GOOD: Standard record; use enum.
record.load({ type: record.Type.CUSTOMER, id: custId });

// BAD: Standard record; avoid string literal.
record.load({ type: 'customer', id: custId });
```

## Best Practices

### JavaScript Best Practices (ES6/SuiteScript 2.1)

```javascript
/**
 * @NApiVersion 2.1
 * @NScriptType ...
 */

// Use const and let instead of var.
const immutableValue = 'fixed';
let mutableValue = 10;

// Use arrow functions for lambda expressions.
const numbers = [1, 4, 9, 16];
const doubled = numbers.map(x => x * 2);

// Use string interpolation.
const name = "John";
console.log(`Hello ${name}`);

// Prefer declarative over imperative.
const sumNumbers = (n) => n.reduce((acc, current) => acc + current);
```

### Immutability
```javascript
// Don't mutate objects directly.
const car = { model: 'AAAA', year: 2020 };

// Do this instead:
const newCar = Object.assign({}, car, { model: 'BBBB' });

// Or use spread operator:
const arr = [1, 2, 3];
const arr2 = [...arr, 4, 5];
```

### OneWorld Considerations

- Build a common SuiteApp for both NetSuite OneWorld and single-instance accounts.
- The Subsidiary field is mandatory for most standard records in OneWorld.
- Some records are unique to OneWorld (for example, Intercompany Journal Entry).

### SuiteTax Considerations

- SuiteTax is the default framework for most new accounts as of 2024.2.
- Tax-related fields moved to the Tax Details sublist.
- Use `runtime.isFeatureInEffect({ feature: 'tax_overhauling' })` to detect SuiteTax.

```javascript
define(['N/runtime'], function(runtime) {
    const isSuiteTaxEnabled = () => {
        return runtime.isFeatureInEffect({
            feature: 'tax_overhauling'
        });
    };
    return { isSuiteTaxEnabled };
});
```

### Sandbox-Aware Scripts

NetSuite sandbox accounts have account IDs with a `_SB` suffix (for example, `1234567_SB1`, `1234567_SB2`). Use `runtime.accountId` to detect the environment and adjust behavior — this prevents sandbox tests from triggering real-world side effects like external API calls, production emails, or payment gateway transactions.

**Pattern: Detect Sandbox**

```javascript
/**
 * @NApiVersion 2.1
 * @NScriptType ScheduledScript
 */
define(['N/runtime', 'N/https', 'N/email', 'N/log'], (runtime, https, email, log) => {

    const execute = () => {
        const isSandbox = runtime.accountId.includes('_SB');

        if (isSandbox) {
            log.audit('Environment', 'Running in sandbox — skipping external calls');
        }

        // GOOD: Skip production payment gateway in sandbox.
        if (!isSandbox) {
            https.post({
                url: 'https://api.paymentgateway.com/charge',
                body: JSON.stringify(chargeData)
            });
        } else {
            // Use sandbox/test endpoint instead.
            https.post({
                url: 'https://sandbox.paymentgateway.com/charge',
                body: JSON.stringify(chargeData)
            });
        }

        // GOOD: Route emails to a test address in sandbox.
        const emailRecipient = isSandbox
            ? 'dev-test@mycompany.com'
            : 'orders@mycompany.com';

        email.send({
            author: runtime.getCurrentUser().id,
            recipients: [emailRecipient],
            subject: 'Order Processed',
            body: 'Your order has been processed.'
        });
    };

    return { execute };
});
```

**Common Sandbox Guard Use Cases:**

| Scenario | Sandbox Behavior |
|----------|-----------------|
| External HTTP calls | Route to test/sandbox endpoint, or skip entirely |
| Email sends | Redirect to dev team address |
| Payment/ERP integrations | Use test credentials or no-op |
| Webhooks / outbound triggers | Skip or log-only |
| SMS / push notifications | Suppress |

### No DOM Manipulation in SuiteScript

SuiteScript must **never** directly access or manipulate the browser DOM. NetSuite's internal DOM structure changes without notice between releases; any direct DOM manipulation will break unpredictably after upgrades.

**Prohibited DOM APIs (never use in SuiteScript):**

```javascript
// BAD: All of these are prohibited.
document.getElementById('custbody_myfield_fs_lbl');
document.querySelector('.uir-field');
jQuery('#some_element').hide();
document.getElementById('myfield').innerHTML = 'value';
window.document.forms[0].elements['custbody_myfield'].value = 'x';
```

**Use SuiteScript APIs instead:**

```javascript
// GOOD: Server-side: use N/ui/serverWidget for forms.
define(['N/ui/serverWidget'], (serverWidget) => {
    const onRequest = (context) => {
        const form = serverWidget.createForm({ title: 'My Form' });
        const field = form.addField({ id: 'custpage_status', type: serverWidget.FieldType.TEXT, label: 'Status' });
        field.defaultValue = 'Active';
    };
});

// GOOD: client-side: Use currentRecord API for field access.
define(['N/currentRecord', 'N/ui/dialog', 'N/ui/message'], (currentRecord, dialog, message) => {
    const saveRecord = (context) => {
        const rec = context.currentRecord;
        const status = rec.getValue({ fieldId: 'custbody_status' });

        if (!status) {
            // GOOD: Use N/ui/dialog — NOT alert() or DOM manipulation.
            dialog.alert({ title: 'Validation Error', message: 'Status is required.' });
            return false;
        }
        return true;
    };

    const pageInit = (context) => {
        // GOOD: use N/ui/message for banner notifications
        message.create({
            title: 'Notice',
            message: 'Please review all fields before saving.',
            type: message.Type.INFORMATION
        }).show({ duration: 5000 });
    };

    return { saveRecord, pageInit };
});
```

**Rule Summary:**

| Need | Wrong Approach | Correct Approach |
|------|----------------|-----------------|
| Add a field to a form | `document.createElement(...)` | `form.addField()` (N/ui/serverWidget) |
| Show a popup | `alert()` / `jQuery.dialog()` | `dialog.alert()` (N/ui/dialog) |
| Read a field value | `document.getElementById('field_fs').value` | `currentRecord.getValue({ fieldId: '...' })` |
| Show a status banner | `$('#msg').show()` | `message.create(...).show()` (N/ui/message) |
| Advanced UI | jQuery widgets / vanilla DOM | UIF SPA (see `12-uif-spa-best-practices.md`) |

### Timezone Handling

NetSuite stores dates internally in Pacific Time (PT). Constructing raw date strings or comparing Date objects built in different timezones produces off-by-one-day errors that are notoriously difficult to diagnose.

**Always use the N/format module for date handling:**

```javascript
/**
 * @NApiVersion 2.1
 * @NScriptType Suitelet
 */
define(['N/format', 'N/runtime', 'N/record', 'N/log'], (format, runtime, record, log) => {

    const onRequest = (context) => {

        // GOOD: Parse a user-supplied date string safely.
        const rawDateStr = context.request.parameters.trandate; // for example, "01/15/2024"
        const parsedDate = format.parse({
            value: rawDateStr,
            type: format.Type.DATE
        });

        // GOOD: Format a Date object for display.
        const displayDate = format.format({
            value: parsedDate,
            type: format.Type.DATE
        });

        // GOOD: Get the current user's configured timezone.
        const userTimezone = runtime.getCurrentUser().getPreference({ name: 'TIMEZONE' });
        log.debug('User TZ', userTimezone); // for example, "America/New_York"

        // GOOD: Set a date field on a record using parsed Date object.
        const rec = record.load({ type: record.Type.SALES_ORDER, id: 123, isDynamic: true });
        rec.setValue({ fieldId: 'trandate', value: parsedDate });
        rec.save();
    };

    return { onRequest };
});
```

**Pitfall patterns to avoid:**

```javascript
// BAD: Constructing a date string directly — timezone-dependent, breaks at DST boundaries.
const today = new Date().toISOString().split('T')[0]; // "2024-01-15" may be wrong day in PT.

// BAD: Comparing raw Date objects built in different ways.
const d1 = new Date('2024-01-15');         // Parsed as UTC midnight
const d2 = new Date(2024, 0, 15);           // Parsed as local midnight
// d1 !== d2 in Pacific Time (-8h offset)

// GOOD: Always round-trip through N/format.
const safeDate = format.parse({ value: '01/15/2024', type: format.Type.DATE });
const safeStr  = format.format({ value: safeDate,    type: format.Type.DATE });
```

**Key rules:**
1. Use `format.parse()` to convert any incoming date string to a JS `Date` object.
2. Use `format.format()` to convert a `Date` object back to a display string.
3. Never manually construct date strings for storage or comparison.
4. When passing dates to `record.setValue`, always pass a JS `Date` object (not a string).
5. Use `runtime.getCurrentUser().getPreference({ name: 'TIMEZONE' })` when timezone-aware display is needed.

## Common Pitfalls

1. **Duplicating built-in functionality** — Always extend, never duplicate NetSuite features.
2. **Using external Suitelets** — Strictly prohibited for public endpoints (DDoS vulnerability).
3. **Ignoring feature combinations** — Test with various feature combinations enabled.
4. **Data structure mismatches** — Ensure field sizes/types match between external apps and NetSuite.
5. **Relying on deprecated technologies** — Use REST over SOAP, SuiteScript 2.1 over 1.0.
6. **Passing strings to DATE fields via `record.setValue`** — DATE fields require a JS `Date` object, not a string. Raw strings like `"1/22/2026"` cause "Invalid date value (must be MM/DD/YYYY)" errors on `record.save()`. Always parse date strings to `new Date()` objects before calling `setValue`. Applies to all script types (Suitelet, Map/Reduce, User Event, etc.).
7. **Passing label text to SELECT fields via `record.setValue`** — SELECT (list/record) fields require the internal numeric ID, not the display label. Passing `"2 - Proficient"` to a SELECT field fails. Pre-load list values with SuiteQL (`SELECT id, name FROM customlist_...`) and resolve text to ID before calling `setValue`.
8. **Passing strings to TIMEOFDAY fields via `record.setValue`** — TIMEOFDAY fields require a JS `Date` object, not a string. Passing `"10:00 AM"` causes "Invalid Field Value" errors on `record.save()`. Parse to a Date object with epoch date: `new Date(1970, 0, 1, hours, minutes, 0)`. NetSuite ignores the date portion; using 1970-01-01 avoids DST edge cases. Same applies to TIME fields.

### SDF Custom Record Field Type Pitfalls

9. **FREEFORMTEXT is not a valid SDF fieldtype** — Using `<fieldtype>FREEFORMTEXT</fieldtype>` in any Object XML (custom record fields, transaction body fields, entity fields) causes validation error: `Invalid "fieldtype" reference key "FREEFORMTEXT"`. Despite appearing in some NetSuite documentation, `FREEFORMTEXT` is not a valid SDF enum value. Use `TEXT` (single-line) or `TEXTAREA` (multi-line) for all custom field types.
10. **Cannot change deployed field types** — Once a field is deployed (for example, as SELECT), changing its type in XML (for example, to TEXT) will fail with "The fieldtype field must not be [newtype]". Instead, create a new field with the desired type and migrate data.
11. **Using string literals for standard record types** — Passing `'customer'` or `'salesorder'` as the `type` parameter instead of `record.Type.CUSTOMER` or `search.Type.SALES_ORDER`. String literals bypass IDE validation and are prone to silent typo failures. Use `record.Type.*` / `search.Type.*` enums for all standard record types; reserve string literals for custom records (`customrecord_*`).
12. **`bodytransactiontypes` element unsupported in SDF Object XML** — Adding `<bodytransactiontypes><bodytransactiontype>WORKORDER</bodytransactiontype></bodytransactiontypes>` to a `transactionbodycustomfield` Object XML causes validation warning: `The object field "bodytransactiontypes" is invalid or not supported`. Transaction body field type filtering cannot be set via SDF Object XML — the field deploys to ALL transaction types. To restrict which transactions show the field, configure it manually in the UI after deployment (Customization > Transaction Body Fields > edit field > Applies To subtab).

### Record Creation Pitfalls

13. **Item subsidiary sublist is static — use body-level `subsidiary` field** — Creating items (Service, Inventory, Non-Inventory, etc.) and attempting to manipulate the `subsidiary` sublist with `selectNewLine`/`setCurrentSublistValue`/`commitLine` throws: `You have attempted an invalid sublist or line item operation. You are either trying to access a field on a non-existent line or you are trying to add or remove lines from a static sublist`. The subsidiary sublist on item records is **static** and cannot be modified via sublist APIs. **Fix**: set `subsidiary` as a body-level field using `setValue`. Set it before `rate` or other subsidiary-dependent fields so sourcing works correctly.

```javascript
// BAD: Subsidiary sublist is static, sublist APIs fail.
const itemRec = record.create({ type: 'serviceitem', isDynamic: true });
itemRec.selectNewLine({ sublistId: 'subsidiary' });         // ERROR
itemRec.setCurrentSublistValue({ sublistId: 'subsidiary',
    fieldId: 'subsidiary', value: subsidiaryId });
itemRec.commitLine({ sublistId: 'subsidiary' });

// GOOD: Set subsidiary as a body-level field.
const itemRec = record.create({ type: 'serviceitem', isDynamic: true });
itemRec.setValue({ fieldId: 'subsidiary', value: subsidiaryId });
itemRec.setValue({ fieldId: 'rate', value: 350 });  // Set after subsidiary.
```

### AMD Module Loading Pitfalls

11. **Mixing `var` and `const`/`let` for the same identifier in overlapping scopes crashes the AMD loader** — `var` declarations hoist to function scope, passing through `try`/`if`/`for` blocks. If a `const` or `let` with the same name exists in an enclosing block (for example, the `try` block), JavaScript throws a `SyntaxError` at parse time. In NetSuite, this prevents the AMD module from loading at all, resulting in an **HTTP 500 with no execution log entries** — the script never reaches `onRequest`/`execute`/etc. This is extremely difficult to diagnose because there is no error message in the UI or logs; only the browser console shows a generic 500. **Fix**: use `const` or `let` consistently for block-scoped variables; never change a `const` to `var` without checking all other declarations of the same name in the enclosing function. Run `node --check <file>` locally before deploying to catch this class of error.

```javascript
// BAD. SyntaxError: 'date' has already been declared.
function parseDate(str) {
    try {
        if (/pattern1/.test(str)) {
            var date = new Date(str);    // var hoists to function scope, through try block.
            return date;
        }
        const date = new Date(str);      // const in try block conflicts with hoisted var.
        return date;
    } catch (e) { return null; }
}

// GOOD: Each const is block-scoped, no conflicts.
function parseDate(str) {
    try {
        if (/pattern1/.test(str)) {
            const date = new Date(str);  // block-scoped to if block.
            return date;
        }
        const date = new Date(str);      // block-scoped to try block; no conflict.
        return date;
    } catch (e) { return null; }
}
```

12. **Calling N/ API modules at `define()` callback scope causes `SUITESCRIPT_API_UNAVAILABLE_IN_DEFINE`** — All SuiteScript API modules (`N/runtime`, `N/record`, `N/query`, etc.) are injected into the `define()` callback but are **not available for use during callback execution**. They only become available when an entry point function (`execute`, `onRequest`, `beforeSubmit`, etc.) is invoked by the platform. Calling `runtime.getCurrentScript()`, `record.load()`, or any N/ API at module scope causes deployment failure with error: `SUITESCRIPT_API_UNAVAILABLE_IN_DEFINE: All SuiteScript API Modules are unavailable while executing your define callback`. **Fix**: use lazy initialization — declare variables as `null` at module scope and initialize them on first access inside a getter function that's only called from entry points.

```javascript
// BAD . Runtime API called at define() time.
define(['N/runtime', 'N/log'], (runtime, log) => {
    const script = runtime.getCurrentScript();  // FAILS at deploy time
    const PERF_ENABLED = script.getParameter({ name: 'custscript_perf' });
    const execute = (context) => { /* ... */ };
    return { execute };
});

// GOOD. Lazy initialization, API called only from entry points.
define(['N/runtime', 'N/log'], (runtime, log) => {
    let _script = null;
    const _getScript = () => {
        if (!_script) { _script = runtime.getCurrentScript(); }
        return _script;
    };
    const execute = (context) => {
        const perfEnabled = _getScript().getParameter({ name: 'custscript_perf' });
        /* ... */
    };
    return { execute };
});
```

## Building Fault Tolerant SuiteApps

### Potential Failure Points
1. Network Connection
2. Third-party Connector SuiteApps
3. Web Services Endpoint
4. NetSuite Backend
5. Server Side SuiteScript
6. External Cloud Systems

### Handling External System Failures
```javascript
// Handle N/http and N/https errors gracefully.
try {
    const response = https.get({ url: externalUrl });
} catch (e) {
    if (e.name === 'SSS_CONNECTION_CLOSED' ||
        e.name === 'SSS_CONNECTION_TIME_OUT') {
        // Handle gracefully (log, retry, notify).
    }
}
```

## SuiteSuccess Considerations

- SuiteSuccess accounts come pre-configured with 20+ SuiteBundles.
- **Do NOT** add, change, delete, or reference objects in pre-existing SuiteSuccess bundles.
- Develop SuiteApps in non-SuiteSuccess accounts.
- Test in SuiteSuccess accounts to uncover potential conflicts.

## Further Reading

Search in NetSuite Help Center or SuiteAnswers:
- NetSuite Documentation Overview
- Understanding Accounting-Related Features
- Understanding General Ledger Impact of Transactions
- Inventory Management
- Understanding NetSuite OneWorld
- Understanding NetSuite Features in Web Services

SHA-256: b6cdbe9e80f345f537250a391f3f36c56d690c66991411f565ba55f62b2032f9