SuiteScript 2.1 Inventory: Which Scripts Actually Have to Move Before 2028.2
The banner counts everything, and most of it is not yours
An administrator opens NetSuite, sees "Update Needed: Update Scripts to SuiteScript 2.1", filters the Scripts list by API version, and gets a number in the four figures. Then someone asks for a plan and a budget, and the number is the only thing on the table.
On one customer sandbox we scanned this month, 1,119 scripts were not on 2.1. Of those, 880 belonged to installed SuiteApps and 211 had nothing deployed. The code that customer actually owned and had to change was 28 scripts.
That gap is the whole subject of this article. The deadline is real. The count NetSuite shows you is the wrong one to plan against.
What Oracle has actually committed to
Oracle published the schedule in Transitioning To SuiteScript 2.1 and repeated it in the September minor release notes for 2026.2:
| Release | Change |
|---|---|
| 2026.2 | SuiteScript 2.1 becomes the standard scripting model for new and existing scripts. |
| 2027.1 | SuiteScript 1.0 enters end-of-life support, covering critical issues only. Other problems require converting to 2.1 first. |
| 2028.1 | 1.0 scripts can no longer be deployed in new accounts but remain deployable in existing ones. 2.0 and 2.x scripts run as 2.1 by default. |
| 2028.2 | All new and existing scripts must use SuiteScript 2.1. Legacy script versions will no longer run. |
Scope is the part people misread. It is not only 1.0. Oracle names SuiteScript 1.0, SuiteScript 2.0, and any file annotated @NApiVersion 2.0 or @NApiVersion 2.x. A script written in 2021 with 2.x because that was the template everyone copied is in scope exactly like a 2012 nlapiSubmitRecord script.
When we wrote about Oracle's AI upgrade assistant and where its 1.0 conversions go wrong, the question was how to convert. This one is about the step before: which scripts are on the list at all.
The version column lies about 2.x
Oracle's own Identifying Scripts That Are Not Using SuiteScript 2.1 points you at the script records and their version. That gets you most of the way. The field behind it, script.apiversion, returns one of three values in SuiteQL: 1.0, 2.0, or 2.1.
There is no 2.x. A file annotated @NApiVersion 2.x is stored on the script record as 2.0. We confirmed this on a live account by comparing the record with the file header.
That matters because 2.0 and 2.x are not tested the same way. NetSuite has separate account preferences to run 2.0 scripts and 2.x scripts in the 2.1 runtime (Setup > Company > Preferences > General Preferences), and Oracle's release note tells you to use them: "Test existing SuiteScript 2.0 and 2.x server scripts by using the available company preferences to run them in the SuiteScript 2.1 runtime before updating the annotation." If you want to flip one preference at a time, you need to know which scripts sit behind each one, and the only place that information lives is the first comment block of each file.
So the inventory has two layers. The script records tell you what is not 2.1. The source tells you which 2.0 is really 2.x.
The query that builds the first layer
This is the inventory we run. It lists every script with its file, so the size is available without opening anything:
SELECT s.id, s.scriptid, s.name, s.scripttype, s.apiversion,
s.isinactive, s.frombundle,
s.scriptfile, f.name AS filename, f.filesize
FROM script s
LEFT JOIN file f ON f.id = s.scriptfile
ORDER BY s.id
Two things will trip you up if you try to filter it in SQL.
frombundle is a relationship field. It comes back as a bundle id ("242998"), a comma-separated list of ids when two bundles claim the same script, or null. frombundle IS NULL in a WHERE clause returned wrong results for us, and NVL(s.frombundle, ...) returned the literal string RELATIONSHIP FIELD. Select it raw and split custom from SuiteApp scripts after the query, not inside it.
The second one only bites some accounts. If you add f.hideinbundle to see which SuiteApp files are locked, the query fails on accounts without the SuiteBundler creation feature with Field 'hideinbundle' for record 'File' was not found. Reason: FEATURE_DISABLED. Keep that column optional.
Deployed is not the same as running
A script record with no deployments is not going to break anything in 2028.2. Neither is one whose deployments are all switched off. Oracle's guidance says the same thing in its own words: inactive or obsolete scripts "should be evaluated for retirement instead of migration."
Deployment status is one aggregate away:
SELECT d.script,
COUNT(*) AS total,
SUM(CASE WHEN d.isdeployed = 'T' THEN 1 ELSE 0 END) AS deployed,
SUM(CASE WHEN d.isdeployed = 'T'
AND d.status IN ('RELEASED', 'SCHEDULED', 'TESTING')
THEN 1 ELSE 0 END) AS live
FROM scriptdeployment d
GROUP BY d.script
Use SUM(CASE ...) here rather than anything clever with LISTAGG. LISTAGG(DISTINCT ...) did not remove duplicates when we tried it.
The execution log is the second signal:
SELECT n.scripttype AS script, TO_CHAR(MAX(n.date), 'YYYY-MM-DD') AS lastlog
FROM scriptnote n
GROUP BY n.scripttype
The column name is misleading. scriptnote.scripttype holds the script's internal id, not a type. The log also has limits you should state out loud when you present the numbers: it covers about 30 days, and a script that never calls log.debug or nlapiLogExecution does not appear in it at all. No log line proves nothing. A log line in the last month proves the script ran.
We tried to get real run history from scheduledscriptinstance and gave up. It has no script column, and joining it to deployments returned "Unexpected Error".
One more distinction worth keeping. A scheduled or map/reduce script with a deployed deployment in status Not Scheduled is not idle. Another script can still call task.create on it, and someone can still press Save & Execute. Put those in their own bucket, "on demand", rather than calling them unused.
Four piles, and only two of them are yours
With version, owner and usage on every row, each script falls into one of these:
- Custom 1.0 that is in use. Rewrite. This is real engineering work, and it is where the 1.0 conversion traps live.
- Custom 2.0 or 2.x that is in use. Turn on the matching 2.1 preference in a sandbox, test the business flow, then change the annotation. Often this is a one-line change. Sometimes it is not, see below.
- SuiteApp scripts. Not your code. The vendor ships the update, and your job is to ask for their 2.1 date and write it down. Managed bundles update themselves on the vendor's schedule. Unmanaged ones need you to install the new version.
- Scripts with nothing deployed, or inactive. Retire them. Oracle's own list of options is update, test, or retire, and retire is the cheapest line item you will ever get.
Plan the migration against the first two piles only, and never put the SuiteApp count into an estimate. On the sandbox above, the four-figure banner number turned into 28 scripts of owned work, a vendor email list, and a cleanup. Those are three different projects with three different owners. Adding them up produces a figure that frightens a budget holder and helps nobody do the work.
There is a gap in any inventory built from script records. A SuiteScript 1.0 client script attached directly to a custom form, on the form's Custom Code tab, has no script record. No query against script will ever list it. The only way to find those today is to walk the custom forms. If your account is old enough to have 1.0 anywhere, assume it has some of these.
Reading the source to size the work
File size gives you a first guess at effort without reading a line. Under 10 KB, 10 to 50 KB, and over 50 KB is a crude split, but it is enough to sort a list.
For 1.0, counting API calls tells you more than line count. Every nlapi* and nlobj* call is a line that has to map to an N/ module. A 400-line script that calls seven distinct 1.0 APIs is a different job from a 400-line script that calls sixty. Here is an illustrative row from a demo account, with both layers shown:
Case Intake customscript_acme_case_intake Email Capture 1.0 In use
No @NApiVersion annotation · 449 lines · 83 1.0 API calls (7 distinct)
nlapiLogExecution ×50, nlobjSearchColumn ×20, nlapiSearchRecord ×5,
nlapiCreateRecord ×3, nlapiSubmitRecord ×3, nlapiAttachRecord ×1, nlapiSubmitFile ×1
Fifty of those 83 calls are logging. What is left is 33 calls across six APIs, which is a medium job, not the large one the raw count suggests.
For 2.0 and 2.x, the question is different. The code already uses the N/ modules. What changes is the JavaScript engine underneath, and some things the old engine accepted are syntax errors or behave differently in 2.1. These are the patterns worth searching for before you flip the preference, each with the SuiteAnswers article that covers it:
| Pattern | What happens in 2.1 | SuiteAnswers |
|---|---|---|
for each (var x in list) |
Not valid syntax | 1024752 |
catch (e if e instanceof X) |
Not valid syntax | 1024673 |
obj.toSource() |
Not available | 1024446 |
Reassigning a const |
Throws | 1024711 |
Variables named let, yield, static, await, enum and similar |
Reserved words | 1024580 |
| Assigning to an undeclared variable | Breaks under strict mode | 1024751 |
e.lineNumber, e.fileName, e.rhinoException |
Error object properties differ | 1024551 |
toLocaleDateString() and friends |
Output format can differ | 1024648 |
parseInt(x) with no radix |
Worth checking | 1024484 |
| Any RESTlet | Request body and return type handling differ | 1024612 |
The first two are non-standard extensions from the old engine, and code that uses them does not load at all. Find those before you touch the preference.
A 2.x file with none of these is usually an annotation change and a test pass. Usually. The list is a heuristic, not a proof, and the test on the real business flow is still the thing that tells you it works.
Where we landed
We built this inventory into MokuStudio as a read-only 2.1 Readiness report, because we needed it for our own client accounts and the queries above were getting pasted around by hand. A quick scan runs the SuiteQL and classifies every script in a few seconds. An optional code analysis reads the source of your custom scripts to resolve 2.0 versus 2.x, count 1.0 API usage and flag the patterns in the table. It does not convert anything, and it has the same blind spot as any record-based inventory: 1.0 client scripts on custom forms do not show up.
What it cannot tell you is which scripts matter to the business. That column comes from the people who own the processes, and the order you migrate in should come from them too. Oracle's recommendation to start with "scripts that support critical business processes" and "high-usage scripts, integrations, and scripts with complex dependencies" is the right one. The inventory only makes sure the list you prioritise is the list you actually own.
Sources
Transitioning To SuiteScript 2.1, Oracle NetSuite Help Center
September Minor Release, NetSuite 2026.2, Oracle NetSuite Help Center
Identifying Scripts That Are Not Using SuiteScript 2.1, Oracle NetSuite Help Center