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.