Introduction: Why SuiteScript 2.x Matters
SuiteScript 2.x is NetSuite's server-side JavaScript framework for customizing and extending ERP functionality. Since its introduction, it has largely replaced SuiteScript 1.0 for new development. Understanding 2.x is essential for any NetSuite developer—whether you're building custom record types, automating workflows, integrating with external systems, or creating sophisticated reporting.
This guide covers everything from module loading and entry points to governance limits, debugging strategies, and deployment best practices. By the end, you'll have a comprehensive reference for building robust, maintainable NetSuite customizations. The content is organized for both newcomers and experienced developers seeking deeper knowledge.
SuiteScript 2.x vs 1.0: Key Differences
SuiteScript 2.x uses ECMAScript 5.1 and a modular architecture. Unlike 1.0, you load modules with require() instead of nlapi* functions. The N/ namespace (N/record, N/search, N/file, etc.) replaces the older nlapi* APIs. TypeScript support is available via the NetSuite TypeScript definitions (N/tsapi or community-defs), enabling better IDE support and type safety.
Governance limits are more granular in 2.x—you can check usage with runtime.getRemainingUsage(). Entry points (beforeLoad, afterSubmit, etc.) are defined in the Script record and receive a context object. Error handling uses standard JavaScript try/catch. Understanding these differences helps when migrating from 1.0 or when debugging legacy scripts.
Module System and Loading
SuiteScript uses CommonJS-style require(). Load modules at the top of your script: define(['N/record', 'N/search', 'N/log'], (record, search, log) => { ... });. The define callback receives modules in the same order as the dependency array. Modules are singletons—loaded once per script execution.
| Module | Purpose |
|---|---|
| N/record | Create, load, update records |
| N/search | Create and run saved searches, lookup |
| N/file | File cabinet operations |
| N/email | Send emails |
| N/runtime | Environment, remaining governance |
| N/url | Resolve record URLs |
| N/format | Number, date formatting |
| N/transaction | Transaction-specific helpers |
For SuiteScript 2.1 (ECMAScript 2015+), you can use require() directly without define. Check your account's script version in the Script record.
Entry Points: When Your Script Runs
Entry points determine when and how your script executes. User Event scripts run on record load (beforeLoad) or submit (beforeSubmit, afterSubmit). Scheduled scripts run on a schedule. Map/Reduce processes large datasets. Suitelets expose custom URLs. RESTlets provide REST endpoints. Workflow Action scripts run from workflows.
beforeLoad: Fires when a record form is loaded. Use for adding custom buttons, hiding fields, or injecting client-side logic. Avoid heavy processing—the user is waiting. beforeSubmit: Fires before record save. Use for validation and field updates. Runs in "dynamic" mode—you can modify the record. afterSubmit: Fires after save. Use for side effects: create related records, send emails, call external APIs. The record is committed; you receive a new record object.
Scheduled scripts have a execute entry point. Map/Reduce has getInputData, map, reduce, summarize. Choose the right entry point for your use case—scheduled for batch jobs, Map/Reduce for large datasets that exceed governance in a single run.
N/record: Working with Records
The N/record module is the primary way to create and modify records. record.create({ type: 'salesorder' }) creates a new sales order. record.load({ type: 'customer', id: 123 }) loads an existing record. Use record.save() to persist changes. For partial updates without loading the full record, use record.submitFields()—it consumes fewer governance units.
Get/set field values with record.getValue(), record.setValue(). For sublists: record.getSublistValue(), record.setSublistValue(). Use record.getLineCount() to iterate lines. Insert lines with record.selectNewLine(), record.setSublistValue(), record.commitLine(). For existing lines, use record.selectLine() then get/set.
Always check context.type in User Event scripts—beforeSubmit fires for create and edit; use context.newRecord vs context.oldRecord in afterSubmit to compare before/after. Handle both create and update when your logic applies to both.
N/search: Searches and Lookups
Use search.create() to run a saved search by ID or create a dynamic search with a search object. The search object has type, filters, columns. Run with search.run() or search.runPaged() for large result sets. runPaged returns a page iterator; use page.getRange() and iterator.nextPage() to avoid loading all rows into memory.
For lookups, search.lookupFields() retrieves specific fields from a record without a full search—useful when you have an ID and need one or two fields. It's more efficient than running a search. Use search.createColumn() and search.createFilter() for type-safe filter/column definitions.
Governance: each result.getRange() call and each row processed consumes units. For scripts that process thousands of rows, use Map/Reduce or chunk your processing with reschedule. Check runtime.getRemainingUsage() before expensive loops.
Governance Limits: Staying Within Budget
NetSuite enforces governance limits to ensure fair resource usage. Each script type has different limits: user events typically 10 units per record operation; scheduled scripts 10,000 units default; Map/Reduce 10,000 per map/reduce invocation. Units are consumed by record operations (create, load, save, delete), search runs, file operations, and HTTP calls.
| Operation | Units (approx) |
|---|---|
| record.load | 1 |
| record.save (create) | 1 |
| record.submitFields | 1 |
| search.run (per 1000 rows) | 10 |
| search.lookupFields | 1 |
| file.create | 1 |
| http.post | 1 |
Use runtime.getRemainingUsage() to check remaining units. If you're approaching the limit, log and exit gracefully or reschedule. Script 1.0 has different limits; when in 2.x, always use 2.x modules. Exceeding limits causes a "Usage Limit Exceeded" error—the transaction may roll back depending on entry point.
Error Handling and Logging
Use try/catch for error handling. Log errors with log.error({ title: 'Error', details: e.message }). Include context (record ID, batch number) for troubleshooting. For User Event scripts, rethrow after logging if you want to prevent save—throw e in beforeSubmit will abort the save.
Use log.audit() for general tracing, log.debug() for verbose output (enable script debugging in account), log.error() for failures. In production, avoid excessive logging—each log consumes a small amount of governance and clutters the Execution Log. Consider a custom log level controlled by a script parameter.
Deployment: Script Records, Bundles, and Promotion
Create a Script record under Customization > Scripting > Scripts. Set the Script File (from File Cabinet), entry points, and parameters. Deploy to All Roles or specific roles. For SuiteApp distribution, use SuiteCommerce Advanced or SuiteCloud Development Framework (SDF).
Scripts live in the File Cabinet under SuiteScripts. Use a logical folder structure. SDF allows version control, deployment across environments (Sandbox to Production), and bundled development. Test in Sandbox before deploying to Production. Use script parameters for environment-specific config (e.g., API URLs).
RESTlets and Suitelets: Custom Endpoints
RESTlets expose REST endpoints. Define get, post, put, delete functions. Receive request and return response objects. Use for integrations—external systems call your RESTlet to push/pull data. Authenticate with OAuth or NLAUTH. Respect rate limits.
Suitelets generate custom HTML pages. Use for custom UIs, file upload handlers, or internal tools. Return HTML with response.write() or use a template. For sensitive operations, validate permissions. Suitelets can be restricted by role and login.
Client-Side Scripts and User Interaction
SuiteScript supports client-side scripts via N/ui or via the older nlobjForm. Use addButton to add custom buttons that call a Suitelet or trigger client logic. For dynamic form behavior, use beforeLoad to inject script and modify the form. Client scripts run in the user's browser—avoid sensitive logic there; call a Suitelet for server-side processing.
For SuiteCommerce, use Backbone.js and the SCA module pattern. Different from standard NetSuite UI—requires SCA-specific development. For standard NetSuite, client scripts are limited; most heavy logic stays server-side. Use workflow for simple client-side field changes when possible.
SuiteScript 2.1: ES6 and Beyond
SuiteScript 2.1 supports ECMAScript 2015+ features: arrow functions, let/const, template literals, destructuring, classes. Use require() directly instead of define. Enable in the Script record by selecting 2.1 as the API version. Modern syntax improves readability and reduces boilerplate.
Async/await is not natively supported—NetSuite's runtime doesn't support Promises in the same way. For sequential async operations, use callbacks or chain logic. Check NetSuite's release notes for new 2.1 features. TypeScript definitions are available for better development experience.
Testing and Debugging Strategies
Enable Script Debugger in Setup > Script Debugger. Set breakpoints and step through code. Execution Log shows script runs, errors, and governance. For Scheduled and Map/Reduce, check the Script Status page. Use log.audit to trace flow in development; remove or reduce in production.
Unit testing: NetSuite doesn't provide a built-in test framework. Use external tools (Jest, Mocha) with mocks for logic that doesn't require NetSuite context. For integration testing, use Sandbox. Create test data and run scripts; verify results. Document test cases for critical scripts.
Performance Optimization in Scripts
Avoid search.create in loops—load IDs first, then batch process. Use record.submitFields for single-field updates. Cache lookup results in a JavaScript object/map. Use search.runPaged for large result sets. Minimize round-trips. For Map/Reduce, design keys for parallel reduce invocations.
Profile your script: log timestamps at key points, measure governance usage. Identify bottlenecks. Sometimes a different approach (e.g., scheduled instead of User Event for batch work) is faster. Consider splitting work across multiple scheduled runs with reschedule.
Security: Permissions and Data Access
Scripts run with the permission level of the user or the deployment role. A script deployed to All Roles runs as the current user—respect their permissions. For scheduled scripts, the deployment role matters. Use minimum required permissions. Never hardcode credentials; use script parameters or secure storage.
Validate input in RESTlets and Suitelets—don't trust client data. Sanitize for SQL injection if building dynamic queries (rare in SuiteScript; prefer search.create). Check context.getUser().role for role-based logic. Audit sensitive operations.
Real-World Example: Custom Approval Workflow
Scenario: Sales Orders over $50,000 require CFO approval. Implement with User Event afterSubmit: when SO is submitted and amount > 50000, create a custom record "Approval Request", set status Pending, and send email to CFO. A Suitelet allows CFO to approve/reject. On approve, workflow or script updates SO status. On reject, notification to sales rep.
Key patterns: use custom records for approval state, email for notifications, Suitelet for approval UI. Consider workflow for simpler cases—workflow can create tasks and route. For multi-stage approvals, custom records with status transitions work well.
Real-World Example: Integration with External CRM
Scenario: Sync Opportunities from NetSuite to an external CRM via RESTlet. External system calls your RESTlet with OAuth. Your RESTlet reads request body, validates, creates/updates Opportunity records. Use N/record for creates, search for lookups. Return JSON response. Handle errors, log for troubleshooting.
Consider rate limits from both sides. Use bulk operations when the external system sends batches. Idempotency: use external ID to prevent duplicates. Document the API for the integration partner.
Migration from SuiteScript 1.0
Inventory your 1.0 scripts. Map nlapi* to N/* equivalents: nlapiCreateRecord → record.create, nlapiSearchRecord → search.create, etc. Test each script in Sandbox. Some 1.0 APIs have no direct 2.x equivalent—check SuiteAnswers. Update entry point definitions. Governance limits differ—profile and adjust.
Phased migration: convert high-value scripts first. Keep 1.0 and 2.0 running in parallel during transition. NetSuite supports both; 1.0 is in maintenance mode. Plan for full migration before 1.0 deprecation (monitor NetSuite release notes).
Map/Reduce for Large-Scale Processing
Map/Reduce splits work across stages. getInputData returns the input (e.g., search iterator). map processes each input and can emit key-value pairs. reduce aggregates values by key. summarize runs once at the end for final reporting. Each stage runs in separate governance context—you get 10,000 units per map and reduce invocation.
Use for: mass updates, large report generation, data migration within NetSuite. Design keys for reduce—all values with the same key go to the same reduce invocation. Avoid emitting too many unique keys (reduces parallelism). Use script parameters for configuration. Monitor script status in Execution Log.
N/http: Calling External APIs
The N/http module lets you make HTTP requests to external systems. Use http.request() with a URL, method (GET, POST, etc.), and optional body. Each call consumes governance units. For OAuth-secured APIs, obtain tokens via N/https (for outgoing) or use a pre-stored token. Handle timeouts and errors—external systems can be slow or unavailable.
Best practice: validate URLs and payloads before sending. Use HTTPS for sensitive data. Log request/response for debugging (sanitize credentials). For high-volume integrations, consider queuing (e.g., process records in batches, respect rate limits). NetSuite's built-in integration features (RESTlets, integration records) may be preferable for some use cases.
N/file: File Cabinet and Attachments
N/file creates and reads files in the File Cabinet. file.create() with a name, type (CSV, PDF, etc.), and contents. Attach files to records with record.attach(). Use for generating reports (CSV export), PDF creation, or storing script output. Each file operation consumes units.
For large files, consider streaming or chunking. File size limits apply. Use appropriate MIME types. Organize files in meaningful folders. For record attachments, the file is associated with the record and visible on the record's Files subtab. Delete old files periodically if they're transient (e.g., temp exports).
N/format and N/email
N/format provides number and date formatting. format.format({ value: 1234.56, type: format.Type.CURRENCY }) returns a formatted string for display. Use for reports, emails, and UI. N/email sends emails. email.send() with a recipient, subject, and body. Can attach files. Supports HTML body. Each email consumes units.
For transactional emails (invoices, order confirmations), consider using email templates with merge fields instead of building HTML in script—easier to maintain. Log email sends for audit. Handle bounces if you track delivery. Respect anti-spam practices (unsubscribe, valid from address).
Testing and Debugging Strategies
Enable Script Debugging under Setup > Company > General Preferences to step through scripts. Use breakpoints in the Netscript Debugger (if available) or log statements to trace execution. Test in Sandbox—never debug in Production. Create test data that covers edge cases: new records, updates, multi-line transactions, multi-subsidiary.
For User Event scripts, test both create and edit. For scheduled scripts, run manually first. Use script parameters to toggle "dry run" mode (log what would happen without making changes). Compare results to expected values. Document test cases for regression. Consider unit tests with a test framework if your team uses one.
Script Parameters and Configuration
Script parameters allow runtime configuration without code changes. Define parameters in the Script record: name, type (string, integer, etc.), default value. Access with runtime.getScriptParameter(). Use for: batch size, threshold amounts, feature flags, external URLs. Different deployments can have different parameter values.
For sensitive values (API keys), use Secure Credentials or script parameters with restricted access. Avoid hardcoding environment-specific values. Document parameters in the Script description. Use sensible defaults so the script works without configuration for common cases.
SuiteScript 2.1: ES6 and Beyond
SuiteScript 2.1 supports ECMAScript 2015+. Arrow functions, let/const, template literals, destructuring, classes. Use require instead of define for simpler module loading. Check your account version—2.1 requires a compatible NetSuite release. Benefits: cleaner syntax, smaller bundle size, better tooling support.
TypeScript definitions (N/tsapi or DefinitelyTyped @netsuite/types) enable static typing. Catch errors at compile time. Better IDE autocomplete. Use for larger codebases. Build step required (tsc). Balance type strictness with development speed. NetSuite's own SDF supports TypeScript.
Client Scripts and User Interaction
Client scripts run in the browser. Use for: field validation, dynamic field display, custom UI behavior. Entry points: pageInit (on load), validateField (on field change), saveRecord (before submit). Client scripts have different limits—no N/record for server-side ops; use N/currentRecord for the current form.
Keep client scripts light—they affect page load time. Validate on client for immediate feedback, but always validate on server (User Event beforeSubmit) for security. Use SuiteScript 2.x client modules (N/currentRecord, N/ui). Avoid N/search in client for heavy lookups—use a Suitelet or RESTlet if needed.
Mass Updates and Batch Processing
For mass updates, prefer Map/Reduce over a single scheduled script when row count is high. Each map invocation gets fresh governance. Design the map to process in chunks. Emit minimal data to reduce. Use script parameters for batch size. Monitor execution in Script Status.
record.submitFields is efficient for single-field updates. For multi-field updates, load/save may be necessary. Consider CSV Import for one-off mass updates if data is in spreadsheets. Document the update process for audit. Have a rollback plan (backup or reverse script) for destructive updates.
Security and Permissions
Scripts run with the permissions of the user or the deployment role. User Event scripts run as the triggering user. Scheduled scripts run as the deployment role—ensure that role has minimum required access. Suitelets and RESTlets can check runtime.getCurrentUser().role for authorization.
Avoid exposing sensitive operations in Suitelets without permission checks. Validate input—don't trust client or external input. Use parameterized queries (SuiteScript handles this) to avoid injection. Log access to sensitive operations. Restrict script deployment to appropriate roles. Review scripts in Security > Script Execution Log for anomalies.
Performance Optimization Tips
Cache lookup results in variables when the same lookup is needed repeatedly in a loop. Use search.create with filters to limit result set. Batch record operations where possible. Avoid nested loops that each run a search. Use getValue/setValue for single fields instead of loading full sublists when not needed.
For scheduled scripts processing many records, consider reschedule: when governance runs low, reschedule the script to continue later. Store state (last processed ID) in a script parameter or custom record. Split work across multiple runs. Profile your script—identify which operations consume the most units.
Common Patterns: Before and After
Default field from lookup: In beforeSubmit, search for related record, set field. Create child record: In afterSubmit, load parent data, create child, set foreign key. Validate cross-field: In beforeSubmit, get values, throw if invalid. Send notification: In afterSubmit, send email or create task. Sync to external: In afterSubmit, call HTTP to push data.
Keep User Event scripts fast—they block the user. Defer heavy work to a queue (custom record) and process with a scheduled script. Use locks (N/cache or custom record with locking) for concurrent update scenarios. Document why you chose a given pattern for future maintainers.
Migration from SuiteScript 1.0
Identify nlapi* calls and map to N/* modules. record.create replaces nlapiCreateRecord. search.load replaces nlapiSearchRecord. Use the NetSuite 1.0 to 2.0 migration guide. Test thoroughly—behavior can differ (e.g., date handling, null handling). Run 1.0 and 2.0 in parallel during transition if possible.
Governance limits differ—2.0 may be stricter in some areas. Entry point signatures change. Logging changes from nlapiLog to log module. No more nlapiOutOfBounds—use standard JavaScript. Plan migration in phases. Update one script at a time. Maintain a compatibility layer if needed for shared logic.
N/cache: Server-Side Caching
The N/cache module provides server-side key-value caching. Use for storing lookup results, configuration, or intermediate data within a script execution. Cache is session-based and has size limits. Useful when the same lookup runs multiple times in a loop—cache the first result, reuse. Reduces governance for repeated search operations.
Cache keys must be strings. Values can be strings or serializable objects. TTL (time-to-live) determines how long the cache persists. Use for expensive computations or external API responses. Note: cache is not shared across script executions; each scheduled run starts fresh. Use custom records for persistent cross-execution state.
Custom Record Types and Mass Updates
Custom records extend NetSuite's data model. Create under Customization > Record Types. Add custom fields. Use for approval workflows, project tracking, custom dashboards. Scripts can create, load, and update custom records via N/record with type set to your custom record internal ID.
Mass updates: use CSV import for simple updates, or SuiteScript for complex logic. Map/Reduce ideal for updating thousands of records. record.submitFields for single-field updates. Design keys for Map/Reduce to process in batches. Test with small dataset first. Have rollback plan for destructive updates.
SuiteFlow vs SuiteScript: When to Use Which
SuiteFlow (Workflow) is declarative—configure in UI, no code. Use for: approval routing, status changes, field defaults, email triggers. Faster to implement for standard patterns. Limited for complex logic, conditional branching, or external calls.
SuiteScript when: complex validation, multi-record operations, external API calls, custom calculations, dynamic form behavior. Scripts are more flexible but require development, testing, deployment. Combine: workflow triggers script for complex part; script handles what workflow cannot. Document decision for future maintainers.
Debugging Production Issues
Execution Log: Setup > System Information > Execution Log. Filter by script, date, status. View errors, governance usage, duration. Enable logging in script for troubleshooting. Sanitize log output—no PII or sensitive data in production logs.
Script Debugger: set breakpoints, step through. Requires enabling in account. Use in Sandbox for complex debugging. For production issues: reproduce in Sandbox if possible. Capture request/response for RESTlets. Check SuiteAnswer for known issues. Escalate to NetSuite support with script ID, error message, reproduction steps.
SuiteScript and Compliance
Scripts that handle financial or sensitive data may need compliance review. SOX: document scripts that affect financial reporting. Segregation: restrict deployment to appropriate roles. Audit: scripts are in Version Control; track changes. Log sensitive operations. Validate inputs. Restrict script access.
Data privacy: scripts processing PII must follow GDPR, CCPA. Minimize data in logs. Secure credentials. Encrypt if storing externally. Document data flows. Compliance team review for new scripts in regulated areas.
Custom Record Types and SuiteScript
Custom records extend NetSuite's data model. Create under Customization > Record Types. Use SuiteScript to create, query, and update custom records. record.create({ type: 'customrecord_myrecord' }). Custom records support sublists, fields, and workflow. Use for: approval queues, config tables, integration logs. Load with record.load; search with search.create({ type: 'customrecord_myrecord' }).
Custom records have internal IDs. Reference from transactions via custom fields. For parent-child custom records, use sublist or custom field linking. Validate field values in beforeSubmit. Use saved searches for reporting on custom records. Consider governance when processing many custom records in a loop. Document custom record purpose and fields for maintenance.
Custom records vs. standard records: custom records don't have built-in forms unless you create them. Use Suitelet for custom UI. Workflow can run on custom records. Scripts can be triggered by workflow. Balance flexibility with complexity—don't over-customize when standard records suffice. Use custom records for domain-specific data that doesn't fit standard types.
N/crypto and Secure Operations
N/crypto provides encryption and hashing. Use for sensitive data (passwords, tokens) stored in custom fields or script parameters. crypto.createHmac() for HMAC. Encrypt before store, decrypt when needed. Never log decrypted values. Use Secure Credentials for API keys—better than script parameters for highly sensitive data.
Hashing: SHA-256, SHA-512 for one-way hash. Use when you need to verify without storing plain text. Sign payloads for integrity. Verify signatures on incoming webhooks. Follow OWASP guidance for crypto. Key management: rotate keys periodically. Document what is encrypted and where keys are stored. Test decryption in Sandbox.
SuiteQL vs. Search Module
SuiteQL is SQL-like query language available via N/query. Use for complex joins, aggregations, reporting. SuiteQL can query standard and custom tables. Syntax differs from saved search—familiar SQL style. Governance: each query consumes units. Use for read-only reporting scripts. Cannot insert/update via SuiteQL.
When to use SuiteQL vs. N/search: SuiteQL for complex multi-table queries, ad-hoc reporting, data export. N/search for standard record operations, when you need search filters/columns API, integration with saved searches. SuiteQL is newer; check NetSuite docs for table/column availability. Both respect record-level permissions. Profile performance—SuiteQL can be faster for complex queries.
Workflow vs. SuiteScript Decision Guide
Use Workflow when: logic is declarative (if X then Y), no complex calculations, standard record operations, email/task creation. Workflow is configurable by admins, no code deploy. Use SuiteScript when: complex logic, loops, external API calls, custom calculations, multi-record operations, need full programmatic control. SuiteScript requires development, testing, deployment.
Hybrid: workflow calls Script Action for custom logic. Workflow handles routing; script handles complexity. Use workflow for approvals, status changes; script for data transformation. Document which tool handles what. Avoid duplicating logic in both. When in doubt, start with workflow—add script when workflow hits limits. Evaluate maintenance: workflow easier for admins to change.
Custom Record Types and SuiteScript
Custom records extend NetSuite's data model. Create under Customization > Record Types. Access via N/record with type: 'customrecord_myrecord'. Custom records support standard fields (name, owner, subsidiary) and custom fields. Use for approval tracking, configuration, or external entity mapping. Search custom records like any other type. Link to transactions via custom fields (entity, transaction).
Custom record scripts: User Event on create/edit/delete. Scheduled scripts for batch updates. Suitelets for custom UIs. Store JSON in long text fields for flexible structure. Use for workflow state, integration cache, or staging. Index custom fields used in search criteria. Consider governance when processing many custom records. Document record purpose and fields for maintainability.
N/query and SuiteQL for Advanced Reporting
SuiteQL is SQL-like query language in NetSuite. Access via N/query or REST. Use for complex joins, subqueries, aggregations. Ideal for reporting integrations, data warehouse feeds, analytics. SuiteQL cannot insert/update—read-only. Combine with REST Record API for full CRUD. Syntax differs from saved search; learn table and column names from SuiteQL reference.
N/query.runSuiteQL() executes a query string. Returns result set. Paginate for large results. Use for one-off reports or scheduled data extraction. Governance: query execution consumes units. Optimize queries—avoid SELECT *, limit rows. Test in SuiteQL workbench (if available) before scripting. Document queries for team. SuiteQL is powerful but has learning curve.
Workflow vs SuiteScript: When to Use Each
Workflows are declarative: define conditions and actions without code. Use for simple field updates, approvals, email notifications, status changes. Workflows are easier to maintain for non-developers. Limitations: no loops, limited logic, cannot call external APIs. Use workflow when it fits.
SuiteScript for: complex validation, multi-record operations, external integrations, custom calculations, anything workflow cannot do. User Event for record-level logic. Scheduled for batch. Map/Reduce for scale. Consider hybrid: workflow triggers, SuiteScript does heavy lifting. Document why you chose script vs workflow for future maintainers. Revisit—sometimes script can be replaced by workflow after NetSuite adds features.
Debugging Production Issues
Production debugging is constrained: limited logging, no breakpoints. Use log.audit and log.error with context (record ID, key values). Check Execution Log for script runs. Enable Debug for specific scripts temporarily. Reproduce in Sandbox with production data copy if possible.
Common production issues: governance exceeded (add batching, use Map/Reduce), permission errors (check deployment role), missing data (validate input, handle nulls), timing (async, scheduled vs immediate). Add try/catch with detailed error logging. Consider a custom log record for high-volume scripts. Use script parameters to enable verbose logging for troubleshooting. Turn off when resolved.
Custom Record Types and SuiteScript
Custom records extend NetSuite's data model. Create under Customization > Record Types. Define fields, sublists, and permissions. SuiteScript creates, loads, and updates custom records like standard records—use record.create with type: 'customrecord_yourid'. Custom records suit: approval workflows, project tracking, config tables, integration staging. Link to standard records via entity or custom fields. Use for state machines, queues, and cross-module data.
Custom record fields: standard types (text, integer, date, select, etc.) and custom list/multi-select. Sublists on custom records: add child list for line-level data. Scripts iterate with getLineCount and getSublistValue. Index custom fields used in search criteria. Permission: restrict by role. Use script for bulk create when importing custom record data. Document custom record purpose and usage.
Best practices: name clearly (e.g., custrecord_approval_request). Use record type for polymorphic logic. Avoid too many custom records—consolidate when possible. Consider standard records (e.g., Support Case) before custom. Custom records enable workflows—trigger on create/update. Saved searches include custom records. Report on custom data for operational visibility. Integrate with external systems via custom record staging.
SuiteFlow vs SuiteScript: Choosing the Right Tool
SuiteFlow (Workflows) is declarative: define states, transitions, and actions in the UI. No code. Use for: approval routing, field defaults, email notifications, status changes, simple branching. Workflows are maintainable by admins. Limited to supported actions. Complex logic (multi-step calculations, external API calls, dynamic branching) often requires SuiteScript.
SuiteScript is imperative: write code. Full control. Use for: complex validation, integration, custom calculations, record creation with logic, batch processing. Scripts require development, testing, deployment. Choose based on: complexity, maintainability, who will change it. Start with workflow; escalate to script when workflow cannot express the logic. Hybrid: workflow triggers script action for complex steps.
Performance: workflows have their own governance. Scripts have script governance. Both run server-side. Workflows are often faster to implement for standard patterns. Scripts offer optimization (e.g., batch operations). Document why you chose workflow vs script for each automation. Future maintainers need that context. NetSuite continues to add workflow features—re-evaluate periodically.
N/encode and N/crypto: Secure Operations
N/encode provides base64 and other encoding. Use for encoding/decoding payloads. N/crypto provides hashing (SHA-256, etc.) and encryption. Use for secure token generation, data integrity. Both consume governance. Use sparingly. For passwords and secrets, use NetSuite's Secure Credentials. Never log sensitive data. Sanitize logs before storing.
Hashing: create a hash of a string for comparison or integrity. Encryption: encrypt before storing, decrypt when reading. Key management: where do you store keys? NetSuite Secure Credentials or script parameters with restricted access. Compliance: consider data residency and encryption at rest. NetSuite encrypts data at rest; your script may handle data in transit. Document crypto usage for audit.
N/redirect and N/ui: Navigation and UI
N/redirect redirects the user to another URL. Use in Suitelet after processing (e.g., redirect to record after create). N/ui provides UI components: message, createRecord, editRecord. Use in beforeLoad to add buttons or links. resolveScriptUrl and resolveRecordUrl generate URLs for scripts and records. Use for email links, dashboard links.
Client-side: N/currentRecord for form manipulation. N/ui for server-side URL generation. Buttons: addButton in beforeLoad, set url to Suitelet. User clicks, Suitelet runs, redirects back. For multi-step wizards, use multiple Suitelets with redirects. Ensure redirect URLs are valid and secure. Test redirect flow with different roles. Document user flows for support.
SuiteQL and SuiteScript: Query Beyond Search
SuiteQL is SQL-like query language for NetSuite. Access via N/query or REST. Use for complex joins, analytics, reporting integrations. SuiteQL can query standard and custom tables. Use when saved search is insufficient. SuiteQL has different governance. Combine with SuiteScript: run SuiteQL, process results, create records. Ideal for data warehouse feeds, complex reporting.
N/query.runSuiteQL returns rows. Paginate for large result sets. Use for read-only. No insert/update via SuiteQL in script—use N/record for that. SuiteQL supports: SELECT, FROM, WHERE, JOIN, GROUP BY, ORDER BY. Table names differ from record types—check SuiteQL schema. Performance: add filters to limit rows. Use for one-off or scheduled extract. Document queries for maintainability.
Appendix A: Full SuiteScript Examples
Example 1: User Event—Validate and Default Fields on Sales Order
define(['N/record', 'N/search', 'N/log'], function(record, search, log) {
function beforeSubmit(context) {
if (context.type !== context.UserEventType.CREATE && context.type !== context.UserEventType.EDIT) return;
var rec = context.newRecord;
var amount = rec.getValue({ fieldId: 'total' }) || 0;
if (amount > 50000) {
var custId = rec.getValue({ fieldId: 'entity' });
var custRec = search.lookupFields({ type: 'customer', id: custId, columns: ['custentity_cfo_approval'] });
if (custRec.custentity_cfo_approval !== 'T') {
throw new Error('Orders over $50,000 require CFO pre-approval on customer record.');
}
}
}
return { beforeSubmit: beforeSubmit };
});
This beforeSubmit script blocks Sales Order save when the total exceeds $50,000 unless the customer has CFO pre-approval. Use search.lookupFields for efficient single-record lookups. Adapt the threshold and field IDs for your setup.
Example 2: Scheduled Script—Batch Update with Governance Check
define(['N/search', 'N/record', 'N/runtime', 'N/log'], function(search, record, runtime, log) {
function execute() {
var remaining = runtime.getRemainingUsage();
if (remaining < 500) { log.audit('Low governance', 'Rescheduling'); return; }
var soSearch = search.create({
type: 'salesorder',
filters: [['mainline', 'is', 'T'], 'AND', ['status', 'anyof', ['SalesOrd:B']]],
columns: ['internalid']
});
var count = 0;
soSearch.run().each(function(r) {
if (runtime.getRemainingUsage() < 100) return false;
record.submitFields({ type: 'salesorder', id: r.id, values: { custbody_processed: true } });
count++;
return true;
});
log.audit('Processed', count + ' orders');
}
return { execute: execute };
});
This scheduled script processes pending Sales Orders, updates a custom "processed" flag, and checks governance before each iteration. Use record.submitFields for single-field updates to conserve units. Return false from each() to stop iteration and reschedule if needed.
Example 3: RESTlet—Create Customer from External System
define(['N/record', 'N/log'], function(record, log) {
function post(context) {
var body = JSON.parse(context.body);
if (!body.companyname || !body.email) {
return { success: false, error: 'companyname and email required' };
}
try {
var custId = record.create({
type: 'customer',
isDynamic: false,
defaultValues: { subsidiary: body.subsidiary || 1 }
});
custId.setValue('companyname', body.companyname);
custId.setValue('email', body.email);
if (body.phone) custId.setValue('phone', body.phone);
custId.save();
return { success: true, id: custId.getValue('id') };
} catch (e) {
log.error('RESTlet error', e.message);
return { success: false, error: e.message };
}
}
return { post: post };
});
This RESTlet accepts JSON in the POST body and creates a Customer record. Validate input, use try/catch, and return a structured response. Deploy with appropriate authentication (OAuth or token-based).
Example 4: Map/Reduce—Summarize and Write to Custom Record
define(['N/search', 'N/record', 'N/reducer'], function(search, record, reducer) {
function getInputData() {
return search.create({ type: 'transaction', filters: [['mainline','is','T']], columns: ['entity','amount'] });
}
function map(context) {
var v = context.value;
reducer.write({ key: v.values.entity.text, value: parseFloat(v.values.amount) || 0 });
}
function reduce(context) {
var total = 0;
context.values.forEach(function(v) { total += v; });
reducer.write({ key: context.key, value: total });
}
function summarize(s) {
s.mapSummary.iterator().each(function(k, v) {
record.create({ type: 'customrecord_daily_summary', values: { custrecord_entity: k, custrecord_total: v } });
return true;
});
}
return { getInputData: getInputData, map: map, reduce: reduce, summarize: summarize };
});
This Map/Reduce script aggregates transaction amounts by entity and writes summaries to a custom record. The map emits key-value pairs; reduce aggregates by key; summarize runs once at the end. Adjust to your custom record and field IDs.
Example 5: Suitelet—Custom Approval Form
define(['N/record', 'N/ui/serverWidget', 'N/log'], function(record, serverWidget, log) {
function onRequest(context) {
if (context.request.method !== 'GET') return;
var id = context.request.parameters.id;
var rec = record.load({ type: 'customrecord_approval', id: id });
var form = serverWidget.createForm({ title: 'Approval: ' + rec.getValue('name') });
form.addSubmitButton({ label: 'Approve' });
form.addButton({ id: 'reject', label: 'Reject', functionName: 'reject' });
form.addField({ id: 'custpage_memo', type: serverWidget.FieldType.LONGTEXT, label: 'Memo' });
form.clientScriptModulePath = 'SuiteScripts/cs_approval.js';
context.response.writePage(form);
}
return { onRequest: onRequest };
});
This Suitelet renders a custom HTML form for approval. Use N/ui/serverWidget to build forms programmatically. Add client script for button behavior. Validate user permissions before displaying. Redirect after submit.
Example 6: beforeLoad—Add Custom Button and Hide Field
define(['N/ui/serverWidget', 'N/url'], function(serverWidget, url) {
function beforeLoad(context) {
if (context.type !== context.UserEventType.VIEW) return;
var form = context.form;
form.addButton({ id: 'custpage_export', label: 'Export to Excel', functionName: 'exportToExcel' });
form.getField({ id: 'email' }).updateDisplayType({ displayType: serverWidget.FieldDisplayType.HIDDEN });
}
return { beforeLoad: beforeLoad };
});
Use beforeLoad to modify the form: add buttons that call client scripts or Suitelets, hide fields by role, or inject help text. Avoid heavy logic—user is waiting. Keep governance low.
Appendix B: Debugging Checklist for SuiteScript
| Issue | Check | Resolution |
|---|---|---|
| Script fails to deploy | File Cabinet path, Script record config | Verify script file exists; check entry point and record type |
| Governance exceeded | Execution Log for unit usage | Add batching, use Map/Reduce, check runtime.getRemainingUsage() |
| Record not found | ID validity, permissions | Validate ID before load; check deployment role access |
| beforeSubmit not firing | Entry point, record type filter | Confirm create/edit selected; check record type in Script |
| RESTlet 401 | OAuth/token, integration record | Verify credentials; check role permissions |
| Search returns no rows | Filters, date range | Run search manually; add debug log for filter values |
Enable Script Debugger in Sandbox for step-through. Use log.audit with contextual values (record ID, key fields). Check SuiteAnswers for known issues. Test with minimal data first.
Best Practices Summary
1. Use 2.x modules exclusively for new development. 2. Check governance before loops. 3. Prefer submitFields over load/save when updating few fields. 4. Use search.lookupFields for simple lookups. 5. Log errors with context. 6. Test in Sandbox. 7. Use script parameters for configuration. 8. Document complex logic. 9. Consider Map/Reduce for high-volume. 10. Follow NetSuite's coding standards from SuiteAnswers.
YRK Consulting develops SuiteScript solutions for clients. Contact us.