Script NetSuite from the browser console on any page
TLDR: The SuiteScript N modules work from the browser console on any NetSuite page. Four of NetSuite's own script files, injected from the console, bring the module loader and the N modules to any page. Try it using the snippet below.
NetSuite's missing console
Every logged-in NetSuite session carries the N module API that client scripts use.
With N.query, N.record, and N.search you can answer a data question with a quick SuiteQL query, patch a field the form will not let you edit, or try script logic against real records before committing to a deployment.
All that is missing is a place to type that code, and every NetSuite tab has one.
Some pages already load SuiteScript modules
Open any record in NetSuite (eg. customer, invoice, item). Press F12 to open the browser's developer tools, click the Console tab, and paste this:
require(['N/query'], q => console.table(
q.runSuiteQL({ query: 'SELECT * FROM currency' }).asMappedResults()
));(index) id name symbol exchangerate ...
0 1 'US Dollar' 'USD' 1 ...
1 2 'British Pound' 'GBP' 1.34 ...
2 3 'Canadian Dollar' 'CAD' 0.71 ...
3 4 'Euro' 'EUR' 1.14 ...
Record pages carry client scripts, so the web page has already loaded the module loader there. The trick above is well established in the NetSuite community. See Marty Zigman's write-up.
Now paste the same line on your NetSuite dashboard: require is not defined.
The loader only exists on pages where NetSuite happened to need it, and many everyday pages do not have it.
We will fix that.
The four files
Everything needed to load the N modules is served by your NetSuite instance as plain static files, at stable relative paths that are the same on every instance.
Inject the same four files on any page, then require(['N']) will work there too.
Each file has a distinct job.
NsRequire.js
A patched fork of RequireJS 2.1.15 that defines the global require and define.
NetSuite's patches (marked !!!SUITESCRIPT PATCH!!! in the served source) treat module ids starting with N/ as absolute paths.
bootstrap_shared.js
Defines shared globals the SuiteScript runtime expects (window.util, window.log) plus the SuiteScriptModuleLoaderError class.
The next file, bootstrap.js, copies that class into a local variable.
bootstrap.js (2.0 client)
Defines require.SSModuleLoader, an object of callbacks that NsRequire's patches invoke on every module resolution.
It also replaces require.load so modules not already in memory are fetched on demand.
N.js (2.1 client)
A 3 MB compiled bundle of every N module definition plus the N aggregator, a single module that exposes every submodule at once (N.query, N.record, N.runtime, and more).
Loading it pre-registers everything, so require(['N']) resolves from memory instead of the network.
Without it you can still pull individual modules like require(['N/query']), but each one is fetched from the server a file at a time, slow enough to notice.
Load the N modules on any page
Code in the browser console runs with your role's full permissions, in whatever NetSuite account the tab is on. Try running this on your NetSuite dashboard:
const NS_BOOTSTRAP = [
'/javascript/suitescript/lib/NsRequire.js',
'/javascript/suitescript/bootstrap_shared.js',
'/javascript/suitescript/2.0/client/bootstrap.js',
'/javascript/suitescript/2.1/client/N.js',
]; // load order matters; the 2.0/2.1 mix is intentional
function inject(src) {
return new Promise((ok, fail) => {
const s = document.createElement('script');
s.src = src;
s.onload = ok;
s.onerror = () => fail(new Error('failed to load ' + src));
document.head.appendChild(s);
});
}
async function getN() {
if (window.N) return window.N;
if (typeof require === 'undefined') {
for (const src of NS_BOOTSTRAP) await inject(src);
} else if (!require.defined('N')) {
await inject(NS_BOOTSTRAP[3]);
}
// error out instead of hanging
return Promise.race([
new Promise((ok, fail) => require(['N'], ok,
e => fail(new Error("require(['N']) failed: " + (e && e.message || e))))),
new Promise((_, fail) => setTimeout(() => fail(
new Error('N did not load in 10s; reload the tab, let it settle, paste again')), 10000)),
]);
}
window.N = await getN();
const me = N.runtime.getCurrentUser();
console.log(me.name + ' / role ' + me.role);
N.query.runSuiteQL({ query: 'SELECT * FROM currency' }).asMappedResults();Thomas Steiner / role 3
(4) [{ id: 1, name: 'US Dollar', symbol: 'USD', ... }, ...]
That is the whole technique. From here, the kind of one-liners it unlocks:
// fix one field on an invoice from the record's VIEW page
N.record.submitFields({ type: 'invoice', id: 12345, values: { memo: 'corrected' } });
// run an existing saved search and pull the first rows
N.search.load({ id: 'customsearch_overdue_invoices' }).run().getRange({ start: 0, end: 25 });
// list a record's real field ids
N.record.load({ type: 'salesorder', id: 831 }).getFields().sort();Caveats
- Nothing asks for confirmation.
record.deleteexecutes immediately and cannot be undone. - You get the client script module set.
Server-only modules like
N/taskandN/rendernever reach the browser. - SuiteQL returns at most 5000 rows per call.
Page with
ROWNUMif you need more. Tim Dietrich talks about it here. record.loadandrecord.saverun synchronously in client context and block the tab until they return; each has a.promise()variant that can be used as a workaround.
The REST API alternative
The same logged-in session also carries NetSuite's REST API, SuiteTalk REST web services.
A same-origin fetch from the console rides your session cookies.
await fetch('/services/rest/record/v1/phonecall/eid:weekly-sync-jane', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title: 'Weekly sync with Jane', phone: '555-0100' }),
});
The N modules already create, edit, and delete records from the console, so REST earns its place in narrower cases. REST also has more prerequisites and unfavorable limits.
REST prerequisites
- The account must have the REST Web Services feature enabled
- Your role needs the REST Web Services permission
- The SuiteQL endpoint (
/services/rest/query/v1/suiteql) requires the SuiteAnalytics Workbook feature enabled and permission
REST limits from the console
- REST caps SuiteQL at 1000 rows per request vs. 5000 per call from
N/queryin the console - The records API does not cover every record type (eg. Custom sales and purchase transactions)
PATCHnever reaches NetSuite from the console'sapp.netsuite.comorigin (the CDN in front of it rejects the method), so partial updates stay withrecord.submitFields.
Training my replacement
When I first learned this trick, it didn't seem particularly useful to a mere human like myself. Navigating one page further only takes a few seconds. Recently, however, I taught this as a skill to my AI agents, and it has improved my workflow significantly. In my next post(s), I plan to cover how AI uses this trick in the browser, and why this trick beats Oracle's AI Connectors. HINT: no TBA...