Detailed Windows Task Scheduler Migration Plan
Date: February 2, 2026
Based on: docs/004-Lean-Windows-Task-Scheduler-Migration-Plan.md & docs/005-Example-Code-Transformations.md
This document details the step-by-step plan to migrate from the Coravel scheduler to Windows Task Scheduler. CRITICAL RULE: Do NOT remove any old code. All replaced code must be commented out to allow for easy rollback and reference.
1. Migration Overview
The goal is to replace the internal Coravel scheduler with external triggers from Windows Task Scheduler via HTTP API calls. This reduces memory usage and improves reliability.
Current Flow: Program.cs -> Coravel -> TaskSchedulerInitializer -> IInvocable Classes
New Flow: Windows Task Scheduler -> API Endpoint -> Job Class -> Business Logic
2. Implementation Steps
Phase 1: Create Job Classes (The “Jobs” Folder)
We will create a new folder Jobs in ErpCrystal_MFG.Api and create new job classes based on the existing schedulers.
-
Create Folder:
ErpCrystal_MFG.Api/Jobs -
Duplicate Files: Copy the following files from
TaskScheduler/toJobs/:IndentEmailScheduler.cs->IndentEmailJob.csInvoiceEmailScheduler.cs->InvoiceEmailJob.csSalesOrderEmailScheduler.cs->SalesOrderEmailJob.csArEmailScheduler.cs->ArEmailJob.csVoucherPaymentEmailScheduler.cs->VoucherPaymentEmailJob.csVoucherReceiptEmailScheduler.cs->VoucherReceiptEmailJob.csAIInsightsScheduler.cs->AIInsightsJob.cs
-
Refactor Each Job Class:
- Namespace: Change to
ErpCrystal_MFG.Api.Jobs. - Class Name: Rename to
[Name]Job. - Interface: Comment out
: IInvocable. - Constructor: Add
ITaskSchedulerRepositoryparameter. - Methods:
- Comment out
public async Task Invoke()signature (keep the body). - Create
public async Task Run(string dbname): Paste the body ofInvokehere.- Change “foreach dbname” loop to run only for the passed
dbname. - Change
continuetoreturn.
- Change “foreach dbname” loop to run only for the passed
- Create
public async Task RunAll(): Implement loop to callRun(dbname)for all DBs fetched viadbrepo.
- Comment out
- Logging: Keep
LogMessageas is.
Example Code Strategy:
// public class IndentEmailScheduler : IInvocable <-- OLD public class IndentEmailJob // : IInvocable <-- NEW { // ... Dependencies ... // public async Task Invoke() <-- OLD (Commented out) // { // foreach (var dbname in dbnamelist) { ... } // } public async Task Run(string dbname) // <-- NEW { // Logic from inside the loop } } - Namespace: Change to
Phase 2: Create Controller
Create ScheduledJobsController.cs in Controllers/ folder.
- Attributes:
[ApiController],[Route("api/jobs")],[ApiKeyAuth]. - Dependencies: Inject all new
*Jobclasses. - Endpoints: Create 2 endpoints per job type:
POST /api/jobs/[job-name]: Callsjob.RunAll().POST /api/jobs/[job-name]/{dbName}: Callsjob.Run(dbName).
Phase 3: Configure Program.cs
Modify ErpCrystal_MFG.Api/Program.cs to disable Coravel and enable new Jobs.
-
Register New Jobs:
// Add these lines builder.Services.AddScoped<IndentEmailJob>(); builder.Services.AddScoped<InvoiceEmailJob>(); // ... repeat for all 7 jobs -
Disable Coravel (Comment Out):
// builder.Services.AddScheduler(); builder.Services.AddQueue(); // KEEP: User requested // builder.Services.AddScoped<TaskSchedulerInitializer>(); -
Disable Scheduler Execution (Comment Out):
// app.Services.UseScheduler(async scheduler => // { // ... // });
Phase 4: Windows Task Scheduler Setup
For each of the 7 tasks, create a Windows Task Scheduler entry (Script provided in docs/004 step 307).
-
Trigger: Match existing CRON schedule.
-
Action:
Invoke-RestMethodto the new API endpoints. -
Auth: Pass
ApiKeyheader (Confirmed analysis ofApiKeyAuthAttribute.cs).PowerShell Script Update:
$action = New-ScheduledTaskAction -Execute "powershell.exe" ` -Argument "-Command `"Invoke-RestMethod -Uri 'https://localhost:port/api/jobs/indent-email' -Method POST -Headers @{'ApiKey' = 'YOUR_API_KEY'} -UseBasicParsing`""
3. Plan for “Comment Out” Strategy
We strictly follow the instruction to comment out rather than delete.
In Program.cs:
/* MIGRATION: Coravel Scheduler disabled
builder.Services.AddScheduler();
builder.Services.AddQueue();
*/In Job Classes:
// using Coravel.Invocable; // MIGRATION: Commented out
public class IndentEmailJob // : IInvocable // MIGRATION: Interface removed
{
// ...
/* MIGRATION: Old Invoke method replaced by Run() and RunAll()
public async Task Invoke()
{
...
}
*/
}4. Resolved Doubts & User Feedback
-
Task Codes:
- Confirmed: Task codes will remain exactly the same as old ones (001, 002, etc.).
-
API Key Analysis:
- Question: Do we need any changes?
- Analysis: Checked
ErpCrystal_MFG.Api/CustomAttributes/ApiKeyAuthAttribute.cs. - Result: The attribute expects the header name
ApiKey(Line 9:private const string apiKeyName = "ApiKey";). - Action: The PowerShell script in
docs/004incorrectly usedX-API-Key. We MUST useApiKeyin the Windows Task Scheduler configuration. No code changes required in the API, only in the scheduler script/configuration.
-
Testing Strategy:
- Confirmed: Testing will be done locally first.
- Plan:
- Run the API locally (IIS Express or Kestrel).
- Set up Windows Task Scheduler on the local developer machine.
- Point the Task Scheduler URL to
https://localhost:[PORT]/api/jobs/.... - Trigger tasks manually via Scheduler and verify local logs.
-
Existing Methods:
- Confirmed:
ITaskSchedulerRepositoryand other dependencies are already present and will be reused.
- Confirmed:
-
Dynamic Scheduling (CRON from DB):
- Question: Earlier CRON was dynamic and fetched from DB. Is it handled in WTS?
- Answer: No. The “Lean” migration shifts the scheduling responsibility to Windows Task Scheduler.
- Implication: The CRON expression in the database will effectively be ignored for triggering. All databases will run when the Windows Task Scheduler triggers the job (e.g., if WTS is set to 9 AM, all databases run at 9 AM).
- Action: If distinct times are required (e.g., Client A at 9 AM, Client B at 10 AM), you must manually create multiple Triggers in Windows Task Scheduler, or accept that all clients will run on the standard schedule defined in WTS.
-
Missing Features & Logic Gaps vs Old Docs:
This section highlights logic present in Coravel/Old Code that is NOT automatically transferred to Windows Task Scheduler. You must handle these manually in WTS settings.
-
Dynamic CRON Schedules:
- Old Logic:
TaskSchedulerInitializerfetched CRON expressions from the DB at startup. - New Logic: WTS uses Static Triggers defined in the Task Scheduler UI.
- GAP: Changes to CRON strings in the DB will NO LONGER change the schedule. You must update the WTS Trigger manually.
- Old Logic:
-
Prevent Overlapping:
- Old Logic:
.PreventOverlapping("Key")in code ensured only one instance ran. - New Logic: In WTS > Settings tab > “If the task is already running, then the following rule applies: Do not start a new instance”.
- GAP: If you forget to check this box in WTS, multiple instances could run simultaneously.
- Old Logic:
-
Retry on Failure:
- Old Logic: Handled by custom code or Coravel policies (if any).
- New Logic: In WTS > Settings tab > “If the task fails, restart every: [1 minute]”.
- GAP: This is a pure configuration setting now, not code.
-
TimeZone Handling:
- Old Logic:
.Zoned(TimeZoneInfo.Local). - New Logic: WTS runs on the Server’s System Time by default.
- GAP: Ensure the Server’s time matches the expected business time (usually standard on single servers).
- Old Logic:
-