If you have been putting off your first SuiteScript 1.0 to 2.1 migration, this tutorial is for you. We will take a real beforeSubmit User Event script on the Sales Order record, one that validates a required custom field and sets a status field, and convert it line by line into a modern SuiteScript 2.1 script. Along the way I will explain every decision so you understand not just what to change, but why.
SuiteScript 1.0 has been deprecated for years, and NetSuite no longer lets you create new 1.0 scripts in most accounts. Version 2.1 is the current standard: it runs on a modern JavaScript engine, supports ES2019+ syntax, and organizes your code into modules. The mental shift is the hardest part, so let us walk through it together.
Step 1: The original SuiteScript 1.0 script
Here is the complete 1.0 script we are starting from. It runs on beforeSubmit, checks that a custom body field custbody_project_code has a value, throws an error if it does not, and then stamps a custom status field.
function beforeSubmit(type)
{
var context = nlapiGetContext();
nlapiLogExecution('DEBUG', 'beforeSubmit start', 'type=' + type);
if (type == 'create' || type == 'edit')
{
var projectCode = nlapiGetFieldValue('custbody_project_code');
if (!projectCode)
{
throw nlapiCreateError('MISSING_PROJECT_CODE',
'Project Code is required before saving this Sales Order.', true);
}
nlapiSetFieldValue('custbody_order_status', 'validated');
}
}
Notice the hallmarks of 1.0: a bare function with no wrapper, global nlapi* helpers, positional arguments, and a data source (the current record) that is completely implicit. Every one of these is going to change.
Step 2: Add the SuiteScript 2.1 structure
Every 2.x script needs three things that a 1.0 script did not: a JSDoc comment block that tells NetSuite the API version and script type, a define() wrapper that declares the modules you depend on, and a return statement that maps your functions to entry points. Start with the skeleton:
/**
* @NApiVersion 2.1
* @NScriptType UserEventScript
*/
define(['N/record', 'N/runtime'], (record, runtime) => {
const beforeSubmit = (context) => {
// logic goes here
};
return { beforeSubmit };
});
The @NApiVersion 2.1 tag is mandatory and must be exactly right, NetSuite reads it from the source to pick the engine. @NScriptType UserEventScript tells NetSuite which entry points are valid. The define() array lists module paths, and the callback receives them in the same order. Whatever you return is how NetSuite finds your entry points, so the key name must match the entry point exactly.
Step 3: Convert nlapiGetContext() to N/runtime and context.newRecord
In 1.0 you called global functions with no clear source of data. In 2.1 the record you are working on is handed to you directly on the context object as context.newRecord. The execution context (user role, script parameters, environment) now comes from the N/runtime module. The type argument also becomes context.type, compared against the context.UserEventType enum instead of magic strings.
const beforeSubmit = (context) => {
const currentUser = runtime.getCurrentUser();
const newRecord = context.newRecord;
if (context.type === context.UserEventType.CREATE ||
context.type === context.UserEventType.EDIT) {
// ...
}
};
Step 4: Convert nlapiGetFieldValue / nlapiSetFieldValue
The global nlapiGetFieldValue('fieldId') becomes a method call on the record object: newRecord.getValue({ fieldId: 'fieldId' }). Likewise nlapiSetFieldValue becomes newRecord.setValue({ fieldId: 'fieldId', value: ... }). Two big differences: the record is explicit, and arguments are passed as a single options object rather than positionally.
Step 5: Replace nlapiLogExecution with log
Logging is now built in. You do not import or declare it. Simply call log.debug(), log.audit(), or log.error(), each taking an object with title and details properties. So nlapiLogExecution('DEBUG', 'title', 'detail') becomes log.debug({ title: 'title', details: 'detail' }).
Step 6: Handle parameter syntax changes
This is the change that trips up the most people. Nearly every SuiteScript 2.x method takes a single object argument with named properties instead of positional arguments. Where 1.0 accepted nlapiSetFieldValue(fieldId, value), 2.1 wants setValue({ fieldId: fieldId, value: value }). Errors are also different: instead of nlapiCreateError, you import N/error and call error.create({ name, message }), then throw it. Getting a “field not found” or silent no-op almost always means you passed positional arguments to a method that expects a named object.
Step 7: Update the Script Deployment record in NetSuite
Converting the code is only half the job. In NetSuite go to Customization > Scripting > Scripts, and either edit the existing Script record or create a new one, uploading your 2.1 .js file to the file cabinet first. Because the API version changed, the safest path is to create a new Script record so NetSuite reads the fresh JSDoc, then confirm the entry point (Before Submit Function) is populated automatically. Open the Deployments tab, set Applies To Sales Order, choose the audience and status (Testing while you validate, Released when ready), and save.
Step 8: Test in Sandbox and check the Execution Log
Always validate in your Sandbox account before touching Production. Create or edit a Sales Order, leave the Project Code blank, and confirm your error blocks the save. Then fill it in and confirm the status field is stamped. To see your log output, open the Script record and click the Execution Log subtab (or open the Script Deployment and view its log). Anything you sent with log.debug, log.audit, or log.error appears there with a timestamp, so this is your first stop whenever behavior does not match expectations.
The completed SuiteScript 2.1 script
Putting all seven code changes together, here is the finished 2.1 version of our User Event script:
/**
* @NApiVersion 2.1
* @NScriptType UserEventScript
*/
define(['N/record', 'N/runtime', 'N/error'], (record, runtime, error) => {
const beforeSubmit = (context) => {
log.debug({
title: 'beforeSubmit start',
details: 'type=' + context.type
});
if (context.type === context.UserEventType.CREATE ||
context.type === context.UserEventType.EDIT) {
const newRecord = context.newRecord;
const projectCode = newRecord.getValue({
fieldId: 'custbody_project_code'
});
if (!projectCode) {
throw error.create({
name: 'MISSING_PROJECT_CODE',
message: 'Project Code is required before saving this Sales Order.'
});
}
newRecord.setValue({
fieldId: 'custbody_order_status',
value: 'validated'
});
}
};
return { beforeSubmit };
});
Compare this side by side with Step 1 and every transformation should now be recognizable: the wrapper, the module imports, the explicit record, the named-object arguments, the built-in logger, and the returned entry point.
Common mistakes to avoid
Shadowing reserved words. log and util are global objects that NetSuite injects into every 2.x script. If you name a variable or module reference log or util, you clobber the global and your logging silently breaks. Never write const log = ....
Missing the return statement. If you forget to return { beforeSubmit }, your code loads without error but the entry point never fires, and you will stare at a script that appears to do nothing. Any time a deployment seems dead, check the return object first.
Wrong entry point name. The key you return must exactly match the entry point name NetSuite expects, beforeSubmit, afterSubmit, or beforeLoad. A typo like beforesubmit or onBeforeSubmit means NetSuite cannot find your function. Case matters.
Get these fundamentals right and the rest of SuiteScript 2.1 becomes far more approachable. Once you have migrated one User Event script, the same pattern, wrapper, imports, explicit record, named arguments, and returned entry points, applies to Client Scripts, Scheduled Scripts, and Map/Reduce scripts too.
Discover more from The NetSuite Pro
Subscribe to get the latest posts sent to your email.
Leave a Reply