This article was originally published on LinkedIn on January 23, 2026.
Since launching the SuiteQL Query Tool, I've received countless feature requests from the NetSuite community. Some requests are universal - features that benefit everyone. Others are highly specific to particular workflows, industries, or organizational requirements. The challenge has always been: how do you build a tool that serves the broad community while also accommodating the unique needs of individual users and organizations?
The answer is extensibility. And today, I'm excited to announce that the next beta release of SuiteQL Query Tool will include a comprehensive Plugin Architecture that allows developers to extend the tool's functionality without modifying the core script.
Why a Plugin Architecture?
The SuiteQL Query Tool has grown significantly since its initial release. What started as a simple query execution interface has evolved into a comprehensive development environment with AI assistance, schema exploration, document generation, and multiple export options. But even with all these features, there are always going to be capabilities that make sense for some users but not others.
Consider these scenarios:
• Audit and Compliance: A financial services company needs to log every query executed, who ran it, and when - for compliance purposes.
• Custom Exports: A retail organization wants to export query results directly to their proprietary inventory management system.
• Query Templates: A consulting firm wants to provide pre-built query templates specific to their implementation methodology.
• Security Restrictions: An IT department needs to disable certain features (like AI) for specific user groups while keeping them available for others.
• Branding: A NetSuite partner wants to add their company logo and custom styling when deploying the tool to clients.
These are all legitimate requirements, but they don't belong in the core product. They're too specific, too niche, or too organization-dependent. A plugin architecture solves this elegantly: the core tool remains focused and maintainable, while organizations can extend it to meet their specific needs.
Design Goals and Principles
When designing the plugin architecture, I established several guiding principles:
Zero Core Modifications: Plugins must work without any changes to the main suitelet script. You simply upload plugin files to a File Cabinet folder and configure a single setting.
Safe by Default: With no plugins configured, the tool works exactly as it always has. The plugin system is completely opt-in and dormant until activated.
Graceful Degradation: If a plugin fails to load or throws an error, the core application continues to function. Plugin errors are isolated and logged.
Full Lifecycle Access: Plugins can hook into both server-side and client-side events, from query execution to result rendering to export operations.
UI Flexibility: Plugins can inject custom UI elements at 17 different locations throughout the interface, from toolbar buttons to modal dialogs.
Dependency Management: Plugins can declare dependencies on other plugins, ensuring proper load order. Version compatibility is enforced automatically.
Getting Started with Plugins
Enabling the plugin system requires just two steps:
1. Create a plugin folder in your NetSuite File Cabinet. This is where you'll store all plugin files.
2. Configure the folder ID by setting CONFIG.PLUGIN_FOLDER_ID in the suitelet script to your folder's internal ID.
That's it. The next time you load SuiteQL Query Tool, it will automatically scan the folder for plugin files (files ending in .sqt-plugin.js or .sqt-plugin.json), validate them, resolve dependencies, and initialize them in the correct order.
Plugin Loading Process: When the application loads, it queries the File Cabinet for plugin files, parses each one, validates version compatibility, performs topological sorting based on dependencies, and then initializes plugins in order. Any errors during this process are logged to the NetSuite execution log and browser console, but won't prevent the main application from loading.
Plugin File Structure
A plugin is a JavaScript file that returns a configuration object. Here's the basic structure:
(function() {
return {
// Required: Unique identifier
name: 'my-company-audit-logger',
// Required: Semantic version
version: '1.0.0',
// Optional: Minimum SQT version required
minAppVersion: '2026.01',
// Optional: Plugin metadata
description: 'Logs all query executions for compliance',
author: 'My Company',
// Optional: Other plugins that must load first
dependencies: [],
// Optional: Built-in features to disable
disables: [],
// Server-side functionality
server: {
hooks: { /* ... */ },
handlers: { /* ... */ }
},
// Client-side functionality
client: {
hooks: { /* ... */ },
init: function(meta, api) { /* ... */ }
},
// UI injection content
ui: {
'toolbar-start': '<button>...</button>',
'status-bar': '<span>...</span>'
},
// Settings schema
settings: {
schema: { /* ... */ }
}
};
})()
Let's break down the key sections:
Identity and Metadata
Every plugin must have a unique name and version. The name should be prefixed with your organization or project to avoid conflicts (e.g., acme-audit-logger rather than just audit-logger).
The optional minAppVersion field specifies the minimum version of SuiteQL Query Tool required. If a user tries to load your plugin on an older version, it will be skipped with a logged warning.
Dependencies
If your plugin depends on functionality from another plugin, list it in the dependencies array. The plugin loader will ensure dependencies are initialized first, and will log an error if a required dependency is missing.
dependencies: ['acme-core-utils', 'acme-ui-components']
Feature Disabling
One of the most powerful capabilities is the ability to disable built-in features. This is useful for organizations that want to hide certain functionality from users - either for security, simplicity, or compliance reasons.
// Hide all AI features and the export functionality disables: ['ai', 'export']
The system supports over 20 disableable features, including individual AI capabilities (chat, explain, validate), export destinations (Airtable, Google Sheets), and UI elements (history sidebar, dark mode toggle, options panel).
Server-Side Integration
Server-side hooks allow plugins to intercept and modify the query execution lifecycle. This runs on NetSuite's servers, giving you access to the full SuiteScript API.
Available Server Hooks
onBeforeQuery: Called before a query is executed. You can modify the query, add logging, or even cancel execution.
server: {
hooks: {
onBeforeQuery: function(data, plugin) {
// data.query - The SQL about to execute
// data.payload - The full request payload
// data.originalQuery - The unmodified query
// Example: Add a comment with timestamp
data.query = '/* Executed: ' + new Date().toISOString() + ' */
' + data.query;
return data;
}
}
}
onAfterQuery: Called after a query completes successfully. Perfect for logging, analytics, or post-processing results.
onAfterQuery: function(data, plugin) {
// data.query - The executed SQL
// data.response - Query results (records, elapsedTime, rowCount)
// Example: Log to a custom record
log.audit({
title: 'Query Executed',
details: JSON.stringify({
rowCount: data.response.rowCount,
elapsedTime: data.response.elapsedTime,
user: runtime.getCurrentUser().email
})
});
return data;
}
onError: Called when query execution fails. Useful for error tracking and alerting.
onError: function(data, plugin) {
// data.query - The query that failed
// data.error - The error object
// Example: Send alert for certain error types
if (data.error.message.includes('permission')) {
// Trigger notification...
}
}
Custom Server Handlers
Beyond hooks, plugins can register custom server-side endpoints. These are accessible via standard HTTP requests from the client.
server: {
handlers: {
getAuditLog: function(context, payload, modules, plugin) {
// Full access to NetSuite modules
var results = modules.query.runSuiteQL({
query: 'SELECT * FROM customrecord_audit_log ORDER BY created DESC'
}).asMappedResults();
context.response.write(JSON.stringify({ records: results }));
}
}
}
From the client, you'd call this handler with:
fetch(scriptUrl, {
method: 'POST',
body: JSON.stringify({
function: 'plugin_my-plugin_getAuditLog',
// ... additional parameters
})
});
Client-Side Integration
Client-side hooks run in the browser and allow plugins to respond to user interactions, modify the UI, and integrate with the application state.
Available Client Hooks
| Hook | Trigger | Common Uses |
|---|---|---|
onInit |
App initialization complete | Setup, load settings, register event listeners |
onBeforeQuery |
Before query execution | Validation, modification, cancellation |
onAfterQuery |
After successful query | Analytics, notifications, result processing |
onResultsDisplay |
After results render to DOM | Custom formatting, injecting UI elements |
onBeforeExport |
Before export operation | Validation, data transformation, cancellation |
onAfterExport |
After export completes | Logging, notifications, cleanup |
onEditorChange |
Editor content changes | Auto-save, validation, suggestions |
Plugin Initialization and Public API
The init function runs when your plugin loads and can return a public API that other code can access:
client: {
init: function(meta, pluginsApi) {
// meta contains { name, version, description }
// pluginsApi provides saveSettings, loadSettings, etc.
var settings = pluginsApi.loadSettings(meta.name) || {
enabled: true,
logLevel: 'info'
};
// Return public API
return {
isEnabled: function() { return settings.enabled; },
setLogLevel: function(level) {
settings.logLevel = level;
pluginsApi.saveSettings(meta.name, settings);
},
getStats: function() { return { queriesLogged: queryCount }; }
};
}
}
Other code can then access your plugin's API:
var auditPlugin = SQT.plugins.get('acme-audit-logger');
if (auditPlugin && auditPlugin.isEnabled()) {
console.log('Audit logging is active');
}
Settings Persistence
The plugin system provides a built-in settings API that handles persistence to both localStorage (for quick access) and optionally to the File Cabinet (for cross-browser/cross-device persistence):
// Save to localStorage only
SQT.plugins.saveSettings('my-plugin', { enabled: true });
// Save to both localStorage and server
SQT.plugins.saveSettings('my-plugin', { enabled: true }, true);
// Load settings
var settings = SQT.plugins.loadSettings('my-plugin');
UI Injection Points
One of the most visible ways to extend SuiteQL Query Tool is through UI injection. The architecture provides 17 injection points throughout the interface where plugins can add custom HTML content.
| Injection Point | Location | Category |
|---|---|---|
toolbar-start |
After the Run button | Toolbar |
toolbar-end |
Before the options group | Toolbar |
more-dropdown |
End of More dropdown menu | Toolbar |
ai-dropdown |
End of AI dropdown menu | Toolbar |
header-right |
Header actions area | Header |
before-editor |
Above the editor panel | Editor |
editor-toolbar |
End of editor mini-toolbar | Editor |
nl-bar |
After natural language bar | Editor |
results-header |
Results header actions | Results |
results-footer |
Below results content | Results |
sidebar-section |
End of history sidebar | Sidebar |
export-menu |
End of export modal | Modals |
local-library-actions |
Local library modal header | Modals |
modals |
After all built-in modals | Modals |
options-panel |
End of options dropdown | Other |
status-bar |
Status bar left section | Other |
To inject content, simply add the injection point name as a key in your plugin's ui object:
ui: {
'toolbar-end': `
<button class="sqt-btn sqt-btn-secondary sqt-btn-sm" onclick="MyPlugin.showDashboard()">
<i class="bi bi-graph-up"></i>
<span>Analytics</span>
</button>
`,
'status-bar': `
<span id="myPluginStatus" style="margin-left: 8px;">
<i class="bi bi-check-circle" style="color: green;"></i>
Plugin Active
</span>
`
}
Real-World Use Cases
To illustrate the power and flexibility of the plugin architecture, here are some real-world scenarios and how they could be implemented:
Query Analytics Dashboard: Track query performance over time, identify slow queries, and visualize usage patterns. The plugin would use the onAfterQuery hook to log execution times to a custom record, then provide a dashboard UI (injected via the modals injection point) that displays charts and statistics. Injection points used: toolbar-end (dashboard button), modals (dashboard modal), status-bar (quick stats).
Query Approval Workflow: For sensitive environments, require manager approval before certain queries can be executed. The plugin would intercept queries via onBeforeQuery, check against a rules engine, and either allow execution or redirect to an approval workflow. Injection points used: before-editor (pending approval banner), modals (approval request modal).
Custom Export Destinations: Export query results to proprietary systems, data warehouses, or third-party services not supported out of the box. The plugin would add new export options to the export menu and handle the data transmission. Injection points used: export-menu (new export buttons), modals (configuration modal).
Query Template Library: Provide a library of pre-built, tested queries specific to your organization or industry. Users can browse categories, preview queries, and insert them with one click. Injection points used: toolbar-start (templates button), modals (template browser), sidebar-section (recent templates).
Gamification and Training: Add achievements, progress tracking, and guided tutorials for users learning SuiteQL. Track queries executed, features used, and award badges for milestones. Injection points used: header-right (achievement icon), status-bar (XP display), modals (achievements modal).
Compliance and Audit Logging: Log every query executed with full context: user, timestamp, query text, results count, and execution time. Essential for regulated industries requiring audit trails. Server hooks used: onAfterQuery, onError. Injection points used: status-bar (logging indicator), options-panel (logging settings).
The Plugin Marketplace Vision
While organizations can build plugins for their internal use, I believe there's an opportunity for something bigger: a community-driven Plugin Marketplace.
Here's what I'm envisionng: A central repository where developers can share their plugins, users can discover new capabilities, and the entire NetSuite community benefits from collective innovation.
• Discovery: Browse plugins by category, search by functionality, and see ratings and reviews from other users.
• Verified Publishers: Trusted developers and NetSuite partners can become verified publishers with enhanced visibility.
• Easy Installation: One-click installation that automatically downloads the plugin to your File Cabinet.
• Version Management: Automatic update notifications when new versions are available, with changelogs and compatibility info.
• Monetization Options: Developers can offer plugins for free, freemium, or paid - creating a sustainable ecosystem.
• Community Support: Discussion forums, issue tracking, and community contributions for open-source plugins.
The marketplace is still in the planning stages, but I'm excited about the possibilities. If you're interested in being an early plugin developer or have ideas for the marketplace, I'd love to hear from you.
What's Next
The Plugin Architecture will be available in Beta 09 of SuiteQL Query Tool 2026.01. This release includes:
• Complete plugin loading and initialization system
• All 17 UI injection points
• Server-side hooks (onBeforeQuery, onAfterQuery, onError)
• Client-side hooks (onInit, onBeforeQuery, onAfterQuery, onResultsDisplay, onBeforeExport, onAfterExport, onEditorChange)
• Custom server handler registration
• Settings persistence API
• Feature disabling system
• Dependency resolution and version compatibility
• A sample "Query Logger" plugin demonstrating key concepts
• Comprehensive developer documentation
I'm incredibly excited about the possibilities this architecture enables. The SuiteQL Query Tool has always been about empowering NetSuite developers and administrators, and the plugin system takes that to a whole new level. I can't wait to see what the community builds.