Skip to main content

STEMBlock AI Plugin Development Guide

What is a Plugin?

A plugin is a self-contained feature module that registers itself with the STEMBlock AI platform. Each plugin consists of:

  • Plugin Manifest — Metadata (name, version, icon, category, UI extension points)
  • Evaluation Domain (optional) — AI evaluation providers with a factory pattern
  • UI Extension Points — Navigation links injected into the sidebar per role
  • Organization Installation — Per-org install/uninstall via the admin UI or API

Architecture Overview

PluginsCoreModule (Global)
├── EvaluationRegistry — tracks evaluation domains
├── PipelineExecutor — runs pipeline-based evaluations
├── PluginManifestRegistry — tracks plugin manifests
└── PluginsService — install/uninstall/query per org

Each plugin module (e.g., EnglishWritingModule):
├── Registers its manifest in onModuleInit
├── Registers its evaluation domain in onModuleInit
├── Contains its own providers, services, controllers
└── Extends BaseProviderFactory for AI provider selection

Creating a New Plugin

Step 1: Create the Manifest

Create a manifest file in your module directory:

// src/my-plugin/my-plugin.manifest.ts
import type { PluginManifest } from '../plugins/manifest/plugin-manifest.types';

export const MY_PLUGIN_MANIFEST: PluginManifest = {
pluginId: 'math-problem-solving', // Unique identifier
version: '1.0.0',
displayName: 'Math Problem Solving',
description: 'AI-powered math evaluation and step-by-step feedback',
author: 'STEMBlock AI',
icon: 'CalculatorIcon', // Heroicon name
category: 'Mathematics',

evaluationDomain: {
domainId: 'math-problem-solving',
executionModel: 'pipeline', // or 'capabilities'
scoreDimensions: ['accuracy', 'methodology', 'explanation'],
},

featureFlag: 'MATH_EVALUATION', // Maps to FeatureFlag enum

ui: [
{
slot: 'sidebar-nav',
route: '/student/math',
label: 'Math',
icon: 'CalculatorIcon',
roles: ['STUDENT'],
},
{
slot: 'sidebar-nav',
route: '/coach/math',
label: 'Math',
icon: 'CalculatorIcon',
roles: ['COACH'],
},
],

requiredEnvVars: ['MATH_EVALUATOR_PROVIDER'],
};

Step 2: Create Your Evaluation Provider (if applicable)

Choose between Pipeline or Capabilities execution model:

Pipeline — for ordered, sequential stages (e.g., moderation -> feedback -> scoring):

import { PipelineEvaluationProvider } from '../plugins/evaluation';

export abstract class MathEvaluatorProvider extends PipelineEvaluationProvider<MathContext> {
abstract getStageDefinitions(): PipelineStageDefinition[];
abstract executeStage(stageId: string, context: MathContext): Promise<StageResult>;
}

Capabilities — for independent operations:

import { CapabilitiesEvaluationProvider } from '../plugins/evaluation';

export abstract class MathEvaluatorProvider extends CapabilitiesEvaluationProvider {
abstract getCapabilities(): CapabilityDefinition[];
abstract executeCapability(capabilityId: string, request: unknown): Promise<unknown>;
}

Step 3: Create Your Provider Factory

Extend BaseProviderFactory to get env-var-driven provider selection with mock fallback:

import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { BaseProviderFactory } from '../plugins/evaluation/base-provider.factory';

@Injectable()
export class MathEvaluatorFactory extends BaseProviderFactory<MathEvaluatorProvider> {
constructor(
configService: ConfigService,
private readonly geminiProvider: GeminiMathProvider,
private readonly mockProvider: MockMathProvider,
) {
super(configService);
}

protected getEnvVarName(): string { return 'MATH_EVALUATOR_PROVIDER'; }
protected getDefaultType(): string { return 'mock'; }

protected getProviderMap(): Map<string, MathEvaluatorProvider> {
return new Map([
['gemini', this.geminiProvider],
]);
}

protected getMockProvider(): MathEvaluatorProvider {
return this.mockProvider;
}
}

Step 4: Create Your NestJS Module

Register with the plugin framework in onModuleInit:

import { Module, type OnModuleInit } from '@nestjs/common';
import { EvaluationRegistry } from '../plugins/evaluation/evaluation-registry.service';
import { PluginManifestRegistry } from '../plugins/manifest/plugin-manifest.registry';
import { MY_PLUGIN_MANIFEST } from './my-plugin.manifest';

@Module({
imports: [DatabaseModule],
controllers: [MathController],
providers: [
MathService,
MathEvaluatorFactory,
GeminiMathProvider,
MockMathProvider,
],
exports: [MathService],
})
export class MathModule implements OnModuleInit {
constructor(
private readonly evaluationRegistry: EvaluationRegistry,
private readonly manifestRegistry: PluginManifestRegistry,
) {}

onModuleInit() {
this.evaluationRegistry.register({
domainId: 'math-problem-solving',
name: 'Math Problem Solving',
factoryToken: 'MathEvaluatorFactory',
executionModel: 'pipeline',
});
this.manifestRegistry.registerManifest(MY_PLUGIN_MANIFEST);
}
}

Step 5: Add to AppModule

// src/app.module.ts
imports: [
// ... existing modules
MathModule, // Add your new module
]

Step 6: Add FeatureFlag (if needed)

Update prisma/schema.prisma:

enum FeatureFlag {
// ... existing flags
MATH_EVALUATION // Add new flag
}

Run: npx prisma migrate dev --name add_math_feature_flag

Plugin Manifest Reference

FieldTypeRequiredDescription
pluginIdstringYesUnique identifier (kebab-case)
versionstringYesSemantic version
displayNamestringYesHuman-readable name
descriptionstringYesShort description
authorstringYesAuthor name
iconstringYesHeroicon component name
categorystringYesPlugin category
evaluationDomainobjectNoAI evaluation config
featureFlagstringYesMaps to FeatureFlag enum
uiPluginUIExtensionPoint[]YesFrontend extension points
requiredEnvVarsstring[]NoRequired environment variables

UI Extension Point

FieldTypeDescription
slot'sidebar-nav'Where to render (currently only sidebar)
routestringNext.js route path
labelstringDisplay text
iconstringHeroicon component name
rolesstring[]Which roles see this link

Evaluation Provider Types

Pipeline Providers

Best for: Sequential evaluation stages where order matters and earlier stages can short-circuit.

Example: English Writing uses Moderation -> Feedback -> Assessment.

Key methods:

  • getStageDefinitions() — returns ordered list of stages
  • executeStage(stageId, context) — runs a single stage, returns StageResult

The PipelineExecutor service handles:

  • Sorting stages by order
  • Running stages sequentially
  • Short-circuit logic (e.g., stop if moderation flags content)
  • Error handling for required stages

Capabilities Providers

Best for: Independent operations that don't depend on each other.

Example: STEM Evaluation exposes evaluateSubmission, generateCoachFeedback, generateParentInsights.

Key methods:

  • getCapabilities() — returns list of available operations
  • executeCapability(capabilityId, request) — runs a specific operation

Installing a Plugin

API Endpoints

GET  /api/v1/plugins/available   — List all registered plugins (ADMIN, ORG_ADMIN)
GET /api/v1/plugins/installed — List installed plugins for current org + role
POST /api/v1/plugins/install — Install: { pluginId: string }
DELETE /api/v1/plugins/:pluginId — Uninstall plugin

Admin UI

Navigate to Settings > App Store to see all available apps. Each card shows:

  • App name, description, version, category, author
  • Install/Uninstall button
  • "Installed" badge for active apps

Seed Data

When running npx prisma db seed, both english-writing and ai-evaluation (formerly stem-evaluation) apps are auto-installed for all organizations.

Frontend Extension Points

The PluginsProvider context fetches installed plugins for the current user and provides:

const { plugins, navLinks, loading } = usePlugins();
  • plugins — all installed apps with UI extension points filtered by user role
  • navLinks — flattened list of sidebar nav links from all installed apps
  • loading — whether the apps query is still loading

Navigation.tsx renders navLinks dynamically for each role section, replacing hardcoded plugin-specific links.

To add a new icon for your app to the App Store page, update the ICON_MAP in app/settings/plugins/page.tsx.

Example: Math Problem Solving Plugin

A complete example plugin is included in the codebase at src/workflows/math-problem-solving/. This demonstrates the full plugin pattern with:

Backend Structure

src/workflows/math-problem-solving/
├── providers/
│ ├── math-evaluator.interface.ts # Abstract provider (3-stage: Validate → Feedback → Score)
│ ├── mock-math.provider.ts # Mock provider for testing
│ ├── math-evaluator.factory.ts # Extends BaseProviderFactory
│ └── index.ts
├── dto/
│ └── submit-math.dto.ts # Input validation
├── math-problem-solving.manifest.ts # Plugin manifest
├── math-problem-solving.service.ts # Business logic
├── math-problem-solving.controller.ts # API endpoints
└── math-problem-solving.module.ts # NestJS module with OnModuleInit

Frontend Pages

app/student/math/page.tsx   # Student view — select problem, submit answer, view AI feedback
app/coach/math/page.tsx # Coach view — placeholder for reviewing student submissions

API Endpoints

GET  /api/v1/math/demo-problems  — List demo math problems
POST /api/v1/math/evaluate — Evaluate a math submission

Demo: Install Flow

The Math plugin is registered but NOT installed by default. This makes it perfect for demoing the install/uninstall flow:

  1. Start the backend — the Math plugin registers its manifest on startup (check logs for "Registered plugin: Math Problem Solving")
  2. Log in as ORG_ADMIN — navigate to Settings > App Store (/settings/plugins)
  3. See all 3 apps — English Writing and AI Evaluation show "Installed", Math shows Install button
  4. Click Install on Math Problem Solving
  5. Navigate as STUDENT — the "Math" link now appears in the sidebar (dynamic from API)
  6. Go to /student/math — select a demo problem, type your answer, click "Submit & Evaluate"
  7. View AI feedback — validation, scoring (accuracy/methodology/explanation/completeness), and detailed feedback
  8. Uninstall — go back to Settings > App Store, click Uninstall on Math. The nav link disappears

This demonstrates the complete lifecycle: registration → discovery → installation → dynamic UI → usage → uninstallation.

Developer SDK

For a streamlined plugin development experience, use the STEMBlock AI Plugin SDK (@stemblockai/plugin-sdk). The SDK repo is at ../stemblockai-plugin-sdk/.

SDK Features

  • CLI Tool (stemblockai-plugin) — Scaffold, validate, bundle, and test plugins
  • Project Templatesevaluation-plugin (with AI pipeline) and basic-plugin (pages only)
  • Manifest Validator — Validates all fields, formats, and constraints before submission
  • Plugin Bundler — Packages plugins into .stemblock distribution format
  • Sandbox Testing — Test plugins in an isolated mock NestJS runtime

Quick Start

# Install the SDK
npm install -g @stemblockai/plugin-sdk

# Create a new plugin
stemblockai-plugin create "Science Quiz" --template evaluation-plugin

# Validate, test, and bundle
cd science-quiz
stemblockai-plugin validate
stemblockai-plugin sandbox --verbose
npm run bundle

Package Format

The .stemblock format is a gzipped JSON archive containing the manifest, compiled backend module, frontend pages, and assets. See the SDK documentation at ../stemblockai-plugin-sdk/docs/ for the full specification: