The four script types we have covered so far account for most of the code in a typical account. They are also the ones people remember they have.
Then there is the other group. Portlets, Mass Update scripts, and Workflow Action scripts survive migrations not because they are hard, but because nobody thinks of them. They are small, they were written once by someone who has left, and the person doing the audit scrolls past them.
All three are quick conversions. The problem is finding them, and one of them loses a feature in 2.x that you need to know about before you deploy.
Why these three hide
A script type filter on the Scripts list will show you every one of them, so in theory nothing hides. In practice these get missed because of where they are used rather than where they are stored.
A Portlet lives on somebody’s dashboard. If it is on the CFO’s home page and nowhere else, it does not come up in conversation. A Mass Update script sits in a menu that gets opened twice a year. A Workflow Action script is buried inside a workflow state, and the people who maintain that workflow think of it as part of the workflow, not as a script.
So when you do the audit, do not stop at the Scripts list. Open your active workflows and look at every Action of type Custom Action. Open Lists > Mass Update > Mass Updates and look under Custom Updates. And ask three or four people to send you a screenshot of their NetSuite home page, because that is genuinely the fastest way to find portlets nobody documented.
Portlets
The 1.0 shape is a function that receives the portlet object and paints into it.
function renderPortlet(portlet, column)
{
portlet.setTitle('Open Cases');
var html = '<ul>';
var results = nlapiSearchRecord('supportcase', 'customsearch_open_cases');
for (var i = 0; results && i < results.length; i++)
{
html += '<li>' + results[i].getValue('casenumber') + '</li>';
}
html += '</ul>';
portlet.setHtml(html);
}
In 2.1 the entry point is render, and it takes a single params object.
/**
* @NApiVersion 2.1
* @NScriptType Portlet
*/
define(['N/search'], (search) => {
const render = (params) => {
const portlet = params.portlet;
portlet.title = 'Open Cases';
let html = '<ul>';
search.load({ id: 'customsearch_open_cases' })
.run()
.each((result) => {
html += '<li>' + result.getValue('casenumber') + '</li>';
return true;
});
html += '</ul>';
portlet.html = html;
};
return { render };
});
The pattern is the same one you have seen all series: methods became properties. setTitle() is now portlet.title, setHtml() is now portlet.html. If you are adding fields or columns, note that the name parameter is now id, and just is now align.
The two methods that are gone
This is the part to read before you deploy. setRefreshInterval() and setScript() do not exist in 2.x. There is no property equivalent. They were removed.
If your portlet was auto-refreshing itself every few minutes, that behaviour disappears the moment you convert it, and nobody will tell you. The dashboard will simply show stale numbers and everyone will assume the data is right. Whoever depends on that portlet needs to know before the switch, not after.
If the refresh genuinely matters, you can do it yourself in the HTML you emit, with a small piece of client-side JavaScript. It is not elegant and you should think about whether a portlet is the right home for the information at all, but it works.
While you are in there
Two things worth fixing at the same time, since you are already editing the file.
Portlets get 1,000 usage units, and they run on page load, for everyone whose dashboard has them. A portlet that loads records in a loop is not just slow for itself, it is slow for the person’s whole home page, every single time they log in. If yours is loading records, switch it to a search that returns the values directly.
Also note that .run().each() stops at 4,000 results. On a portlet that is almost never a real constraint, but if you converted a loop that used to run over everything, be aware the ceiling is there.
Mass Update scripts
These are the smallest conversion in the entire series. The 1.0 version takes two loose parameters:
function massUpdate(rec_type, rec_id)
{
var rec = nlapiLoadRecord(rec_type, rec_id);
rec.setFieldValue('custbody_reviewed', 'T');
nlapiSubmitRecord(rec);
}
The 2.1 version uses the each entry point, and the parameters are properties on a single object: params.type and params.id.
/**
* @NApiVersion 2.1
* @NScriptType MassUpdateScript
*/
define(['N/record'], (record) => {
const each = (params) => {
try {
record.submitFields({
type: params.type,
id: params.id,
values: { custbody_reviewed: true }
});
} catch (e) {
log.error({
title: 'Failed on ' + params.type + ' ' + params.id,
details: e
});
}
};
return { each };
});
Two things I changed beyond the mechanical translation, and I would change both in your version too.
The load-and-save became submitFields. Mass Update scripts get 1,000 usage units per record invocation, which sounds generous until you remember a transaction load plus save is 40 of them. submitFields does the same job for 10 and does not run sourcing you did not ask for.
The try/catch is not optional. A Mass Update script has no summarize stage, no error iterator, and no built-in way to tell you which records failed. If you do not catch and log per record, a failure is just a number in the mass update results screen with nothing behind it. Catch it, log the record ID, and you have a list to work from.
And the broader question, while you are here: if the Mass Update is doing real work over thousands of records, it probably wants to be a Map/Reduce driven by a saved search instead. Mass Update is a fine tool when a human needs to pick the records from a list and press go. It is a poor tool for scheduled bulk work, and we covered the alternative earlier in the series.
Workflow Action scripts
The 1.0 version is a bare function that reads the record through globals and returns a value.
function setApprovalTier()
{
var rec = nlapiGetNewRecord();
var total = parseFloat(rec.getFieldValue('total') || 0);
if (total > 50000) return 'EXEC';
if (total > 10000) return 'DIRECTOR';
return 'MANAGER';
}
In 2.1 the entry point is onAction, and the record arrives on the context object instead of through a global.
/**
* @NApiVersion 2.1
* @NScriptType WorkflowActionScript
*/
define([], () => {
const onAction = (context) => {
const rec = context.newRecord;
const total = parseFloat(rec.getValue({ fieldId: 'total' }) || 0);
if (total > 50000) return 'EXEC';
if (total > 10000) return 'DIRECTOR';
return 'MANAGER';
};
return { onAction };
});
You get context.newRecord and context.oldRecord, and the old id parameter is now workflowId.
The thing to preserve carefully is the return value. Whatever onAction returns gets written into the field configured on the workflow action, and the workflow then branches on it. That contract is invisible from inside the script file. If you refactor the function and change what it returns, or return nothing on some path you did not think about, the workflow does not error. It just takes a different branch, silently, and someone notices next month that approvals have been going to the wrong person.
So before you touch one of these, open the workflow, find the action, and write down which field it sets and what the transitions look for. Then make sure your 2.1 version returns exactly those values on exactly the same conditions.
Deploying all three
The rollback story here is better than anything else in this series, because all three are attached rather than scheduled.
For a Portlet, deploy the 2.1 version as a new script, add it to your own dashboard first, compare it to the old one side by side, then swap it on the dashboards that use it. The old portlet stays available the whole time.
For a Mass Update, both versions show in the Custom Updates list at once. Run the new one against a handful of records with a tight filter, check the results, then use it for real and remove the old one from the menu.
For a Workflow Action, the swap happens inside the workflow action record, and it is a single field. Point it at the new script, test the workflow end to end in Sandbox, and if anything is off, point it back.
In all three cases, leave the 1.0 script in place and unused for a month before deleting it.
That is the last of them
Portlets, Mass Updates and Workflow Actions are usually a single afternoon’s work between them, which is exactly why they get left until the end and then forgotten entirely. Do them now, while you still have the migration context loaded in your head, rather than discovering one of them in 2027 because a dashboard went blank.
The full audit is the thing that catches them, and it only catches them if you look past the Scripts list to the places they are actually used.
The NetSuite Pro runs SuiteScript audits and handles 1.0 to 2.1 migrations end to end as fixed-fee engagements. Get in touch.
Discover more from The NetSuite Pro
Subscribe to get the latest posts sent to your email.
Leave a Reply