Skip to main content

STEMBlock.ai Workflow Integration Guide

This document explains how to integrate custom AI workflows into the STEMBlock.ai platform.

Overview

STEMBlock.ai provides four core AI workflow services:

  1. Automated Grading - AI-powered evaluation of student submissions
  2. Parent Communication - AI-generated progress reports and updates
  3. Learning Paths - Personalized learning recommendations
  4. Assignment Creation - AI-assisted assignment generation

Architecture

┌─────────────────────────────────────────────────────────────────┐
│ Frontend (Next.js) │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Coach │ │ Student │ │ Parent │ │
│ │ Dashboard │ │ Dashboard │ │ Dashboard │ │
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
└─────────┼──────────────────┼──────────────────┼─────────────────┘
│ │ │
▼ ▼ ▼
┌─────────────────────────────────────────────────────────────────┐
│ API Gateway (NestJS) │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Subscription Guard │ │
│ │ (Validates tier and usage limits) │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │ │
│ ┌───────────┬───────────┬───────────┬───────────┐ │
│ │ Grading │ Comms │ Learning │ Assignment│ │
│ │ Workflow │ Workflow │ Paths │ Creation │ │
│ └─────┬─────┴─────┬─────┴─────┬─────┴─────┬─────┘ │
└────────┼───────────┼───────────┼───────────┼────────────────────┘
│ │ │ │
▼ ▼ ▼ ▼
┌─────────────────────────────────────────────────────────────────┐
│ AI Provider (OpenAI) │
└─────────────────────────────────────────────────────────────────┘

Creating a Custom Workflow

Step 1: Define the Workflow Module

Create a new module in src/workflows/:

// src/workflows/my-workflow/my-workflow.module.ts
import { Module } from '@nestjs/common';
import { MyWorkflowService } from './my-workflow.service';
import { MyWorkflowController } from './my-workflow.controller';
import { DatabaseModule } from '../../database/database.module';
import { SubscriptionsModule } from '../../subscriptions/subscriptions.module';

@Module({
imports: [DatabaseModule, SubscriptionsModule],
controllers: [MyWorkflowController],
providers: [MyWorkflowService],
exports: [MyWorkflowService],
})
export class MyWorkflowModule {}

Step 2: Create the Workflow Service

// src/workflows/my-workflow/my-workflow.service.ts
import { Injectable, ForbiddenException, Logger } from '@nestjs/common';
import { PrismaService } from '../../database/prisma.service';
import { SubscriptionsService } from '../../subscriptions/subscriptions.service';
import { FeatureFlag } from '@prisma/client';

@Injectable()
export class MyWorkflowService {
private readonly logger = new Logger(MyWorkflowService.name);

constructor(
private prisma: PrismaService,
private subscriptionsService: SubscriptionsService,
) {}

async executeWorkflow(userId: string, data: any): Promise<any> {
// 1. Check feature access based on subscription tier
const hasAccess = await this.subscriptionsService.hasFeatureAccess(
userId,
FeatureFlag.MY_WORKFLOW, // Add to FeatureFlag enum in Prisma schema
);

if (!hasAccess) {
throw new ForbiddenException(
'This feature is not available on your current plan.',
);
}

// 2. Check usage limits
const usageCheck = await this.subscriptionsService.checkUsageLimit(
userId,
FeatureFlag.MY_WORKFLOW,
);

if (!usageCheck.allowed) {
throw new ForbiddenException(
`You have reached your monthly limit. Upgrade for more capacity.`,
);
}

// 3. Execute your workflow logic
const result = await this.performWorkflow(data);

// 4. Record usage
await this.subscriptionsService.recordUsage(
userId,
FeatureFlag.MY_WORKFLOW,
);

return result;
}

private async performWorkflow(data: any): Promise<any> {
// Your workflow logic here
const prompt = this.buildPrompt(data);
const aiResponse = await this.callAI(prompt);
return this.parseResponse(aiResponse);
}

private buildPrompt(data: any): string {
return `Your prompt template here with ${data.field}`;
}

private async callAI(prompt: string): Promise<string> {
// TODO: Integrate with OpenAI or other AI provider
this.logger.debug('AI Prompt', { promptLength: prompt.length });

// Example OpenAI integration:
// const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
// const response = await openai.chat.completions.create({
// model: 'gpt-4',
// messages: [{ role: 'user', content: prompt }],
// });
// return response.choices[0].message.content;

return 'AI response placeholder';
}

private parseResponse(response: string): any {
// Parse and structure the AI response
return { result: response };
}
}

Step 3: Create the Controller

// src/workflows/my-workflow/my-workflow.controller.ts
import { Controller, Post, Body, UseGuards } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
import { JwtAuthGuard } from '../../auth/guards/jwt-auth.guard';
import { GetUser } from '../../auth/decorators/get-user.decorator';
import { MyWorkflowService } from './my-workflow.service';
import { ExecuteWorkflowDto } from './dto/execute-workflow.dto';

@ApiTags('My Workflow')
@Controller('api/v1/workflows/my-workflow')
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
export class MyWorkflowController {
constructor(private readonly myWorkflowService: MyWorkflowService) {}

@Post('execute')
@ApiOperation({ summary: 'Execute custom workflow' })
async executeWorkflow(
@GetUser() user: any,
@Body() dto: ExecuteWorkflowDto,
) {
const result = await this.myWorkflowService.executeWorkflow(
user.id,
dto,
);

return {
data: result,
meta: {
timestamp: new Date().toISOString(),
},
};
}
}

Step 4: Create DTOs

// src/workflows/my-workflow/dto/execute-workflow.dto.ts
import { IsString, IsOptional } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';

export class ExecuteWorkflowDto {
@ApiProperty({ description: 'Input data for the workflow' })
@IsString()
input: string;

@ApiProperty({ required: false })
@IsOptional()
@IsString()
options?: string;
}

Step 5: Register in App Module

// src/app.module.ts
import { MyWorkflowModule } from './workflows/my-workflow/my-workflow.module';

@Module({
imports: [
// ... other modules
MyWorkflowModule,
],
})
export class AppModule {}

Step 6: Add Feature Flag

Update the Prisma schema to include your new feature flag:

// prisma/schema.prisma
enum FeatureFlag {
AI_GRADING
PARENT_COMMUNICATION
LEARNING_PATHS
ASSIGNMENT_CREATION
MY_WORKFLOW // Add your new feature
// ...
}

Then seed the feature limits:

// prisma/seed.ts
const featureLimits = [
// Community tier
{ tier: 'COMMUNITY', feature: 'MY_WORKFLOW', monthlyLimit: 5, enabled: true },
// Team tier
{ tier: 'TEAM', feature: 'MY_WORKFLOW', monthlyLimit: 50, enabled: true },
// Enterprise tier
{ tier: 'ENTERPRISE', feature: 'MY_WORKFLOW', monthlyLimit: null, enabled: true },
];

Subscription Integration

Tier-Based Access Control

All workflows should check the user's subscription tier before executing:

// Check if feature is enabled for user's tier
const hasAccess = await this.subscriptionsService.hasFeatureAccess(
userId,
FeatureFlag.MY_WORKFLOW,
);

// Check if user has remaining quota
const usageCheck = await this.subscriptionsService.checkUsageLimit(
userId,
FeatureFlag.MY_WORKFLOW,
);

Feature Limits by Tier

FeatureCommunityTeamEnterprise
AI Grading10/month100/monthUnlimited
Parent Communication3/month50/monthUnlimited
Learning Paths1/month20/monthUnlimited
Assignment Creation20/monthUnlimited

Frontend Integration

Using the Workflow API

// lib/api.ts
export const myWorkflowAPI = {
execute: async (data: ExecuteWorkflowDto): Promise<any> => {
const response = await api.post('/api/v1/workflows/my-workflow/execute', data);
return response.data;
},
};

React Component Example

'use client';

import { useState } from 'react';
import { myWorkflowAPI } from '@/lib/api';
import { Button, Card } from '@/components/ui';

export function MyWorkflowComponent() {
const [loading, setLoading] = useState(false);
const [result, setResult] = useState(null);
const [error, setError] = useState(null);

const handleExecute = async () => {
try {
setLoading(true);
setError(null);
const response = await myWorkflowAPI.execute({ input: 'my data' });
setResult(response.data);
} catch (err: any) {
if (err.response?.status === 403) {
setError('Upgrade your plan to use this feature');
} else {
setError('An error occurred');
}
} finally {
setLoading(false);
}
};

return (
<Card>
<Button onClick={handleExecute} disabled={loading}>
{loading ? 'Processing...' : 'Execute Workflow'}
</Button>
{error && <p className="text-red-500">{error}</p>}
{result && <pre>{JSON.stringify(result, null, 2)}</pre>}
</Card>
);
}

AI Provider Configuration

OpenAI Integration

Set your OpenAI API key in the environment:

# .env
OPENAI_API_KEY=sk-your-api-key-here

Example usage in workflow:

import OpenAI from 'openai';

private async callOpenAI(prompt: string): Promise<string> {
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
});

const completion = await openai.chat.completions.create({
model: 'gpt-4',
messages: [
{
role: 'system',
content: 'You are an educational AI assistant.',
},
{
role: 'user',
content: prompt,
},
],
temperature: 0.7,
max_tokens: 2000,
});

return completion.choices[0].message.content || '';
}

Prompt Engineering Best Practices

  1. Be specific - Clearly define the task and expected output format
  2. Use examples - Provide few-shot examples when possible
  3. Set constraints - Define length, format, and tone requirements
  4. Handle edge cases - Include instructions for unusual inputs
  5. Parse reliably - Request structured output (JSON) for easy parsing

Error Handling

Common Error Codes

Error CodeDescriptionResolution
403Feature not availableUpgrade subscription tier
429Usage limit exceededWait for next billing cycle or upgrade
500AI service errorRetry or contact support

Graceful Degradation

try {
const result = await this.callAI(prompt);
return this.parseResponse(result);
} catch (error) {
this.logger.error('AI service failed', error);

// Provide fallback or queue for retry
return this.fallbackResponse(data);
}

Testing Workflows

Unit Tests

describe('MyWorkflowService', () => {
let service: MyWorkflowService;
let subscriptionsService: MockSubscriptionsService;

beforeEach(async () => {
const module = await Test.createTestingModule({
providers: [
MyWorkflowService,
{ provide: SubscriptionsService, useClass: MockSubscriptionsService },
{ provide: PrismaService, useClass: MockPrismaService },
],
}).compile();

service = module.get(MyWorkflowService);
});

it('should check subscription before executing', async () => {
await expect(service.executeWorkflow('user-id', { input: 'test' }))
.resolves.toBeDefined();
expect(subscriptionsService.hasFeatureAccess).toHaveBeenCalled();
});

it('should throw if feature not available', async () => {
subscriptionsService.hasFeatureAccess.mockResolvedValue(false);
await expect(service.executeWorkflow('user-id', { input: 'test' }))
.rejects.toThrow(ForbiddenException);
});
});

Monitoring & Analytics

Track workflow usage with:

// Record workflow execution
await this.prisma.workflowExecution.create({
data: {
userId,
workflowType: 'MY_WORKFLOW',
inputSize: JSON.stringify(data).length,
executionTimeMs: endTime - startTime,
success: true,
},
});

Support

For questions about workflow integration: