SuiteScript 2.0 Intellisense for vsCode has Arrived

This blog has afforded me a window into what developers are struggling with. Over the last year+, I’ve seen an uptick in developers using vsCode and SuiteScript 2.0. The problem has been no good solution that brings SuiteScript 2.0 intellisense to vsCode… until now.

Several weeks ago, I began corresponding with Eric Birdsall, another SuiteScript developer who contacted me via my blog. He was interested in a solution to this yet unsolved mystery. When we first talked, he’d been combining NetSuite’s multi-file 2.0 API solution into a single file, much like the 1.0 API. His work produced a lot of excitement, but yielded a very shallow solution, not workable.


Next, I tried reflection, peering back into SuiteScript 2’s modules (‘N/record’, ‘N/search’, ‘N/file’) prototypes to build a working solution. Again, it was way to sparse and unusable.

So Eric plowed head first into a solution. He spent a weekend working through NetSuite’s developer guides and building an extension to vsCode. It’s called SuiteSnippets. Version 1.0.1 is currently available, and it works as expected, bringing intellisense to vsCode. I’d highly encourage you to give it a try. And thank you Eric.


SuiteScript’s Quirky Array Methods

I spent all afternoon chasing this bug down. In the end, I learned that the same line of JavaScript code when run in NetSuite’s debugger acts differently than when run in a RESTlet. How can that be?

First, let’s cover some of the really quirky stuff regarding arrays. All my testing was done in SuiteScript 2.0 under NetSuite version 2020.1. I tested in debugger and in a RESTlet I was working on. At issue: Array methods.

[‘123′,’456′,’789’].find(‘456’) will not run in either debugger or a RESTlet. It throws an error when executing the statement.

[‘123′,’456′,’789’].findIndex(‘456’) same!

[‘123′,’456′,’789’].filter(‘456’) to my amazement, actually works and produces consistent results when run client-side, or server-side in debugger or a RESTlet. Awesome!

Here is where it gets annoyingly confusing.

[‘123′,’456′,’789’].indexOf(‘456’) does not throw an error when executing this statement in either scenario. It runs in both debugger and in a RESTlet. However, in debugger it produces a result of 1 and in a RESTlet (also running SuiteScript 2.0) it produces a result of -1.


Oh… One more thing. Since .filter() works, feel free to give this a try. It’s not as efficient as either .findIndex() or .indexOf(), but in my preliminary testing, it works.

        function getIndexOfInArray(arrayOfString, valueToIndex) {
            var index = -1;
                function (a, i) {
                    if (a == valueToIndex) index = i;
                    return a == valueToIndex;
            return index;

SuiteScript Filter Expression Example

I had trouble getting the ‘anyof’ search operator to work in a complex SuiteScript 2.0 filter expression. Perhaps it’s possible. However, I got tired of messing with it. Here is my workaround.


I also needed my results to be unique. I supplied a list of item names and only had room to associate one internal ID with each name in that list. Here is how I solved that problem.


In my initial test, I included 190 item names in my arrayOfNames. All names in that list matched items where display names did not match item name in the databsae. Some had ‘(COPIED)’ in the item name and were still active. In summary, it was a good sample set.

My test ran is a couple of seconds. It matched 100% of my arrayOfNames (190 out of 190). We have about 40,000 items in our item’s table in NetSuite. This was in sandbox too! So, it was pretty fast.

Here is the code in a form you can paste directly into debugger.

require([‘N/log’, ‘N/search’],
    function (log, search) {
        function getItemIdsByName(arrayOfNames) {
            var myFilters = new Array();
                function (name) {
                    if (myFilters.length > 0) myFilters.push(‘or’);
                        [‘itemid’, ‘is’, name],
                        [‘displayname’, ‘is’, name]
            var mySearch = search.create({
                type: search.Type.ITEM,
                columns: [‘internalid’, ‘itemid’, ‘displayname’],
                filters: [
                    [‘isinactive’, ‘is’, false],
                    [‘itemid’, ‘doesnotcontain’, ‘(COPIED)’],
            var myPages = mySearch.runPaged({ pageSize: 1000 });
            var matches = new Array();
            for (var i = 0; i < myPages.pageRanges.length; i++) {
                var myPage = myPages.fetch({ index: i });
                    function (match) {
                        var itemid = match.getValue(‘itemid’);
                        var displayname = match.getValue(‘displayname’);
                        var internalid = match.getValue(‘internalid’);
                            itemid: itemid,
                            displayname: !displayname ? itemid : displayname,
                            internalid: internalid
                        return true;
            var results = new Array();
                function (name) {
                    var firstMatch = matches.filter(
                        function (a) {
                            // If the itemid or the displayname matches a name in arrayOfNames, it matches.
                            if (a.itemid === name) return true;
                            if (a.displayname === name) return true;
                            return false;
                    if (firstMatch && firstMatch.length > 0) {
                            itemid: name,
                            internalid: firstMatch[0].internalid
            return results;
        var arrayOfNames = [
            ‘Your Item Name here #1’,
            ‘Your Item Name here #2’,
            ‘Your Item Name here #3’,
            ‘Your Item Name here #190’,
        var matches = getItemIdsByName(arrayOfNames);
        // The log function is slow. Don’t call it more than once if you are
        // interested in response time.
        log.debug(‘Matched ‘ + matches.length + ‘ out of ‘ + arrayOfNames.length);
        // If you want to see results and not measure response time.
        // matches.forEach(
        //     function (match) {
        //         log.debug(‘itemid: ‘ + match.itemid + ‘, internalid: ‘ + match.internalid);
        //     }
        // )

SuiteScript – Modify Item Groups Server-Side

If you follow me, you know I’m working on item groups and discovering they are quirky! The same code run from the client-side will not work from the server-side. I recently posted notes about working with Item Groups from the client. As I continued to write client-side SuiteScript, I realized it was not as performant as I would have liked. 

Here are the differences when handling Item Groups between client-side & server-side:

  1. Client-side, to delete an item group you delete the “Item Group”, all sub-items, and the “End of Item Group” item. They are treated just like other item types.
  2. Client-side, to add an item group you add all lines back exactly as they were removed. Add the group, all sub-items and also the trailing end of group. In my previous post, I noted that in order to accomplish this you needed to look up the Item Group definition to get the list of sub-items.
  3. Server-side, you cannot delete the “End of Item Group” item. It throws an error. You delete the “Item Group” and all sub-items and “End of Item Group” go too!
  4. Server-side, when you add an “Item Group” all sub-items and the “End of Item Group” item are added for you. There is no need to look up sub-items. So adding one item, can actually add many lines. However, if you’ve modified anything in the item group, there is no mechanism to remember your changes when you add the group back. It comes back as though it is being added for the first time (which it is).

One more caveat, Item groups are editable. So let’s look at the scenario where you are rearranging a transaction. Keep in mind, some financial transactions are locked and cannot be rearranged. However, a transaction like an estimate which has not become a sales order can be rearranged as follows (meaning reordering line items).

This is my algorithm for doing this from server-side code.

  1. Remove all sublist items from bottom to top.
  2. When you encounter an “End of Item Group” (identified by checking internalId == ‘0’), stop deleting until you reach the “Item Group”. When you delete it, all subsequent lines in the group will be deleted along with it.
    Server-Side Delete
  3. When adding items back, do this from top down.
  4. When add lines and you encounter an “Item Group”, stop and remove all items in the group, with the exception of the “End of Item Group”. That cannot be deleted. After the delete completes, the last line is the “End of Item Group” and the line above it is the “Item Group”. The group is empty!
  5. Then INSERT all sub-items back into the group. In essence, you are re-filling the group with its’ sub-items. Why? In my scenario, it is possible that items in the group have been modified/added/removed since the group was originally added to the transaction.
    Server-Side Add

So that’s it in a nutshell.

My project required an algorithm to reorder items in an estimate. I originally performed this operation client-side. However, it was not performant on lengthy transactions. To fix this, I’m now calling a RESTlet (server-side) to handle all the removal and adding back of line items.

In my sandbox, I was able to take a transaction with 50+ lines that ran over 1 minute and it completed in ~10 seconds. For a transaction with 1-5 items, you could probably get away with running your code client-side.

Hope this saves you some time. I know it took a lot of experimentation for me to get over this speed-bump!

SuiteScript – Item Groups

Programming Item Groups is not well documented. If you plan to do anything with them via SuiteScript, I’d encourage you to review these notes first.

What brings me to this post? I’m currently augmenting NetSuite’s estimate module. In order to accomplish this, I needed a full working knowledge of Item Groups. Here are my notes, mostly for me, but feel free to use them too.

The Basics:

  1. When you add an item group as a single part, NetSuite expands that to include all parts/items included defined in the group.
  2. Once added to a transaction, the items in the group are fully editable (unlike Kits).
  3. Items are added in the quantities defined in the group and at the current price assigned to a customer. This will include the customer’s “Pricing Group” price. If that is not present, then an alternate price level for that customer. And if that is not present, then the item’s base price. So, standard pricing rules apply!
  4. Under the hood of a transaction, Item Groups will include the following lines:
    1. An item Group definition, complete with item name and description. No price.
    2. A line item for each item included in the group’s definition.
    3. An “EndGroup” item, with an internalId of zero.
  5. To delete an item group, you delete the “Group” item. All sub-items will be removed with it.
  6. To add a group item via SuiteScript, it works differently between server-side and client-side. This is very (very, very) important! At the time of this writing, you won’t find this documented anywhere else.

Here are some examples in SuiteScript 2.0. I’m testing in version 2019.2.

When deleting an item group, simply delete the item with type=”Group”. That takes all items between it and including the item with type=”EndGroup”.

Delete Example

If you are executing a server-side script, in a Suitelet or Restlet, adding an Item Group is as simple as adding an item. Just add the group item and the rest will magically follow.

Server-Side Example

The really tricky scenario is when you’re writing a client-side script. In this case you need to know some rules.

  1. You must add the “Item Group” item first.
  2. Then you must add items that will be included in the group. Since Item groups are editable via a client-side script, you can literally add whatever sub-items you want here! However, it might be nice to actually lookup the items in the group and add those.
  3. If you specify a quantity on any sub-items that differ from the quantities defined in the group, it overrides Item Group defined quantities. This automatically changes the total price of the group. When I say overrides, I mean in the transaction you’re currently working on. The actual definition of the Item Group remains unchanged.
  4. If you specify a rate (unit price) or amount (extended price) that differs from the values defined in the group, it override those values and changes the total on the group (again, in the current transaction).
  5. Quantities, rates and amounts are optional on sub-items. Leave them off, you get system-defined values.
  6. You must add a trailing item of type “EndGroup” with an internalId of zero. It must also have a tax code defined (or at least in my system it did).

Here is an example showing how to add an item group in client-side SuiteScript. In my example, my group actually contains 3 sub-items. I’m only adding one. I’m also changing the quantity, rate (unit price) and amount (extended price) on the sub-item. I was verifying that it correctly updated the group’s total price and cost. It did!

Client-Side Example

I plan to return to this post whenever I’m working on Item Groups. There are simply too many rules to remember. I’ll need these notes!

Here is the code in format you can cut and paste into your debugger.

    function (record) {
        var rec = record.load({
            type: record.Type.ESTIMATE,
            id: 933121,                     //Demo Estimate with other line items
            isDynamic: true,
        // In this example line 5 is the item with type=”Group”
        // below it are 3 inventory items
        // below them is 1 item of type=”EndGroup”
            sublistId: ‘item’,
            line: 5,
            ignoreRecalc: true,
        // that last statement just whacked them all!
    function (record) {
        var rec = record.load({
            type: record.Type.ESTIMATE,
            id: 933121,                     //Demo Estimate with other line items
            isDynamic: true,
            sublistId: ‘item’,
            sublistId: ‘item’,
            fieldId: ‘item’,
            value: 157309                   // Group Item that includes 3 other items
            sublistId: ‘item’,
            fieldId: ‘quantity’,
            sublistId: ‘item’
        var id =
    function (record) {
        var rec = record.load({
            type: record.Type.ESTIMATE,
            id: 933121,                     //Demo Estimate with other line items
            isDynamic: true,
        // Group Item
        rec.selectNewLine({ sublistId: ‘item’ });
        rec.setCurrentSublistValue({ sublistId: ‘item’, fieldId: ‘item’, value: 157309 });
        rec.setCurrentSublistValue({ sublistId: ‘item’, fieldId: ‘groupsetup’, value: true });
        // rec.setCurrentSublistValue({ sublistId: ‘item’, fieldId: ‘itemtype’, value: ‘Group’ });
        // rec.setCurrentSublistValue({ sublistId: ‘item’, fieldId: ‘quantity’, value: 1 });
        // rec.setCurrentSublistValue({ sublistId: ‘item’, fieldId: ‘taxcode’, value: 38538 });
        rec.commitLine({ sublistId: ‘item’ });
        rec.selectNewLine({ sublistId: ‘item’ });
        rec.setCurrentSublistValue({ sublistId: ‘item’, fieldId: ‘item’, value: 52505 });
        // rec.setCurrentSublistValue({ sublistId: ‘item’, fieldId: ‘ingroup’, value: true });
        rec.setCurrentSublistValue({ sublistId: ‘item’, fieldId: ‘quantity’, value: 5 }); // Override quantity
        rec.setCurrentSublistValue({ sublistId: ‘item’, fieldId: ‘rate’, value: 1000 });   // Override Unit Price
        rec.setCurrentSublistValue({ sublistId: ‘item’, fieldId: ‘amount’, value: 5000 });  // Override Ext. price
        // rec.setCurrentSublistValue({ sublistId: ‘item’, fieldId: ‘taxcode’, value: 38538 });
        rec.commitLine({ sublistId: ‘item’ });
        // rec.selectNewLine({ sublistId: ‘item’ });
        // rec.setCurrentSublistValue({ sublistId: ‘item’, fieldId: ‘item’, value: 52381 });
        // // rec.setCurrentSublistValue({ sublistId: ‘item’, fieldId: ‘ingroup’, value: true });
        // rec.setCurrentSublistValue({ sublistId: ‘item’, fieldId: ‘quantity’, value: 1 });
        // // rec.setCurrentSublistValue({ sublistId: ‘item’, fieldId: ‘taxcode’, value: 38538 });
        // rec.commitLine({ sublistId: ‘item’ });
        // rec.selectNewLine({ sublistId: ‘item’ });
        // rec.setCurrentSublistValue({ sublistId: ‘item’, fieldId: ‘item’, value: 52148 });
        // // rec.setCurrentSublistValue({ sublistId: ‘item’, fieldId: ‘ingroup’, value: true });
        // rec.setCurrentSublistValue({ sublistId: ‘item’, fieldId: ‘quantity’, value: 1 });
        // // rec.setCurrentSublistValue({ sublistId: ‘item’, fieldId: ‘taxcode’, value: 38538 });
        // rec.commitLine({ sublistId: ‘item’ });
        // End Group
        rec.selectNewLine({ sublistId: ‘item’ });
        rec.setCurrentSublistValue({ sublistId: ‘item’, fieldId: ‘item’, value: 0 });
        rec.setCurrentSublistValue({ sublistId: ‘item’, fieldId: ‘itemtype’, value: ‘EndGroup’ });
        rec.setCurrentSublistValue({ sublistId: ‘item’, fieldId: ‘taxcode’, value: 38538 });
        rec.commitLine({ sublistId: ‘item’ });
        var id =

Checking the Status of Background Scripts – SuiteScript Example

I was running into governance issues with long-running tasks. My workaround: launch a map/reduce script in the background to perform the lengthy task. The problem: How to let a non-Administrator know when their background script had completed. This turned out to be easy, but undocumented. 

Check NetSuite’s documentation for “search.Type.” You’ll find a long list of record types searchable by the ‘N/search’ module. Unfortunately, there is at least one missing entry: “scriptexecutionlog”.

Here is a working example in SuiteScript 2.0. Notice, I’m using require() and not define(), so this is ready to paste into debugger. Also, don’t forget to comment out a couple of my filters.

Script Execution Log Code

One more note. I looked long and hard to find a way other than reading server logs to see the status of my background scripts. However, I control both Title and Detail fields written by the map() function in the map/reduce scripts in the background. So I can tell my user whatever I choose as they read their log files. The map reduce script can calculate “Percent complete”, or I can just show “Running” until the job ends. Until I find a better way… that’s the plan.

Here is the code in format you can more easily copy.

require([‘N/search’, ‘N/log’],
    function (search, log) {
        var scriptLog = new Array();
         var mySearch = search.create({
            type: ‘scriptexecutionlog’,
            columns: [
                search.createColumn({ name: ‘date’, sort: ‘DESC’ }),
                search.createColumn({ name: ‘detail’ }),
                search.createColumn({ name: ‘internalid’ }),
                search.createColumn({ name: ‘name’, join: ‘script’ }),
                search.createColumn({ name: ‘scriptfile’, join: ‘script’ }),
                search.createColumn({ name: ‘scriptid’, join: ‘script’ }),
                search.createColumn({ name: ‘scripttype’, join: ‘script’}),
                search.createColumn({ name: ‘time’ }),
                search.createColumn({ name: ‘title’ }),
                search.createColumn({ name: ‘user’ }),
            filters: [
                [‘’, ‘startswith’, ‘Logic’],
                [‘user’, ‘is’, ‘Kevin McCracken’],
                [‘date’, ‘onorafter’, ‘yesterday’],
                [‘script.scripttype’, ‘is’, ‘MAPREDUCE’],
         var myPages = mySearch.runPaged({ pageSize: 1000 });
         for (var i = 0; i < myPages.pageRanges.length; i++) {
            var myPage = myPages.fetch({ index: i });
               function (result) {
                var date = result.getValue({ name: ‘date’ });
                var detail = result.getValue({ name: ‘detail’ });
                var internalid = result.getValue({ name: ‘internalid’ });
                var name = result.getValue({ name: ‘name’, join: ‘script’ });
                var scriptfile = result.getValue({ name: ‘scriptfile’, join: ‘script’ });
                var scriptid = result.getValue({ name: ‘scriptid’, join: ‘script’ });
                var scripttype = result.getValue({ name: ‘scripttype’, join: ‘script’ });
                var time = result.getValue({ name: ‘time’ });
                var title = result.getValue({ name: ‘title’ });
                var user = result.getValue({ name: ‘user’ });
                objEntry = {
                    date: date,
                    detail: detail,
                    internalid: internalid,
                    name: name,
                    scriptfile: scriptfile,
                    scriptid: scriptid,
                    scripttype: scripttype,
                    time: time,
                    title: title,
                    user: user

Updated NetSuite Sync Quick Start

I’m working from home due to the COVID-19 lockdown in our area. This forced me to revisit installing NetSuite Sync. Here is my updated quick start. It’s gotten easier.

NetSuite Sync is a tool that allows you to upload files from your computer to the NetSuite file cabinet. As a developer, I use it to update JavaScript files I’m writing to the cloud.

Here is my original blog post on this topic.

Skip the part about using Git Bash… that was unnecessary. Just set the environment variable as I’ve done below. Remember to say NO to “Sandbox Account?” whether you intend to connect to Sandbox or not. Also, if you are connecting to your sandbox, then the next question will be “Select an Account.” That’s where you pick a sandbox account.

The image below is from within vsCode where I’ve opened a terminal window and installed NetSuite Sync.

NetSuite Sync - How To

Also, don’t forget to set the keyboard shortcut so you can upload via [ctl] + [alt] + u.

NetSuite Support – Who owns my issue?

I’ve written a number of blog posts on NetSuite performance and support. First let me say up front that this post ends positive. My company has had our bumps and bruises with NetSuite, but overall it is a very good system. I’ve had conversations with influential people at NetSuite and they are listening. And now, here’s my update. 

Yesterday, we experienced a huge slowdown. Response times to edit a sales order went from around 8 seconds to over a minute. It wasn’t every sales order. It was off and on. More off than on. Poor response times seemed to come in waves. When things slowed, they slowed for everyone, all transactions, all browsers, all networks.

I picked up the phone and called NetSuite support. The phone system immediately alerted me that NetSuite was experiencing a problem with a subset of accounts and call times might be longer than normal. Fantastic! They were on it. While on hold I checked their status page. Again, it showed there was a problem. No details, but they seemed to be aware. In the past, the status page had been very far behind reality. It was good to see it was keeping up this time.

Both of these two things had been on my radar and were a part of a phone conversation I had with Michael McLoughlin, NetSuite’s Senior Director of Support, back in July of 2019. Michael called me, a customer, to discuss how he might improve my NetSuite experience. We talked for over an hour about status page, phone system issues, SuiteAnswers, etc. Michael asked me a couple of questions and then listened to my lengthy response.

Back to yesterday’s performance issue. I stayed on the line because I’ve done several performance studies and presentations on the topic. I thought I might be able to help. Interestingly enough, Kris, the customer service rep told me I was not one of the affected customers. We pushed through that and began looking into my problem.

I asked for a GoToMeeting. He obliged. I showed him my response times by opening a handful of sales orders. Response times were horrific. He pulled up log files and concluded the problem was client scripts. It must not be a NetSuite issue. It had to be browser or scripts I’d written that were causing the slowdown. We iterated through him explaining this to me multiple times and finally I stopped him. “We agree on the fact that it is client scripts, but you are telling me I own this. I say this is a NetSuite issue and no matter how many times you explain it to me, I do not accept your premise.”

To his credit, Kris stayed on the call and watched me show him poor response times in multiple browsers and multiple networks. We opened Chrome Dev Tools and used performance monitor to find a script that was taking huge amounts of time to complete. I showed him the script name and I guessed it was owned by NetSuite. I knew with certainty it was not owned by me.

After more than an hour on the phone, Kris asked if he could take the problem offline and visit with the performance team about it. Shortly after that I received an email letting me know that my problem had become a real “Issue”, number 577720.

This morning the fix for issue 577720 was released into my account. The issue was titled “Performance > Overall Slowness > High Client Time.” After receiving the email, I was curious about the problem. I looked up issue 577720. All it included was the title. The details read, “Alternate Solution: None.”

Issue 577720

I’m guessing NetSuite would have fixed my problem whether I stayed on the line or not. Don’t know. Certainly, I can’t tell from the details.

I want to commend Michael McLoughlin for the work he’s done improving NetSuite customer support, documentation and status page. His improvements have been very helpful to us customers!

Michael, if you are listening, here’s one more tip. Please put some details in my issue and possibly on the status page. It will help us both next time when I call with a performance problem. I was a Network Engineer for a large company for a decade and ran a software consulting company for over a decade. I’ve been around the block. Getting some detail regarding what caused this slowdown will help me better understand the 8 to 10 seconds it takes on average to edit a sales order.

To keep things in perspective, delivering a sales order to my screen is more complicated than landing a man on the moon. There is transaction routing, content delivery, shared hosting, data center management, disaster recovery and more involved in getting me that screen. Lots of code, multiple owners. The more I know about your end, the better equipped I’ll be to help you fix me next time!


Modifying Estimate Statuses

Here is another one of those things that is ridiculously easy to do and hard to find in the documentation. I found it once, but when I went back to make a change… Well.. frustration!

I wanted to add a status to an estimate. We have this concept of an estimate being in “Draft” status. Where is that change made? Oh… and we call our estimates quotations.

Status on Quote

This list is found under Setup >> Sales >> Customer Statuses. Of course! How could I miss it (to be read in a very facetious tone)?

Menu options

Here is where I added “Draft” and set its probability.

Customer Status List

I searched the web for “estimate statuses”, “transaction statuses”, “document statuses” and many other permutations. And… No luck. Hopefully next time, I’ll find this blog post!

SuiteScript & SuiteTalk – Cannot Access Sublist Field

This one is obvious, and yet it got me again. You save a sublist field on a transaction. Then you go to read it back and it’s not there. What the heck happened? There never was an error message indicating something went wrong.

Here’s an example event script. It is not a fully working example. It just highlights the problem.

code example

The problem is this. In whatever form you execute your event script, the sublist field you are updating is not exposed. To correct this…

If you are trying to write to the field using SuiteScript. Edit the form where you are running your event script.

Customize Form

Go to the sublist fields tab.

Sublist Fields tab

Make sure the field you are updating is visible in the form. This is set via a checkbox.

For SuiteTalk, make sure when you create your transaction (in this case a sales order) that you are using a form where the field is visible. You must specify the form by its internal ID.


Both of these scenarios result in no error and no action!