004 Lean Windows Task Scheduler Migration Plan

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)

  1. Do NOT redesign your scheduler framework
  2. Do NOT centralize into new orchestration services (yet)
  3. Replace Coravel’s trigger mechanism with HTTP triggers
  4. Keep existing job logic almost unchanged

πŸ—οΈ Current Architecture Analysis

Existing Coravel-Based Scheduler

Coravel Engine β†’ TaskSchedulerInitializer β†’ IInvocable Schedulers β†’ Business Logic

7 Scheduled Task Types

  1. 001 - Indent Email Scheduler
  2. 002 - Invoice Email Scheduler
  3. 003 - Sales Order Email Scheduler
  4. 004 - AR Email Scheduler
  5. 005 - Voucher Payment Email Scheduler
  6. 006 - Voucher Receipt Email Scheduler
  7. 007 - AI Insights Scheduler

Key Components

  • TaskSchedulerInitializer.cs: Dynamically schedules tasks based on database configuration
  • 7 Scheduler Classes: Each implements IInvocable interface with Invoke() method
  • Program.cs: Configures Coravel with AddScheduler(), AddQueue(), and UseScheduler()

🧩 New Architecture (Minimal Change)

AFTER Migration

Windows Task Scheduler β†’ HTTPS + API Key β†’ Thin API Endpoints β†’ Existing Scheduler Logic (renamed) β†’ Business Logic

What 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 (TaskSchedulerConfig model)

What Goes Away ❌

  • Coravel packages (Coravel, Coravel.Scheduling, Coravel.Invocable)
  • IInvocable interface
  • TaskSchedulerInitializer.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

  1. Action: HTTPS call to API endpoint
  2. Trigger: Daily/Time-based as per current CRON schedules
  3. 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 Highest

Manual Setup via GUI

  1. Open Task Scheduler on Win-S IIS Server
  2. Click Create Task
  3. 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
  4. Triggers Tab:
    • New β†’ Daily β†’ Start: 9:00:00 AM β†’ OK
  5. 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'}"
  6. Conditions Tab: Adjust as needed (default usually OK)
  7. 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)

  1. Week 1: Create new job classes and controller

    • Copy scheduler files to Jobs folder
    • Refactor IInvocable β†’ regular classes
    • Add Run() and RunAll() methods
    • Create ScheduledJobsController.cs
    • Update Program.cs service registrations
  2. 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)

  1. Deploy updated API to staging
  2. Configure Windows Task Scheduler jobs in staging
  3. Run both systems in parallel (Coravel + Windows Scheduler)
  4. Compare results for consistency
  5. Fix any issues discovered

Phase 3: Production Migration (1 Week)

  1. Day 1: Deploy updated API (keep Coravel code intact but disabled)
  2. Day 2: Configure Windows Task Scheduler jobs
  3. Days 3-5: Monitor both systems (Coravel OFF, Windows Scheduler ON)
  4. Day 6: Verify all jobs ran successfully for 3+ days
  5. Day 7: Remove Coravel packages and cleanup code

Phase 4: Cleanup & Optimization

  1. Remove Coravel NuGet packages
  2. Delete TaskSchedulerInitializer.cs
  3. Remove any Coravel-specific UI components from Web project
  4. 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.cs implemented with all endpoints
  • Program.cs updated (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.cs deleted
  • 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

  1. Stop all Windows Task Scheduler jobs
  2. Re-enable Coravel in Program.cs (uncomment lines)
  3. Restart API application
  4. Verify Coravel jobs resume normally
  5. Investigate and fix Windows Scheduler issues
  6. 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

  1. Technical Documentation:

    • Update architecture diagrams
    • Document new API endpoints
    • Windows Task Scheduler setup guide
    • Troubleshooting guide
  2. Operational Documentation:

    • Daily monitoring checklist
    • Job failure response procedures
    • Schedule modification procedures
  3. Developer Documentation:

    • How to add new scheduled jobs
    • Job class template
    • Testing procedures

πŸ’‘ Future Enhancements (Post-Migration)

Phase 2 Improvements (Optional)

  1. Centralized Monitoring Dashboard: Simple web page showing job status
  2. Enhanced Alerting: Email/SMS notifications for job failures
  3. Job Dependencies: Simple job chaining if needed
  4. 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.