NetSuite: SuiteBrowser Remote App Support

Published on January 13, 2024.

One of SuiteBrowser's features that I've been exploring is its ability to support "remote apps." These are apps that run inside of NetSuite, but whose source code (and any other assets it needs) are hosted externally. There's no SuiteApp or bundle that needs to be installed, and no SuiteScripts needed.

These apps can be developed using whatever language or technologies you'd prefer. However, they're very much like Suitelets and Client Scripts in that they interact with NetSuite using standard SuiteScript modules.

Here's a video of a very basic remote app that displays a list of open Opportunities.

The source code for the app is included at the end of this post. If you're a SuiteScript developer then I think you'll be familiar with most of the code.

The "executeInXojoSync" function is used to make calls into SuiteBrowser's "App Runner" engine and execute SuiteScript. It can be used to do other things as well, such as adjusting the name of the app's window, resizing it, etc.

In the code, you'll see that I'm using executeInXojoSync to load the SuiteScript "N/query" module, run a SuiteQL query, and return the results. I then use the results to generate a table, and load the content into the body of the page.

The code is hosted on one of my servers. When the user opens the app, the code is loaded and executed by SuiteBrowser's App Runner engine.

There's a lot more that can be done with this concept. For example, you can build and market apps that are used by multiple clients, control how the apps are licensed, how trials and upgrades are handled, and so on.

I'll write more about SuiteBrowser and "remote apps" in the future. Stay tuned!

SuiteBrowser Remote App Sample Code

<!DOCTYPE html>
<html>

	<head>
		<title>Opportunity Tracker</title>
		<meta charset="utf-8">
		<meta name="viewport" content="width=device-width, initial-scale=1">
		<link rel="stylesheet" href="supersimple.css">	
		<link rel="stylesheet" href="datatables.css">
		<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
		<script src="https://cdn.datatables.net/1.12.1/js/jquery.dataTables.min.js"></script>			
	</head>
	
	<body onload="initApp();">
		<div id="content"></div>		
	</body>
	
	<script>
	
		function initApp() {
							
			executeInXojo( "setWindowTitle", "Opportunity Tracker" );
							
			var jsCode = "require( [ 'N/query' ] );";			
			
			var jsResult = executeInXojoSync( jsCode );	
						
			jsCode = `
			
				const query = require( 'N/query' );

				let sql = \`
					SELECT
						TranID,
						TranDate,
						BUILTIN.DF( Entity ) AS Customer,
						Title,
						BUILTIN.DF( Employee ) AS SalesRep,		
						'$' || ProjectedTotal AS ProjectedTotal,
						( Probability * 100 ) || '%' AS Probability,
						REPLACE( BUILTIN.DF( Status ), BUILTIN.DF( Type ) || ' : ', '' ) AS Status
					FROM 
						Transaction	
					WHERE
						Type = 'Opprtnty'
						AND BUILTIN.DF( Status ) NOT LIKE '%Closed%'			
					ORDER BY
						TranID
				\`;

				let queryResults = query.runSuiteQL( { query: sql, params: [] } ).asMappedResults();

				JSON.stringify( queryResults, null, 5 );
				
			`;	
			
			var beginTime = new Date().getTime();					
			
			jsResult = executeInXojoSync( "runCode", jsCode );
			
			var elapsedTime = ( new Date().getTime() - beginTime ) ;
									
			var records = JSON.parse( jsResult );
						
			var columnNames = Object.keys( records[0] );

			var thead = '<thead>';
			thead += '<tr>';
			for ( i = 0; i < columnNames.length; i++ ) {
				thead += '<th>' + columnNames[i] + '</th>';
			}
			thead += '</tr>';
			thead += '</thead>';

			var tbody = '<tbody>';
			for ( r = 0; r < records.length; r++ ) {		
				tbody += '<tr>';
				for ( i = 0; i < columnNames.length; i++ ) {
					var value = records[r][ columnNames[i] ];
					if ( value === null ) {
						value = '';
					}
					tbody += '<td>' + value + '</td>';
				}				
				tbody += '</tr>';		
			}	
			tbody += '</tbody>';

			var content = `
				<h1 style="margin-bottom: 9px;">Opportunity Tracker</h1>
				<h5 style="margin-top: 0px; margin-bottom: 6px; color: #4d5f79; font-weight: 600; font-size: 14px;">There are currently ${records.length} open opportunities.</h5>
				<table id="transactionsTable" style="margin-top: 0px; margin-bottom: 12px; width: 100%;">
				${thead}
				${tbody}
				</table>
				<p style="margin-top: 0px; margin-bottom: 24px; font-size: 12px; color: #4d5f79;">
					Elapsed Time: ${elapsedTime}ms.
				</p>
			`;
												
			document.getElementById("content").innerHTML = content;
			
			var dataTableOptions = { 
				"pageLength": 10, 
				"lengthMenu": [ 10, 25, 50, 100, 250, 500, 1000 ] 
			} 	  
				
			$(`#transactionsTable`).DataTable( dataTableOptions );			
			
		}
		
	</script>	
	
</html>
About Me

Hello, I’m Tim Dietrich. I design and build custom software for businesses running on NetSuite — from mobile apps and Web portals to Web APIs and integrations.

I’ve created several widely used open-source solutions for the NetSuite community, including the SuiteQL Query Tool and SuiteAPI, which help developers and businesses get more out of their systems.

I’m also the founder of SuiteStep, a NetSuite development studio focused on pushing the boundaries of what’s possible on the platform. Through SuiteStep, I deliver custom software and AI-driven solutions that make NetSuite more powerful, accessible, and future-ready.

Copyright © 2025 Tim Dietrich.