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:
- Automated Grading - AI-powered evaluation of student submissions
- Parent Communication - AI-generated progress reports and updates
- Learning Paths - Personalized learning recommendations
- 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
| Feature | Community | Team | Enterprise |
|---|---|---|---|
| AI Grading | 10/month | 100/month | Unlimited |
| Parent Communication | 3/month | 50/month | Unlimited |
| Learning Paths | 1/month | 20/month | Unlimited |
| Assignment Creation | ❌ | 20/month | Unlimited |
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
- Be specific - Clearly define the task and expected output format
- Use examples - Provide few-shot examples when possible
- Set constraints - Define length, format, and tone requirements
- Handle edge cases - Include instructions for unusual inputs
- Parse reliably - Request structured output (JSON) for easy parsing
Error Handling
Common Error Codes
| Error Code | Description | Resolution |
|---|---|---|
| 403 | Feature not available | Upgrade subscription tier |
| 429 | Usage limit exceeded | Wait for next billing cycle or upgrade |
| 500 | AI service error | Retry 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:
- Email: support@stemblock.ai
- Documentation: https://docs.stemblock.ai
- GitHub Issues: https://github.com/stemblockai/stemblockai-backend/issues