Chidi Okwudire IT Professional. ERP Enthusiast. NetSuite Certified (Administrator, SuiteCloud Developer II, and ERP Consultant). Celigo Certified (Level 4+). Passionate About Empowerment Through Knowledge Sharing. Always Eager to Learn.

Saved Search Deployment Failed? Try This Before Resorting to Manual Deployment!

11 min read

Picture the following real-life scenario: You’re in the middle of a go-live, the pressure is on to deliver fast, and you’re working around the clock. You have a complex saved search in Sandbox that you need to deploy to production. SuiteBundler is not an option because the bundle is pulling in too many dependencies. You’re not a developer, and/or you’ve learned from experience that SDF is not guaranteed to work either. You tried the “Copy to Account” feature but got the infamous “Unexpected error.


Before resorting to manually recreating the search with the attendant risk of inadvertently introducing errors not to speak of the frustration of copy-pasting complex formulas, there is one more option that you should consider. This article provides a step-by-step guide on how to quickly recreate a saved search in a target environment using a well-known Chrome plugin and simple code executed in the browser. The best part is that once you grasp the logic, you can confidently execute this strategy without needing a developer.

When deploying a saved search via conventional techniques fails, you can leverage a well-known Chrome extension to extract the saved search definition, tweak the code, and execute it in the browser to recreate the search in the target environment. The best part: This option is not limited to developers; NetSuite administrators and business analysts with tech affinity can follow the steps outlined in this article to successfully deploy their saved searches.

For the more visual learners, the following recording (2:26) summarizes the approach:


NetSuite’s Native Deployment Options

NetSuite offers some options for deploying customizations from one environment to another that should be leveraged whenever possible. While the focus of this article is not on deployment options, a quick refresher might be useful. The interested reader is invited to review this earlier article I wrote on NetSuite deployment options for a more in-depth discussion.

Note that all of the options described below are controlled by features that are not enabled by default in a vanilla NetSuite account. Thus, if you do not find the option in your account, consult the official documentation for how to enable the corresponding feature(s).

Option 1: SuiteBundler

Although this is considered NetSuite’s legacy deployment approach, SuiteBundler remains a powerful solution and the best deployment option in some situations. The biggest drawback with this technique is that it is notorious for pulling in dependencies which may produce bloat and/or possibly lead to overriding artifacts in the target environment.

Note that while SuiteBundler is still supported, per the official documentation, it is no longer being updated with new features.

Option 2: SuiteCloud Development Framework (SDF)

SDF is a developer-oriented toolset created by NetSuite to, among others, address the deployment challenge. It is primarily intended for developers and generally inaccessible to others; setting up a development environment just for deployment is out of the reach of most non-developers. Moreover, while SDF works fine, there are situations where it errors out for unclear reasons. Thus, it is not uncommon for SDF to fail when deploying a saved search.

Option 3: Copy to Account

Copy to Account is effectively SDF “lite”. It is intended to make SDF more accessible to non-developers. With the feature enabled, a Copy to Account link shows up on supported records (typically at the top-right of the page and in edit mode). Clicking this link triggers a wizard that guides the user through the validation and deployment steps. In the background, it is basically executing SDF commands. Thus, where SDF fails, Copy to Account typically fails too. Sadly, the reverse is also true: I’ve seen Copy to Account fail where SDF can be brute-forced to succeed.

When Copy to Account works, it is nice and sweet. But, it is quite common to get the infamous error: An unexpected error occurred and there is insufficient information to provide a resolution. Very helpful, especially when you are under pressure!

Saved search deployment via Copy to account error: An unexpected error occurred and there is insufficient information to provide a resolution.

A Creative Approach for Saved Search Deployment

When all the native options fail, there is always the option to manually recreate the search in the target environment. While there’s nothing wrong with that, it is often painstaking, frustrating, and slow. Moreover, it increases the risk of introducing bugs. Thus, manual deployment should be considered the last resort, only after you have evaluated and exhausted all other options, including the technique I share next.

Step 1: Install NetSuite Search Export Chrome Extension

To get started, install the Google Chrome extension “NetSuite: Search Export” if you do not already have it. As of the time of writing, this is what it looks like in the Chrome web store:

NetSuite search export chrome plugin by David Smith

The NetSuite community has been gifted with this invaluable extension created by David Smith. The extension allows exporting the script equivalent of a saved search in the UI. NetSuite developers regularly use this extension to figure out how to reproduce a search in code. And, as you will see shortly, we will leverage it for search deployment. The good news is that even non-developers can use this tool with some guidance.

Step 2: Expose the Saved Search Source Code

Open the search you want to deploy in edit mode. With the search export Chrome extension installed, you should see an “Export as Script” link as illustrated below. Clicking it will open a popup that contains the code we need.

How to expose saved search code using the NetSuite Search Export Chrome Extension

If you’re a developer, you should understand what you are seeing with no further explanation needed. For non-developers, you do not need to understand every line of code but I suspect that it is intuitive enough for you to gain a general understanding of how that code translates to your search: Section filters in the code matches the Criteria subtab of the search while section columns relates to Results > Columns in the UI.

Step 3: Prepare the Code for Deployment

What we will do next is take the relevant parts of the code from the extension to produce code that we will use to recreate the search in the target environment within the browser. I will try to explain this carefully as I anticipate non-developers in the audience. First, I recommend you use a text editor (online or locally on your machine). I recommend one that has JavaScript syntax highlighting and formatting capabilities. For my demonstration, I used the JavaScript Beautifier from FreeFormatter.com. There are several desktop text editor apps, Notepad++ being my preferred one.

3.1 Create a Code Template

Copy the template below and paste it into your text editor.

require(['N/search'], function (search) {
   try {

      // TODO: Your code goes here

      console.log('Search recreated successfully');

   } catch (e) {
      console.error(e.message);
   }
})

3.2 Copy the Chrome Extension Output

From the Chrome extension, copy the code in the “SS2.X” section and paste it into the template where it says “TODO: Your code goes here” (feel free to remove this line but make sure you paste the code inside that try {} block). Format the code so it is outlined properly and easier to work with.

Your result should look like this:

require(['N/search'], function (search) {
   try {

      var transactionSearchObj = search.create({
         type: "transaction",
         filters: [
            ["posting", "is", "T"],
            "AND",
            ["account.type", "noneof", "@NONE@"]
         ],
         columns: [
            search.createColumn({
               name: "postingperiod",
               summary: "GROUP",
               sort: search.Sort.ASC,
               label: "Period"
            }),
            search.createColumn({
               name: "formulanumeric",
               summary: "SUM",
               formula: "CASE WHEN {account.type} = 'Income' THEN {quantity} ELSE 0 END",
               label: "Qty Sold"
            }),
            search.createColumn({
               name: "formulanumeric",
               summary: "SUM",
               formula: "CASE WHEN {account.type} IN ('Cost of Goods Sold','Expense') THEN {quantity} ELSE 0 END",
               label: "Qty Purchased"
            }),
            search.createColumn({
               name: "formulanumeric",
               summary: "SUM",
               formula: "CASE WHEN {account.type} = 'Income' THEN {quantity} ELSE 0 END - CASE WHEN {account.type} IN ('Cost of Goods Sold','Expense') THEN {quantity} ELSE 0 END",
               label: "Net Qty"
            }),
            search.createColumn({
               name: "formulacurrency",
               summary: "SUM",
               formula: "CASE WHEN {account.type} = 'Income' THEN {amount} ELSE 0 END",
               label: "Total Revenue"
            }),
            search.createColumn({
               name: "formulacurrency",
               summary: "SUM",
               formula: "CASE WHEN {account.type} IN ('Cost of Goods Sold','Expense') THEN {amount} ELSE 0 END",
               label: "Total Cost"
            }),
            search.createColumn({
               name: "formulacurrency",
               summary: "SUM",
               formula: "CASE WHEN {account.type} = 'Income' THEN {amount} ELSE 0 END - CASE WHEN {account.type} IN ('Cost of Goods Sold','Expense') THEN {amount} ELSE 0 END",
               label: "Gross Profit"
            }),
            search.createColumn({
               name: "formulapercent",
               summary: "AVG",
               formula: "sum(case when {accounttype} = 'Income' then {amount} else 0 end - case when {accounttype} IN ('Cost of Goods Sold','Expense') then {amount} else 0 end) / NULLIF(sum(case when {accounttype} = 'Income' then {amount} end),0)",
               label: "Gross Profit %"
            })
         ]
      });

      var searchResultCount = transactionSearchObj.runPaged().count;
      log.debug("transactionSearchObj result count", searchResultCount);
      transactionSearchObj.run().each(function (result) {
         // .run().each has a limit of 4,000 results
         return true;
      });

      /*
      transactionSearchObj.id="customsearch1705670136714";
      transactionSearchObj.title="NSI Transaction Search to Deploy (copy)";
      var newSearchId = transactionSearchObj.save();
      */

      console.log('Search recreated successfully');

   } catch (e) {
      console.error(e.message);
   }
})

The above code is effectively divided into 3 sections:

  • Section 1: The search creation code. In my example above that spans lines 4 – 61. I recognize that it might be hard to tell where this section ends. Proper indentation might help with that but given long code, you might still not be able to visually see the entire block. A tip would be to look out for the line that starts section 2 instead.
  • Section 2: The search execution code. This section starts at the line beginning with `var searchResultCount =… In my example, that is lines 63 – 68.
  • Section 3: The search creation code. The extension author has done some prep work to facilitate our effort. Lines 70 – 74 contain the code for recreating the search but it is commented out (in JavaScript everything between /* and */ is considered a comment; with syntax highlighting, such sections will also be displayed in a different color).

3.3 Modify the Code

Now that we’ve identified the three sections of the code, we are set to make the minor modifications to create a new search:

  • Delete Section 2 – the search execution code – as we do not need it and it will fail in the target environment because the search does not yet exist. Again, this extension is intended for developers thus it makes sense for them to have the code to run the search but we do not need that code when recreating the search.
  • Uncomment Section 3 – the search creation code – by deleting the line with the /* and the line with the */ to convert Section 3 from a comment to executable code.
  • In Section 3, update the id and title of the search as desired. In my example, that means replacing customsearch1705670136714 with something more meaningful (typically the ID of the original search) and updating the title on the next line accordingly.

Here’s the complete code after modifications.

require(['N/search'], function (search) {
   try {

      var transactionSearchObj = search.create({
         type: "transaction",
         filters: [
            ["posting", "is", "T"],
            "AND",
            ["account.type", "noneof", "@NONE@"]
         ],
         columns: [
            search.createColumn({
               name: "postingperiod",
               summary: "GROUP",
               sort: search.Sort.ASC,
               label: "Period"
            }),
            search.createColumn({
               name: "formulanumeric",
               summary: "SUM",
               formula: "CASE WHEN {account.type} = 'Income' THEN {quantity} ELSE 0 END",
               label: "Qty Sold"
            }),
            search.createColumn({
               name: "formulanumeric",
               summary: "SUM",
               formula: "CASE WHEN {account.type} IN ('Cost of Goods Sold','Expense') THEN {quantity} ELSE 0 END",
               label: "Qty Purchased"
            }),
            search.createColumn({
               name: "formulanumeric",
               summary: "SUM",
               formula: "CASE WHEN {account.type} = 'Income' THEN {quantity} ELSE 0 END - CASE WHEN {account.type} IN ('Cost of Goods Sold','Expense') THEN {quantity} ELSE 0 END",
               label: "Net Qty"
            }),
            search.createColumn({
               name: "formulacurrency",
               summary: "SUM",
               formula: "CASE WHEN {account.type} = 'Income' THEN {amount} ELSE 0 END",
               label: "Total Revenue"
            }),
            search.createColumn({
               name: "formulacurrency",
               summary: "SUM",
               formula: "CASE WHEN {account.type} IN ('Cost of Goods Sold','Expense') THEN {amount} ELSE 0 END",
               label: "Total Cost"
            }),
            search.createColumn({
               name: "formulacurrency",
               summary: "SUM",
               formula: "CASE WHEN {account.type} = 'Income' THEN {amount} ELSE 0 END - CASE WHEN {account.type} IN ('Cost of Goods Sold','Expense') THEN {amount} ELSE 0 END",
               label: "Gross Profit"
            }),
            search.createColumn({
               name: "formulapercent",
               summary: "AVG",
               formula: "sum(case when {accounttype} = 'Income' then {amount} else 0 end - case when {accounttype} IN ('Cost of Goods Sold','Expense') then {amount} else 0 end) / NULLIF(sum(case when {accounttype} = 'Income' then {amount} end),0)",
               label: "Gross Profit %"
            })
         ]
      });

      transactionSearchObj.id = "customsearch_nsi_trx_search_example";
      transactionSearchObj.title = "NSI Transaction Search in Target Env";
      var newSearchId = transactionSearchObj.save();

      console.log('Search recreated successfully');

   } catch (e) {
      console.error(e.message);
   }
})

3.4 Switch to the Target Environment and Execute the Code in the Browser

We’re now ready to execute the code!

  • First, be sure to switch to the target NetSuite environment.
  • Then navigate to a record page e.g. open a saved search (in edit mode) or a transaction (in view or edit mode). This is important because, on the landing page and generally on list pages, the necessary modules are not available and you will get the error Uncaught ReferenceError: require is not defined if you try to run the code there.

    Uncaught ReferenceError: require is not defined
  • Open the browser console. In most browsers, F12 takes you there. Otherwise, find the settings menu and look for the option “Developer Tools” or something similar. For my demonstration, I used Google Chrome where F12 works fine.

    Use F12 to open the browser console
  • Navigate to the Console tab, paste the code from the previous step, and press enter to execute it. If all goes well, you should see the log message Search recreated successfully unless you deleted it in a prior step in which case you’ll see no message. As long as you do not see any errors, you’re on track.
  • Navigate to the list of saved searches. Your new search should be there!

That’s all there is to it. And, as demonstrated in the video at the beginning of the article, the entire process can be completed in about 2 minutes which is pretty impressive. That said, there are some gotchas that you must be aware of.


Reflection

Limitations

This approach is not a silver bullet. While it is quite unlikely that creating the search in the target environment will fail if you carefully follow the steps outlined above, there are some important limitations:

  1. Some search types are not scriptable and thus not supported by the Chrome extension. An example is the workflow instance search as pointed out by one of our readers. In such cases, this approach is simply not viable. The good news is that this category is relatively small.
  2. Only the search criteria and results are copied over. Any other configuration would need to be manually recreated. This is largely because NetSuite’s search.create API does not expose most of the options available in the UI. Besides, search criteria, result columns, and a few settings, everything else needs to be recreated. I do not consider this a huge issue as the pain of recreating searches is often with complex criteria/result formulas.
  3. While this approach works for both summary and non-summary searches, if you have a summary search with non-summary fields, the non-summary fields will get omitted. For my demonstration, I deliberately used a summary search with non-summary fields. The following image shows my starting search vs. the recreated search. Notice that the non-summary fields were omitted. This is a limitation of the Chrome extension and can be addressed as I will explain shortly.
Non summary fields are not captured

Advanced Use Cases

Now that you understand the basic pattern, let’s consider a few more advanced use cases. I am pretty sure there are other use cases that you can come up with (please share via the comments if you have any interesting ones).

Account-Specific Values

Imagine your search had an account-specific value. For example, a specific account is referenced by its internal ID in the search criteria. Yes, I know this is not recommended practice but there are situations where we cannot avoid account-specific values. For the sake of illustration, consider an adapted version of our working example with a condition that targets a specific GL account:

Search with account-specific value

As far as I know, no native deployment option supports account-specific values; they either get dropped (bundler) or cause an error (SDF and Copy to Account). However, with our code-driven search recreation approach, you can easily circumvent this limitation. Here’s an example of how the account-specific value will show up in the code.

var transactionSearchObj = search.create({
   type: "transaction",
   filters:
   [
      ["posting","is","T"], 
      "AND", 
      ["account.type","noneof","@NONE@"], 
      "AND", 
      ["accountmain","noneof","112"]
   ]

// Note: This is incomplete code! The rest has been truncated for brevity

If the internal ID is the same in both environments, the steps already described will suffice. If the internal IDs are different, you simply need to find the ID in the target environment and update it in the saved search code (line 10 in the sample code above).

Summary Search with Non-Summary Fields

As mentioned previously, non-summary fields in a summary search will be dropped as the Chrome extension does not include them; I am not sure why. This can be annoying. To work around it, you can take the following approach:

  • Extract the code for the summary search as described above.
  • Create a copy of the search and remove the summary fields i.e. the new search will only have the non-summary fields in the results.
  • Extract the code of the new search and copy only the elements in the columns array.
  • Append them to the columns array of your original search to produce a consolidated search. You will need a comma to properly combine the two groups of columns! Again, familiarity with basic JavaScript syntax (of JSON array syntax) is implied.
  • Execute your search code in the target environment.

It might sound more complicated than it really is. However, as the following demonstration shows, it is rather straightforward!


I hope you’ve found this article to be fun and insightful. It would not have been possible without the following individuals whom I would like to acknowledge.

Firstly, a shout-out to David Smith for developing and sharing the NetSuite Search Export Chrome extension. If you’re reading this, a huge thank you! I want to be like you when I grow up 😉

Secondly, a shout-out to Meir Bulman for bringing this technique to my attention. Thanks, Meir! Although I use the Chrome Extension in question quite regularly (and I suspect most NetSuite developers do too), I had not considered this use case. A light bulb went on in my head as soon as Meir described the approach. It made so much sense and I wondered why I had not seen it before. All I’ve done in this article is outline the steps, hopefully in a way that makes it accessible to more users.

This “accidental” discovery highlights two important things:

  1. The need to remain humble and continue to be open to learning and see things differently.
  2. The need to be surrounded by professionals who can challenge us to become better. Too many NetSuite professionals are “orphaned” in a world of their own (often not of their own making) where they become the expert in their minds and growth takes a hit.

I am privileged to work with strong NetSuite professionals at Prolecto and I am thrilled whenever I get to learn something new, no matter how big or small. By the way, Meir has a cool NetSuite blog too where he writes about the good (accounting) stuff. Check it out!


Finally, share this article within your circles. I bet there’s someone you know who needs to know. Subscribing to the no-nonsense NetSuite Insights newsletter is the best way to never miss an insight. We’re always open to contributors who are willing to share their insights with the community.


NetSuite Insights is proud to partner with Prolecto Resources Inc. – the unrivaled #1 NetSuite Systems Integrator and thought leader in the space! Learn more about how Prolecto can supercharge your NetSuite experience and deliver the best return on your NetSuite investment.

Other Posts You Might Find Interesting

Chidi Okwudire IT Professional. ERP Enthusiast. NetSuite Certified (Administrator, SuiteCloud Developer II, and ERP Consultant). Celigo Certified (Level 4+). Passionate About Empowerment Through Knowledge Sharing. Always Eager to Learn.

3 Replies to “Saved Search Deployment Failed? Try This Before Resorting to Manual Deployment!”

  1. Great help, thanks! I do see that it is not working for all saved searches unfortunately. I am trying for example a Saved Workflow Instance Search. In that case I get a extension error ‘Export as Script Not Supported’ > This search type is not supported by SuiteScript. So it is limited to the SuiteScript Supported Records. But that’s something I can live with :).

Leave a Reply

×