Lean Windows Task Scheduler Migration Plan
For ERP Crystal MFG
Date: February 2, 2026
Author: Roo (Architect Mode)
Based On: Leaner-Low-Risk-Plan.md instructions
π― Core Principles (As Per Instructions)
- Do NOT redesign your scheduler framework
- Do NOT centralize into new orchestration services (yet)
- Replace Coravel’s trigger mechanism with HTTP triggers
- Keep existing job logic almost unchanged
ποΈ Current Architecture Analysis
Existing Coravel-Based Scheduler
Coravel Engine β TaskSchedulerInitializer β IInvocable Schedulers β Business Logic7 Scheduled Task Types
- 001 - Indent Email Scheduler
- 002 - Invoice Email Scheduler
- 003 - Sales Order Email Scheduler
- 004 - AR Email Scheduler
- 005 - Voucher Payment Email Scheduler
- 006 - Voucher Receipt Email Scheduler
- 007 - AI Insights Scheduler
Key Components
- TaskSchedulerInitializer.cs: Dynamically schedules tasks based on database configuration
- 7 Scheduler Classes: Each implements
IInvocableinterface withInvoke()method - Program.cs: Configures Coravel with
AddScheduler(),AddQueue(), andUseScheduler()
π§© New Architecture (Minimal Change)
AFTER Migration
Windows Task Scheduler β HTTPS + API Key β Thin API Endpoints β Existing Scheduler Logic (renamed) β Business LogicWhat Stays the Same β
- Multi-database loops (
foreach (var dbname in dbnamelist)) - Logging system (
LogMessage()calls) - Email/Report generation
- Error handling patterns
- Business repositories (DI injection)
- AI Insights flow
- Database configuration (
TaskSchedulerConfigmodel)
What Goes Away β
- Coravel packages (
Coravel,Coravel.Scheduling,Coravel.Invocable) IInvocableinterfaceTaskSchedulerInitializer.cs(scheduling logic moves to Windows Task Scheduler)- CRON-based runtime scheduling (moves to Windows Task Scheduler triggers)
- Coravel-specific code in
Program.cs
What Gets Added β (Very Small)
- One controller with endpoints (
ScheduledJobsController.cs) - Minor refactor of scheduler classes β job classes
- Windows Task Scheduler jobs (7 tasks)
- Simple API key authentication (already exists)
π Target Structure (Simple)
ErpCrystal_MFG.Api/
ββ Controllers/
β ββ ScheduledJobsController.cs β NEW
ββ Jobs/ β NEW FOLDER
β ββ IndentEmailJob.cs β Renamed from IndentEmailScheduler.cs
β ββ InvoiceEmailJob.cs β Renamed from InvoiceEmailScheduler.cs
β ββ SalesOrderEmailJob.cs β Renamed from SalesOrderEmailScheduler.cs
β ββ ArEmailJob.cs β Renamed from ArEmailScheduler.cs
β ββ VoucherPaymentEmailJob.cs β Renamed from VoucherPaymentEmailScheduler.cs
β ββ VoucherReceiptEmailJob.cs β Renamed from VoucherReceiptEmailScheduler.cs
β ββ AIInsightsJob.cs β Renamed from AIInsightsScheduler.cs
ββ TaskScheduler/ β DELETE (or keep for reference during migration)π Refactor Pattern (Key Concept)
OLD (Coravel) - IndentEmailScheduler.cs
using Coravel.Invocable;
// ... other using statements
public class IndentEmailScheduler(IEmailRepository iemailrepository,
IUtilityMethodsRepository iutilitymethodsrepository,
IIndentRepository iindentrepository,
IEnumerable<string> dbnamelist) : IInvocable
{
private readonly IEmailRepository _IEmailRepository = iemailrepository;
private readonly IUtilityMethodsRepository _IUtilityMethodsRepository = iutilitymethodsrepository;
private readonly IIndentRepository _IIndentRepository = iindentrepository;
public async Task Invoke()
{
foreach (var dbname in dbnamelist)
{
// Existing logic (setup logging, get instructions, process indents)
// ...
}
}
private void LogMessage(string path, string message)
{
// Existing logging
}
}NEW (Lean) - IndentEmailJob.cs
// REMOVED: using Coravel.Invocable;
// KEPT: All other using statements
public class IndentEmailJob // REMOVED: : IInvocable
{
private readonly IEmailRepository _IEmailRepository;
private readonly IUtilityMethodsRepository _IUtilityMethodsRepository;
private readonly IIndentRepository _IIndentRepository;
private readonly ITaskSchedulerRepository _ITaskSchedulerRepository; // NEW: To fetch db list
public IndentEmailJob(IEmailRepository iemailrepository,
IUtilityMethodsRepository iutilitymethodsrepository,
IIndentRepository iindentrepository,
ITaskSchedulerRepository itaskschedulerrepository) // NEW parameter
{
_IEmailRepository = iemailrepository;
_IUtilityMethodsRepository = iutilitymethodsrepository;
_IIndentRepository = iindentrepository;
_ITaskSchedulerRepository = itaskschedulerrepository;
}
// Method for single database
public async Task Run(string dbname)
{
// SAME logic from inside foreach loop (almost copy-paste)
string pathDirectoryName = $"\\MFGReports\\Docs\\{dbname}\\Logs";
if (!Directory.Exists(pathDirectoryName))
{
Directory.CreateDirectory(pathDirectoryName);
}
var pathName = $"{pathDirectoryName}\\{dbname}.csv";
var logFilePath = Path.GetFullPath(pathName);
if (!File.Exists(logFilePath))
{
LogMessage(logFilePath, $"Activity,DateTime,Status,Notes");
}
// Continue with existing logic...
}
// Method for all databases (like old Invoke())
public async Task RunAll()
{
var dbnamelist = await _ITaskSchedulerRepository.GetDbNameList("001");
foreach (var dbname in dbnamelist)
{
await Run(dbname);
}
}
private void LogMessage(string path, string message)
{
// Keep existing logging unchanged
}
}π API Trigger (Thin Layer)
ScheduledJobsController.cs
using Microsoft.AspNetCore.Mvc;
using ErpCrystal_MFG.Api.Jobs; // NEW namespace
using ErpCrystal_MFG.Api.CustomAttributes;
[ApiController]
[Route("api/jobs")]
[ApiKeyAuth] // Existing authentication attribute
public class ScheduledJobsController : ControllerBase
{
private readonly IndentEmailJob _indentEmailJob;
private readonly InvoiceEmailJob _invoiceEmailJob;
private readonly SalesOrderEmailJob _salesOrderEmailJob;
private readonly ArEmailJob _arEmailJob;
private readonly VoucherPaymentEmailJob _voucherPaymentEmailJob;
private readonly VoucherReceiptEmailJob _voucherReceiptEmailJob;
private readonly AIInsightsJob _aiInsightsJob;
public ScheduledJobsController(
IndentEmailJob indentEmailJob,
InvoiceEmailJob invoiceEmailJob,
SalesOrderEmailJob salesOrderEmailJob,
ArEmailJob arEmailJob,
VoucherPaymentEmailJob voucherPaymentEmailJob,
VoucherReceiptEmailJob voucherReceiptEmailJob,
AIInsightsJob aiInsightsJob)
{
_indentEmailJob = indentEmailJob;
_invoiceEmailJob = invoiceEmailJob;
_salesOrderEmailJob = salesOrderEmailJob;
_arEmailJob = arEmailJob;
_voucherPaymentEmailJob = voucherPaymentEmailJob;
_voucherReceiptEmailJob = voucherReceiptEmailJob;
_aiInsightsJob = aiInsightsJob;
}
[HttpPost("indent-email")]
public async Task<IActionResult> RunIndentEmails()
{
await _indentEmailJob.RunAll();
return Ok(new { Message = "Indent email job completed" });
}
[HttpPost("indent-email/{dbName}")]
public async Task<IActionResult> RunIndentEmailForDb(string dbName)
{
await _indentEmailJob.Run(dbName);
return Ok(new { Message = $"Indent email job completed for {dbName}" });
}
[HttpPost("invoice-email")]
public async Task<IActionResult> RunInvoiceEmails()
{
await _invoiceEmailJob.RunAll();
return Ok(new { Message = "Invoice email job completed" });
}
[HttpPost("invoice-email/{dbName}")]
public async Task<IActionResult> RunInvoiceEmailForDb(string dbName)
{
await _invoiceEmailJob.Run(dbName);
return Ok(new { Message = $"Invoice email job completed for {dbName}" });
}
// Similar endpoints for all 7 job types...
// Sales Order Email: /api/jobs/sales-order-email
// AR Email: /api/jobs/ar-email
// Voucher Payment Email: /api/jobs/voucher-payment-email
// Voucher Receipt Email: /api/jobs/voucher-receipt-email
// AI Insights: /api/jobs/ai-insights
}βοΈ Program.cs Changes
Current Program.cs (Lines to Modify)
// Line 18: REMOVE
builder.Services.AddScheduler();
// Line 19: REMOVE
builder.Services.AddQueue();
// Line 166: REMOVE or comment out during migration
builder.Services.AddScoped<TaskSchedulerInitializer>();
// Lines 210-215: REMOVE ENTIRE BLOCK
app.Services.UseScheduler(async scheduler =>
{
using var scope = app.Services.CreateScope();
var initializer = scope.ServiceProvider.GetRequiredService<TaskSchedulerInitializer>();
await initializer.InitializeTasks(scheduler);
});Updated Program.cs (Additions)
// Add after other service registrations (around line 166)
builder.Services.AddScoped<IndentEmailJob>();
builder.Services.AddScoped<InvoiceEmailJob>();
builder.Services.AddScoped<SalesOrderEmailJob>();
builder.Services.AddScoped<ArEmailJob>();
builder.Services.AddScoped<VoucherPaymentEmailJob>();
builder.Services.AddScoped<VoucherReceiptEmailJob>();
builder.Services.AddScoped<AIInsightsJob>();β° Windows Task Scheduler Configuration
General Pattern for Each Job
- Action: HTTPS call to API endpoint
- Trigger: Daily/Time-based as per current CRON schedules
- Settings: Run whether user is logged on or not
PowerShell Script Example for Indent Email Job
# Create Windows Task Scheduler Job for Indent Emails (Task Code 001)
$action = New-ScheduledTaskAction -Execute "powershell.exe" `
-Argument "-Command `"Invoke-RestMethod -Uri 'https://your-api.erpcrystal.com/api/jobs/indent-email' -Method POST -Headers @{'X-API-Key' = 'YOUR_API_KEY'} -UseBasicParsing`""
# Daily at 9:00 AM (equivalent to CRON: 0 9 * * *)
$trigger = New-ScheduledTaskTrigger -Daily -At 9:00AM
# Configure task settings
$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable
# Register the task
Register-ScheduledTask -Action $action -Trigger $trigger -Settings $settings `
-TaskName "ERP Crystal - Indent Email Job" -Description "Sends indent emails daily at 9 AM" `
-User "SYSTEM" -RunLevel HighestManual Setup via GUI
- Open Task Scheduler on Win-S IIS Server
- Click Create Task
- General Tab:
- Name:
ERP Crystal - Indent Email Job - Description:
Sends indent emails daily at 9 AM - Run whether user is logged on or not
- Run with highest privileges
- Name:
- Triggers Tab:
- New β Daily β Start: 9:00:00 AM β OK
- Actions Tab:
- New β Start a program
- Program/script:
powershell.exe - Arguments:
-Command "Invoke-RestMethod -Uri 'https://your-api.erpcrystal.com/api/jobs/indent-email' -Method POST -Headers @{'X-API-Key' = 'YOUR_API_KEY'}"
- Conditions Tab: Adjust as needed (default usually OK)
- Settings Tab:
- Allow task to be run on demand
- Run task as soon as possible after a scheduled start is missed
- If the task fails, restart every 1 minute (up to 3 times)
π Security Considerations
Existing Security (Keep As-Is)
- API Key Authentication: Already implemented via
[ApiKeyAuth]attribute - HTTPS: Ensure API endpoints are HTTPS only
- Firewall: Restrict access to API from server IP only
Additional Security (Optional)
- IP Restriction: Configure IIS to allow requests only from the server’s own IP (127.0.0.1) and maybe the Win-S server IP
- Network Isolation: Ensure API is not publicly exposed (internal network only)
π Logging Strategy
Keep Existing Logging
- Continue using
LogMessage()method in job classes - Log files at:
\\MFGReports\\Docs\\{dbname}\\Logs\\{dbname}.csv
Enhanced Logging (Optional Additions)
// Add at start of Run() method
LogMessage(logFilePath, $"Job triggered from Windows Scheduler at {DateTime.Now}");Monitoring
- Windows Task Scheduler history (View β Show All Running Tasks)
- Application Event Viewer logs
- Existing CSV log files for business logic results
π§ͺ Migration Strategy (Safe, Phased Approach)
Phase 1: Development & Testing (1-2 Weeks)
-
Week 1: Create new job classes and controller
- Copy scheduler files to Jobs folder
- Refactor
IInvocableβ regular classes - Add
Run()andRunAll()methods - Create
ScheduledJobsController.cs - Update
Program.csservice registrations
-
Week 2: Test in Development Environment
- Test each endpoint manually via Postman/Swagger
- Verify logging works correctly
- Test individual database execution
- Test all databases execution
- Leave Coravel active during testing (dual mode)
Phase 2: Staging Deployment (3-5 Days)
- Deploy updated API to staging
- Configure Windows Task Scheduler jobs in staging
- Run both systems in parallel (Coravel + Windows Scheduler)
- Compare results for consistency
- Fix any issues discovered
Phase 3: Production Migration (1 Week)
- Day 1: Deploy updated API (keep Coravel code intact but disabled)
- Day 2: Configure Windows Task Scheduler jobs
- Days 3-5: Monitor both systems (Coravel OFF, Windows Scheduler ON)
- Day 6: Verify all jobs ran successfully for 3+ days
- Day 7: Remove Coravel packages and cleanup code
Phase 4: Cleanup & Optimization
- Remove Coravel NuGet packages
- Delete
TaskSchedulerInitializer.cs - Remove any Coravel-specific UI components from Web project
- Update documentation
β οΈ Risk Mitigation
Risk 1: Job Execution Failures
- Mitigation: Windows Task Scheduler has built-in retry mechanisms
- Backup: Keep Coravel code intact during initial migration period
- Monitoring: Check Windows Task Scheduler history daily
Risk 2: Database Connection Issues
- Mitigation: Job classes already handle per-database connections
- Testing: Verify each database works independently via
/{dbName}endpoints
Risk 3: Performance Impact
- Mitigation: Same business logic, just different trigger mechanism
- Monitoring: Track execution times vs Coravel baseline
Risk 4: Security Issues
- Mitigation: Use existing API key auth, HTTPS only
- Testing: Verify unauthorized access is blocked
β Validation Checklist
Before Migration
- All 7 job classes created in
Jobs/folder -
ScheduledJobsController.csimplemented with all endpoints -
Program.csupdated (Coravel services removed, job services added) - API endpoints testable via Swagger/Postman
- Individual database execution works (
/api/jobs/{job}/{dbName}) - All databases execution works (
/api/jobs/{job}) - Logging works as before
During Migration
- Windows Task Scheduler jobs configured for all 7 tasks
- Jobs run successfully in staging environment
- Log output matches Coravel output
- Email delivery verified
- No data corruption or duplicates
After Migration
- All jobs running via Windows Task Scheduler for 3+ days
- Performance comparable to Coravel
- Error handling working correctly
- Coravel packages removed from solution
-
TaskSchedulerInitializer.csdeleted - Documentation updated
π Rollback Plan
Rollback Triggers
(Execute if any of these occur within first 7 days)
- Critical job failures (>50% of jobs failing)
- Data corruption or duplication
- Performance degradation >50%
- Security breach detected
Rollback Procedure
- Stop all Windows Task Scheduler jobs
- Re-enable Coravel in
Program.cs(uncomment lines) - Restart API application
- Verify Coravel jobs resume normally
- Investigate and fix Windows Scheduler issues
- Re-attempt migration after fixes
π Implementation Timeline
| Phase | Duration | Key Activities |
|---|---|---|
| Development | 1-2 weeks | Create job classes, controller, update Program.cs |
| Testing | 3-5 days | Manual testing, endpoint validation |
| Staging | 3-5 days | Parallel run, comparison testing |
| Production | 1 week | Gradual cutover, monitoring |
| Cleanup | 2-3 days | Remove Coravel packages, update docs |
Total Estimated Time: 3-4 weeks (conservative estimate)
π― Success Criteria
Technical Success
- All 7 scheduled tasks functioning via Windows Task Scheduler
- No data loss or corruption during migration
- Performance equal to or better than Coravel
- Enhanced monitoring capabilities
- Reduced maintenance overhead
Business Success
- Minimal business disruption during migration
- Improved reliability (no more scheduler stop issues)
- Better visibility into task execution (Windows Scheduler history)
- Foundation for future enhancements
π Documentation Updates Required
-
Technical Documentation:
- Update architecture diagrams
- Document new API endpoints
- Windows Task Scheduler setup guide
- Troubleshooting guide
-
Operational Documentation:
- Daily monitoring checklist
- Job failure response procedures
- Schedule modification procedures
-
Developer Documentation:
- How to add new scheduled jobs
- Job class template
- Testing procedures
π‘ Future Enhancements (Post-Migration)
Phase 2 Improvements (Optional)
- Centralized Monitoring Dashboard: Simple web page showing job status
- Enhanced Alerting: Email/SMS notifications for job failures
- Job Dependencies: Simple job chaining if needed
- Performance Metrics: Track execution times, success rates
Keep It Simple Philosophy
Remember the core principle: Do NOT over-engineer. Start with the minimal solution (HTTP triggers + Windows Scheduler). Only add complexity when proven necessary by actual business needs.
This lean migration plan follows the instructions in Leaner-Low-Risk-Plan.md to provide a minimal, low-risk approach to replacing Coravel with Windows Task Scheduler while keeping existing business logic intact.