Plugins

Extend OpenAnalyst with custom data sources, visualizations, agent capabilities, and export formats. Build, test, and publish plugins to the marketplace.

Plugin Architecture Overview

The OpenAnalyst plugin system allows developers to extend the platform with custom functionality that integrates natively into the web application. Plugins run in an isolated sandboxed environment within the browser, communicating with the host application through a structured message-passing API. They have access to a defined set of platform capabilities depending on the permissions declared in their manifest.

Plugins are distributed as ZIP archives containing a manifest file, JavaScript entry point, and any static assets. They are installed by workspace administrators through the Settings page or the plugin marketplace and can be enabled or disabled per workspace.

Note: Plugins execute entirely within the user's browser for web app plugins, or within the OpenAnalyst agent runtime for agent plugins. They do not run on OpenAnalyst servers and cannot access other workspaces' data.

Plugin Types

There are four categories of plugins, each targeting a different extension point:

TypeKey IdentifierWhat It Extends
Data Source PlugindatasourceAdds a new connector type to the dataset library (e.g., a proprietary API or internal data warehouse)
Visualization PluginvisualizationAdds custom chart or table types to dashboards and reports
Agent PluginagentProvides additional tools and skills that AI agents can invoke during a session
Export PluginexportAdds new export destinations or file formats (e.g., push to Notion, export as PPTX)

Plugin Manifest

Every plugin must include a manifest.json file at the root of its distribution archive. The manifest declares the plugin identity, type, entry point, required permissions, and configuration schema.

{
  "id": "com.example.radar-chart",
  "name": "Radar Chart",
  "version": "1.2.0",
  "description": "Adds a customizable radar/spider chart visualization type.",
  "author": {
    "name": "Example Corp",
    "email": "plugins@example.com",
    "url": "https://example.com"
  },
  "type": "visualization",
  "entry": "dist/index.js",
  "icon": "assets/icon.svg",
  "minAppVersion": "2.0.0",
  "permissions": [
    "read:query_results",
    "write:dashboard"
  ],
  "configSchema": {
    "type": "object",
    "properties": {
      "colorScheme": {
        "type": "string",
        "enum": ["default", "monochrome", "warm", "cool"],
        "default": "default",
        "title": "Color Scheme"
      },
      "showLabels": {
        "type": "boolean",
        "default": true,
        "title": "Show axis labels"
      }
    }
  }
}

Plugin Entry Point and Lifecycle Hooks

The entry point file must export a default object conforming to the plugin interface for its declared type. The plugin host calls lifecycle hooks at predictable moments during the plugin's existence within the application.

// dist/index.js (Visualization Plugin entry point)
import { registerVisualization } from '@openanalyst/plugin-sdk';

export default registerVisualization({
  // Called once when the plugin is first loaded
  onLoad(context) {
    console.log('Radar Chart plugin loaded, version:', context.pluginVersion);
  },

  // Called when a chart instance is created on a dashboard
  onMount(container, queryResults, config) {
    renderRadarChart(container, queryResults, config);
  },

  // Called when query results or config change while the chart is displayed
  onUpdate(container, queryResults, config) {
    updateRadarChart(container, queryResults, config);
  },

  // Called when the chart is removed from the dashboard
  onUnmount(container) {
    destroyRadarChart(container);
  },

  // Called when the plugin is uninstalled from the workspace
  onUnload() {
    // Clean up any global state
  },
});

Plugin API

Plugins communicate with OpenAnalyst through the plugin API object injected into their context. The available methods depend on the declared permissions.

// Context object available in lifecycle hooks
interface PluginContext {
  pluginId: string;
  pluginVersion: string;
  workspaceId: string;

  // Storage: key-value store scoped to this plugin and workspace
  storage: {
    get(key: string): Promise<unknown>;
    set(key: string, value: unknown): Promise<void>;
    delete(key: string): Promise<void>;
  };

  // Notifications
  notify: {
    success(message: string): void;
    error(message: string): void;
    info(message: string): void;
  };

  // Access to query results (requires read:query_results permission)
  queries: {
    getLatestResults(queryId: string): Promise<QueryResults>;
    runQuery(sql: string, datasetId: string): Promise<QueryResults>;
  };

  // Dashboard write access (requires write:dashboard permission)
  dashboard: {
    addWidget(config: WidgetConfig): Promise<void>;
    updateWidget(id: string, config: Partial<WidgetConfig>): Promise<void>;
  };
}

Example: Creating a Custom Chart Plugin

The following walkthrough creates a minimal but functional radar chart plugin using D3.js.

  1. Scaffold the plugin directory structure:
    radar-chart/
    ├── manifest.json
    ├── package.json
    ├── src/
    │   └── index.ts
    ├── assets/
    │   └── icon.svg
    └── dist/          # generated by build
  2. Install the plugin SDK and build dependencies:
    npm install @openanalyst/plugin-sdk d3
    npm install --save-dev typescript esbuild @types/d3
  3. Implement the visualization:
    // src/index.ts
    import * as d3 from 'd3';
    import { registerVisualization } from '@openanalyst/plugin-sdk';
    
    function render(container: HTMLElement, rows: Record<string, number>[], config: Record<string, unknown>) {
      const categories = Object.keys(rows[0]);
      const values = categories.map((c) => rows[0][c] as number);
      const max = d3.max(values) ?? 1;
      const width = container.clientWidth;
      const height = container.clientHeight;
      const radius = Math.min(width, height) / 2 - 40;
    
      d3.select(container).selectAll('*').remove();
    
      const svg = d3.select(container)
        .append('svg')
        .attr('width', width)
        .attr('height', height)
        .append('g')
        .attr('transform', `translate(${width / 2},${height / 2})`);
    
      const angleSlice = (Math.PI * 2) / categories.length;
      const rScale = d3.scaleLinear().range([0, radius]).domain([0, max]);
    
      // Draw axes
      categories.forEach((cat, i) => {
        const angle = angleSlice * i - Math.PI / 2;
        svg.append('line')
          .attr('x1', 0).attr('y1', 0)
          .attr('x2', rScale(max) * Math.cos(angle))
          .attr('y2', rScale(max) * Math.sin(angle))
          .attr('stroke', '#444').attr('stroke-width', 1);
    
        if (config.showLabels) {
          svg.append('text')
            .attr('x', (rScale(max) + 12) * Math.cos(angle))
            .attr('y', (rScale(max) + 12) * Math.sin(angle))
            .attr('text-anchor', 'middle')
            .attr('fill', '#ccc')
            .attr('font-size', '12px')
            .text(cat);
        }
      });
    
      // Draw data polygon
      const radarLine = d3.lineRadial<number>()
        .radius((d) => rScale(d))
        .angle((_, i) => i * angleSlice)
        .curve(d3.curveLinearClosed);
    
      svg.append('path')
        .datum(values)
        .attr('d', radarLine)
        .attr('fill', 'rgba(255, 133, 82, 0.3)')
        .attr('stroke', '#ff8552')
        .attr('stroke-width', 2);
    }
    
    export default registerVisualization({
      onMount(container, results, config) { render(container, results.rows, config); },
      onUpdate(container, results, config) { render(container, results.rows, config); },
      onUnmount(container) { d3.select(container).selectAll('*').remove(); },
    });
  4. Build and package:
    # Build
    npx esbuild src/index.ts --bundle --outfile=dist/index.js --format=esm
    
    # Package as ZIP
    zip -r radar-chart-v1.2.0.zip manifest.json dist/ assets/

Example: Creating a Data Source Plugin

A data source plugin registers a new connection type that users can select when adding a dataset. It must implement a connection test method and a data fetch method.

// src/index.ts
import { registerDataSource } from '@openanalyst/plugin-sdk';

export default registerDataSource({
  // Renders the connection form fields
  configFields: [
    { key: 'apiEndpoint', label: 'API Endpoint URL', type: 'url', required: true },
    { key: 'apiToken', label: 'API Token', type: 'password', required: true },
    { key: 'tableName', label: 'Resource Name', type: 'text', required: true },
  ],

  // Validates the connection; throw to indicate failure
  async testConnection(config) {
    const response = await fetch(config.apiEndpoint + '/health', {
      headers: { Authorization: `Bearer ${config.apiToken}` },
    });
    if (!response.ok) throw new Error(`Connection test failed: ${response.statusText}`);
  },

  // Returns the schema (columns) of the data source
  async getSchema(config) {
    const response = await fetch(`${config.apiEndpoint}/schema/${config.tableName}`, {
      headers: { Authorization: `Bearer ${config.apiToken}` },
    });
    const schema = await response.json();
    return schema.fields.map((f: { name: string; type: string }) => ({
      name: f.name,
      type: f.type,
    }));
  },

  // Fetches data; params contains filters, limit, offset
  async fetchData(config, params) {
    const url = new URL(`${config.apiEndpoint}/data/${config.tableName}`);
    if (params.limit) url.searchParams.set('limit', String(params.limit));
    if (params.offset) url.searchParams.set('offset', String(params.offset));
    const response = await fetch(url.toString(), {
      headers: { Authorization: `Bearer ${config.apiToken}` },
    });
    return response.json();
  },
});

Plugin Permissions

Plugins must declare every permission they need in the manifest. Users are shown these permissions during installation. Attempting to use an undeclared permission throws a PermissionDeniedError.

PermissionGrants Access To
read:query_resultsRead query results for datasets the user can access
write:dashboardAdd or update widgets on dashboards
read:reportsRead report definitions and generated outputs
write:reportsCreate and modify reports
read:datasetsRead dataset metadata (not raw data)
write:datasetsCreate and update datasets
networkMake outbound HTTP requests to domains listed in allowedDomains
storagePersist key-value data in the plugin storage namespace

Warning: Plugins requesting the network permission must also declare an allowedDomains array in the manifest. Requests to domains not in this list are blocked at the network layer.

Publishing to the Marketplace

  1. Create a developer account at app.openanalyst.com and enroll in the Developer Program from Settings.
  2. Build and test your plugin locally by installing it as a private plugin in a test workspace.
  3. Run the plugin validator to check manifest completeness and permission declarations:
    npx @openanalyst/plugin-cli validate ./radar-chart-v1.2.0.zip
  4. Submit the plugin for review through the Developer Portal. The review process typically takes 3-5 business days.
  5. Once approved, your plugin appears in the public marketplace. You can push updates by submitting a new ZIP with an incremented version in the manifest.