Plugins
Overview
Plugins are lightweight, versioned QuickJS programs that perform data transformation without model calls. They complement agents --- where agents call models to reason, plugins deterministically reshape, check, and score data in the processing pipeline.
The philosophy: write plugins in the Workbench, execute them in the platform. The Workbench (the authoring UI) handles writing, bundling, and testing. The platform only runs pre-validated code.
| Property | Plugin | Agent |
|---|---|---|
| Calls a model? | No | Yes |
| Execution | QuickJS sandbox | LLM inference |
| Purpose | Transform / check / score data | Reason and synthesize |
| Determinism | Deterministic | Probabilistic |
Plugin Types
There are three plugin types, each with a fixed input/output contract:
| Type | Purpose | Contract |
|---|---|---|
| Extractor | Transform unstructured → structured | extract(input) → { data, confidence } |
| Validator | Check data against rules | validate(input) → { valid, errors } |
| Evaluator | Score content quality | evaluate(input) → { score, passed, feedback } |
Extractor Contract
An extractor takes structured data plus configuration and returns extracted data with a confidence score (0.0--1.0).
// Input: any structured data + configuration
// Output: extracted data with confidence score
function extract(input) {
const { data, config } = input;
// Process data...
return {
data: { /* extracted fields */ },
confidence: 0.95 // 0.0-1.0, how confident in extraction
};
}
Validator Contract
A validator checks data against rules and returns a pass/fail flag with a list of error strings.
// Input: data to validate + rules
// Output: pass/fail with error list
function validate(input) {
const { data, rules } = input;
const errors = [];
// Check rules...
if (!data.required_field) {
errors.push("Missing required field");
}
return {
valid: errors.length === 0,
errors: errors // String array
};
}
Evaluator Contract
An evaluator scores content against criteria and returns a score, a pass/fail decision against a threshold, and human-readable feedback.
// Input: content to evaluate + criteria
// Output: score, pass/fail, feedback
function evaluate(input) {
const { content, criteria, threshold = 0.7 } = input;
// Score content...
const score = calculateScore(content, criteria);
return {
score: score, // 0.0-1.0
passed: score >= threshold,
feedback: "Content meets 85% of criteria"
};
}
Unified Schema
All three plugin types share a single plugins table with a plugin_type discriminator column, rather than separate tables per type.
CREATE TYPE plugin_type AS ENUM ('extractor', 'validator', 'evaluator');
CREATE TABLE plugins (
id UUID PRIMARY KEY,
tenant_id UUID NOT NULL REFERENCES tenants(id),
-- Identity (versioned by name within type)
plugin_type plugin_type NOT NULL,
name TEXT NOT NULL,
version INTEGER NOT NULL DEFAULT 1,
description TEXT,
-- Code storage
code TEXT NOT NULL, -- Source JavaScript
bundled_code TEXT, -- esbuild output (optional)
-- NPM dependencies (for Workbench bundling)
dependencies JSONB DEFAULT '{}',
-- Schema definitions
input_schema JSONB, -- JSON Schema for input
output_schema JSONB, -- JSON Schema for output
-- Metadata
is_active BOOLEAN DEFAULT true,
is_global BOOLEAN DEFAULT false,
UNIQUE(tenant_id, plugin_type, name, version)
);
Why a Single Table
A single table with a discriminator wins over separate extractors / validators / evaluators tables because the three types are structurally identical:
- Same storage needs --- code, bundled_code, dependencies, input/output schemas
- Same execution model --- all run in QuickJS
- Same API patterns --- CRUD plus execute
- Extensible --- adding a new plugin type later is trivial; no new table, no new migration shape
The is_global flag and the (tenant_id, plugin_type, name, version) uniqueness constraint do the rest of the work that separate tables would otherwise require.
Execution Model
Plugins run in isolated QuickJS contexts. Each execution gets a fresh, fully isolated context --- no shared state, no escape hatches.
Sandbox Limits
| Limit | Value |
|---|---|
| Memory | 4MB (lighter than renderers) |
| Stack | 256KB |
| Filesystem access | None |
| Network access | None |
| Module imports | None (use bundled_code for dependencies) |
Because there is no module system in the sandbox, NPM dependencies must be pre-bundled into self-contained JavaScript (bundled_code) by the Workbench before publishing.
Execution Functions
#![allow(unused)] fn main() { // Execution functions (source code) execute_extractor(code, input, tenant_id) → ExtractResult execute_validator(code, input, tenant_id) → ValidateResult execute_evaluator(code, input, tenant_id) → EvaluateResult // Bundled variants (esbuild output) execute_bundled_extractor(bundled_code, input, tenant_id) execute_bundled_validator(bundled_code, input, tenant_id) execute_bundled_evaluator(bundled_code, input, tenant_id) }
Built-in Plugins
A set of global plugins is seeded in migration and available to all tenants out of the box:
| Type | Plugin | Purpose |
|---|---|---|
| Extractor | json-path | Extract fields using dot-notation paths |
| Extractor | regex-extract | Extract data using regex patterns |
| Validator | required-fields | Check required fields are present |
| Validator | type-check | Validate field types |
| Validator | range-check | Validate numeric ranges |
| Evaluator | completeness | Score based on filled vs. expected fields |
| Evaluator | length-check | Evaluate content length |
| Evaluator | keyword-coverage | Check presence of expected keywords |
Global vs. Tenant Plugins
Plugins are either global (available to every tenant) or tenant-specific.
| Attribute | Global | Tenant-specific |
|---|---|---|
| Created by | Admins only | Any authorized user |
| Visible to | All tenants | Single tenant |
| Stored with | Global tenant UUID | User's tenant UUID |
| Modify / Delete | Admins only | Tenant users |
Precedence: when a tenant-specific plugin and a global plugin share the same name, the tenant-specific plugin wins. This lets a tenant override a built-in with its own implementation without affecting other tenants.
Versioning
Plugins are versioned by the tuple (tenant_id, plugin_type, name, version):
- Creating a plugin with an existing name bumps the version rather than overwriting
get_plugin_by_namereturns the latest active version- Old versions remain in place for audit and rollback
Error Handling
Plugin code returns structured results, not exceptions. A plugin should never rely on throwing to signal failure --- it should return a result that conveys the failure.
// BAD: throws and crashes
function extract(input) {
throw new Error("Failed");
}
// GOOD: returns error in result (handled by wrapper)
function extract(input) {
if (!input.data) {
return { data: null, confidence: 0 };
}
// ...
}
The Rust execution wrapper still catches any JavaScript exceptions that do escape and converts them into typed Rust errors --- but the contract is to fail gracefully through the return value.
REST Endpoints
| Method | Path | Description |
|---|---|---|
GET | /v1/plugins | List all plugins |
GET | /v1/plugins/type/:type | List plugins by type |
GET | /v1/plugins/:type/:name | Get a specific plugin |
POST | /v1/plugins | Create a plugin |
POST | /v1/plugins/:type/:name/execute | Execute a plugin |
PATCH | /v1/plugins/:type/:name/bundle | Update bundled code |
DELETE | /v1/plugins/:type/:name | Deactivate (soft delete) |
Workbench Integration
The Workbench (authoring UI) owns the full plugin lifecycle up to publishing:
- Plugin authoring with syntax highlighting
- NPM dependency resolution
- esbuild bundling into self-contained JavaScript
- Testing against sample inputs
- Publishing to the platform via
POST /v1/plugins
The platform executes only pre-validated, pre-bundled code --- keeping the runtime simple, sandboxed, and fast.
Implementation
| File | Purpose |
|---|---|
migrations/009_plugins.sql | Database schema + seeded built-in plugins |
crates/qadra-traits/src/lib.rs | Plugin types, PluginRepository trait |
crates/qadra-db-postgres/src/lib.rs | PluginRepository implementation |
src/runtime/plugin.rs | QuickJS execution functions |
src/api/routes/v1/plugins.rs | REST API handlers |