bc-code-intelligence-mcp
Version:
BC Code Intelligence MCP Server - Complete Specialist Bundle with AI-driven expert consultation, seamless handoffs, and context-preserving workflows
615 lines (521 loc) • 17.9 kB
Markdown
# Task Scheduler Pattern Implementation - AL Code Examples
This sample demonstrates implementing robust task scheduling patterns for workflow automation in Business Central.
## Task Definition Interface
```al
// Core task interface for scheduler operations
interface IScheduledTask
{
procedure Execute(): Boolean;
procedure GetTaskName(): Text[50];
procedure GetDescription(): Text[250];
procedure CanRetry(): Boolean;
procedure GetMaxRetries(): Integer;
}
```
## Task Scheduler Engine
```al
// Main task scheduling engine
codeunit 50300 "Task Scheduler Engine"
{
var
TaskQueue: List of [Codeunit "Scheduled Task Entry"];
ExecutingTasks: Dictionary of [Guid, Boolean];
procedure ScheduleTask(Task: Interface IScheduledTask; ExecutionTime: DateTime; Priority: Integer)
var
TaskEntry: Codeunit "Scheduled Task Entry";
TaskId: Guid;
begin
TaskId := CreateGuid();
TaskEntry.Initialize(TaskId, Task, ExecutionTime, Priority);
AddToQueue(TaskEntry);
LogTaskScheduled(TaskId, Task.GetTaskName(), ExecutionTime);
end;
procedure ScheduleRecurringTask(Task: Interface IScheduledTask; FirstExecution: DateTime; RecurrencePattern: Text[50])
var
NextExecution: DateTime;
TaskId: Guid;
begin
TaskId := CreateGuid();
NextExecution := FirstExecution;
repeat
ScheduleTask(Task, NextExecution, 5); // Default priority
NextExecution := CalculateNextExecution(NextExecution, RecurrencePattern);
until NextExecution > CalcDate('1Y', Today); // Limit to 1 year ahead
end;
procedure ProcessScheduledTasks()
var
TaskEntry: Codeunit "Scheduled Task Entry";
CurrentTime: DateTime;
begin
CurrentTime := CurrentDateTime;
foreach TaskEntry in TaskQueue do begin
if TaskEntry.IsReadyForExecution(CurrentTime) and not IsTaskExecuting(TaskEntry.GetTaskId()) then
ExecuteTaskAsync(TaskEntry);
end;
end;
local procedure ExecuteTaskAsync(TaskEntry: Codeunit "Scheduled Task Entry")
var
TaskId: Guid;
ExecutionResult: Boolean;
begin
TaskId := TaskEntry.GetTaskId();
ExecutingTasks.Set(TaskId, true);
try
ExecutionResult := TaskEntry.Execute();
HandleTaskCompletion(TaskEntry, ExecutionResult);
finally
ExecutingTasks.Remove(TaskId);
end;
end;
local procedure HandleTaskCompletion(TaskEntry: Codeunit "Scheduled Task Entry"; Success: Boolean)
begin
if Success then begin
RemoveFromQueue(TaskEntry);
LogTaskCompleted(TaskEntry.GetTaskId(), TaskEntry.GetTaskName());
end else begin
HandleTaskFailure(TaskEntry);
end;
end;
local procedure HandleTaskFailure(TaskEntry: Codeunit "Scheduled Task Entry")
var
RetryCount: Integer;
MaxRetries: Integer;
begin
RetryCount := TaskEntry.GetRetryCount();
MaxRetries := TaskEntry.GetMaxRetries();
if (RetryCount < MaxRetries) and TaskEntry.CanRetry() then begin
TaskEntry.IncrementRetryCount();
TaskEntry.SetNextExecutionTime(CalcDate('5M', CurrentDateTime)); // Retry in 5 minutes
LogTaskRetry(TaskEntry.GetTaskId(), TaskEntry.GetTaskName(), RetryCount + 1);
end else begin
RemoveFromQueue(TaskEntry);
LogTaskFailed(TaskEntry.GetTaskId(), TaskEntry.GetTaskName());
HandleDeadLetter(TaskEntry);
end;
end;
local procedure AddToQueue(TaskEntry: Codeunit "Scheduled Task Entry")
begin
TaskQueue.Add(TaskEntry);
SortQueueByPriorityAndTime();
end;
local procedure RemoveFromQueue(TaskEntry: Codeunit "Scheduled Task Entry")
var
Index: Integer;
CurrentEntry: Codeunit "Scheduled Task Entry";
begin
for Index := 1 to TaskQueue.Count do begin
CurrentEntry := TaskQueue.Get(Index);
if CurrentEntry.GetTaskId() = TaskEntry.GetTaskId() then begin
TaskQueue.RemoveAt(Index);
break;
end;
end;
end;
local procedure SortQueueByPriorityAndTime()
begin
// Simplified sorting - in production, implement proper priority queue
end;
local procedure IsTaskExecuting(TaskId: Guid): Boolean
var
IsExecuting: Boolean;
begin
exit(ExecutingTasks.Get(TaskId, IsExecuting) and IsExecuting);
end;
local procedure CalculateNextExecution(LastExecution: DateTime; Pattern: Text[50]): DateTime
begin
case Pattern of
'DAILY':
exit(LastExecution + 24 * 60 * 60 * 1000); // Add 24 hours
'WEEKLY':
exit(CalcDate('1W', DT2Date(LastExecution)));
'MONTHLY':
exit(CalcDate('1M', DT2Date(LastExecution)));
else
exit(LastExecution + 60 * 60 * 1000); // Default: hourly
end;
end;
local procedure HandleDeadLetter(TaskEntry: Codeunit "Scheduled Task Entry")
begin
// Move failed tasks to dead letter queue for manual review
end;
local procedure LogTaskScheduled(TaskId: Guid; TaskName: Text[50]; ExecutionTime: DateTime)
begin
// Log scheduling event
end;
local procedure LogTaskCompleted(TaskId: Guid; TaskName: Text[50])
begin
// Log completion event
end;
local procedure LogTaskRetry(TaskId: Guid; TaskName: Text[50]; RetryAttempt: Integer)
begin
// Log retry event
end;
local procedure LogTaskFailed(TaskId: Guid; TaskName: Text[50])
begin
// Log failure event
end;
}
```
## Scheduled Task Entry
```al
// Individual task entry management
codeunit 50301 "Scheduled Task Entry"
{
var
TaskId: Guid;
Task: Interface IScheduledTask;
ScheduledTime: DateTime;
Priority: Integer;
RetryCount: Integer;
MaxRetries: Integer;
NextExecutionTime: DateTime;
procedure Initialize(NewTaskId: Guid; NewTask: Interface IScheduledTask; ExecutionTime: DateTime; TaskPriority: Integer)
begin
TaskId := NewTaskId;
Task := NewTask;
ScheduledTime := ExecutionTime;
NextExecutionTime := ExecutionTime;
Priority := TaskPriority;
RetryCount := 0;
MaxRetries := Task.GetMaxRetries();
end;
procedure Execute(): Boolean
begin
exit(Task.Execute());
end;
procedure IsReadyForExecution(CurrentTime: DateTime): Boolean
begin
exit(CurrentTime >= NextExecutionTime);
end;
procedure CanRetry(): Boolean
begin
exit(Task.CanRetry() and (RetryCount < MaxRetries));
end;
procedure IncrementRetryCount()
begin
RetryCount += 1;
end;
procedure SetNextExecutionTime(ExecutionTime: DateTime)
begin
NextExecutionTime := ExecutionTime;
end;
procedure GetTaskId(): Guid
begin
exit(TaskId);
end;
procedure GetTaskName(): Text[50]
begin
exit(Task.GetTaskName());
end;
procedure GetRetryCount(): Integer
begin
exit(RetryCount);
end;
procedure GetMaxRetries(): Integer
begin
exit(MaxRetries);
end;
procedure GetPriority(): Integer
begin
exit(Priority);
end;
procedure GetNextExecutionTime(): DateTime
begin
exit(NextExecutionTime);
end;
}
```
## Sales Report Generation Task
```al
// Example task: Generate sales reports
codeunit 50302 "Sales Report Generation Task" implements IScheduledTask
{
var
ReportParameters: Dictionary of [Text, Variant];
procedure Execute(): Boolean
var
SalesReportGeneration: Codeunit "Sales Report Generator";
ReportId: Integer;
FromDate: Date;
ToDate: Date;
begin
// Extract parameters
if not ReportParameters.Get('FromDate', FromDate) then
FromDate := CalcDate('-1M', Today);
if not ReportParameters.Get('ToDate', ToDate) then
ToDate := Today;
if not ReportParameters.Get('ReportId', ReportId) then
ReportId := Report::"Sales - Invoice";
// Generate report
try
SalesReportGeneration.GenerateReport(ReportId, FromDate, ToDate);
NotifyReportCompletion(ReportId, FromDate, ToDate);
exit(true);
except
LogReportError(GetLastErrorText);
exit(false);
end;
end;
procedure GetTaskName(): Text[50]
begin
exit('SALES_REPORT_GENERATION');
end;
procedure GetDescription(): Text[250]
begin
exit('Generates scheduled sales reports for management review');
end;
procedure CanRetry(): Boolean
begin
exit(true);
end;
procedure GetMaxRetries(): Integer
begin
exit(3);
end;
procedure SetParameters(Parameters: Dictionary of [Text, Variant])
begin
ReportParameters := Parameters;
end;
local procedure NotifyReportCompletion(ReportId: Integer; FromDate: Date; ToDate: Date)
var
EmailMessage: Codeunit "Email Message";
Email: Codeunit Email;
begin
// Send email notification about report completion
EmailMessage.Create(
'manager@company.com',
'Sales Report Generated',
StrSubstNo('Sales report %1 for period %2 to %3 has been generated successfully.', ReportId, FromDate, ToDate)
);
Email.Send(EmailMessage);
end;
local procedure LogReportError(ErrorText: Text)
begin
// Log report generation error
end;
}
```
## Inventory Reorder Task
```al
// Example task: Automatic inventory reordering
codeunit 50303 "Inventory Reorder Task" implements IScheduledTask
{
procedure Execute(): Boolean
var
Item: Record Item;
PurchaseHeader: Record "Purchase Header";
PurchaseLine: Record "Purchase Line";
ItemsProcessed: Integer;
begin
ItemsProcessed := 0;
Item.SetRange(Blocked, false);
Item.SetRange("Replenishment System", Item."Replenishment System"::Purchase);
Item.SetFilter("Reorder Point", '>0');
if Item.FindSet() then
repeat
Item.CalcFields(Inventory);
if Item.Inventory <= Item."Reorder Point" then begin
if CreateReorderPurchaseOrder(Item) then
ItemsProcessed += 1;
end;
until Item.Next() = 0;
// Log results
Message('Automatic reordering completed. %1 items processed.', ItemsProcessed);
exit(true);
end;
procedure GetTaskName(): Text[50]
begin
exit('INVENTORY_REORDER');
end;
procedure GetDescription(): Text[250]
begin
exit('Automatically creates purchase orders for items below reorder point');
end;
procedure CanRetry(): Boolean
begin
exit(true);
end;
procedure GetMaxRetries(): Integer
begin
exit(2);
end;
local procedure CreateReorderPurchaseOrder(Item: Record Item): Boolean
var
PurchaseHeader: Record "Purchase Header";
PurchaseLine: Record "Purchase Line";
VendorNo: Code[20];
OrderQuantity: Decimal;
begin
// Get preferred vendor
VendorNo := GetPreferredVendor(Item."No.");
if VendorNo = '' then
exit(false);
// Calculate order quantity
OrderQuantity := Item."Maximum Inventory" - Item.Inventory;
if OrderQuantity <= 0 then
exit(false);
// Create purchase header
PurchaseHeader.Init();
PurchaseHeader."Document Type" := PurchaseHeader."Document Type"::Order;
PurchaseHeader."No." := '';
PurchaseHeader.Insert(true);
PurchaseHeader.Validate("Buy-from Vendor No.", VendorNo);
PurchaseHeader.Modify(true);
// Create purchase line
PurchaseLine.Init();
PurchaseLine."Document Type" := PurchaseHeader."Document Type";
PurchaseLine."Document No." := PurchaseHeader."No.";
PurchaseLine."Line No." := 10000;
PurchaseLine.Insert(true);
PurchaseLine.Validate(Type, PurchaseLine.Type::Item);
PurchaseLine.Validate("No.", Item."No.");
PurchaseLine.Validate(Quantity, OrderQuantity);
PurchaseLine.Modify(true);
exit(true);
end;
local procedure GetPreferredVendor(ItemNo: Code[20]): Code[20]
var
ItemVendor: Record "Item Vendor";
begin
ItemVendor.SetRange("Item No.", ItemNo);
ItemVendor.SetRange("Vendor No.", '<>''''');
if ItemVendor.FindFirst() then
exit(ItemVendor."Vendor No.");
exit('');
end;
}
```
## Task Scheduler Management
```al
// Management interface for task scheduler
page 50300 "Task Scheduler Management"
{
PageType = List;
ApplicationArea = All;
UsageCategory = Administration;
Caption = 'Task Scheduler Management';
layout
{
area(Content)
{
group(SchedulerStatus)
{
Caption = 'Scheduler Status';
field(IsRunning; SchedulerRunning)
{
ApplicationArea = All;
Caption = 'Scheduler Running';
Editable = false;
}
field(ActiveTasks; ActiveTaskCount)
{
ApplicationArea = All;
Caption = 'Active Tasks';
Editable = false;
}
}
}
}
actions
{
area(Processing)
{
action(StartScheduler)
{
Caption = 'Start Scheduler';
ApplicationArea = All;
trigger OnAction()
begin
StartTaskScheduler();
end;
}
action(StopScheduler)
{
Caption = 'Stop Scheduler';
ApplicationArea = All;
trigger OnAction()
begin
StopTaskScheduler();
end;
}
action(ScheduleSalesReport)
{
Caption = 'Schedule Sales Report';
ApplicationArea = All;
trigger OnAction()
begin
ScheduleSalesReportTask();
end;
}
action(ScheduleInventoryReorder)
{
Caption = 'Schedule Inventory Reorder';
ApplicationArea = All;
trigger OnAction()
begin
ScheduleInventoryReorderTask();
end;
}
}
}
var
TaskScheduler: Codeunit "Task Scheduler Engine";
SchedulerRunning: Boolean;
ActiveTaskCount: Integer;
trigger OnOpenPage()
begin
UpdateSchedulerStatus();
end;
local procedure StartTaskScheduler()
begin
// Start background processing
TaskScheduler.ProcessScheduledTasks();
SchedulerRunning := true;
UpdateSchedulerStatus();
Message('Task scheduler started successfully.');
end;
local procedure StopTaskScheduler()
begin
SchedulerRunning := false;
UpdateSchedulerStatus();
Message('Task scheduler stopped.');
end;
local procedure ScheduleSalesReportTask()
var
SalesReportTask: Codeunit "Sales Report Generation Task";
Parameters: Dictionary of [Text, Variant];
ExecutionTime: DateTime;
begin
// Set parameters
Parameters.Set('FromDate', CalcDate('-1M', Today));
Parameters.Set('ToDate', Today);
Parameters.Set('ReportId', Report::"Sales - Invoice");
SalesReportTask.SetParameters(Parameters);
// Schedule for tomorrow at 6 AM
ExecutionTime := CreateDateTime(CalcDate('1D', Today), 060000T);
TaskScheduler.ScheduleTask(SalesReportTask, ExecutionTime, 1);
Message('Sales report scheduled for %1', ExecutionTime);
end;
local procedure ScheduleInventoryReorderTask()
var
InventoryReorderTask: Codeunit "Inventory Reorder Task";
ExecutionTime: DateTime;
begin
// Schedule for daily execution at 8 AM
ExecutionTime := CreateDateTime(CalcDate('1D', Today), 080000T);
TaskScheduler.ScheduleRecurringTask(InventoryReorderTask, ExecutionTime, 'DAILY');
Message('Inventory reorder task scheduled for daily execution at 8:00 AM');
end;
local procedure UpdateSchedulerStatus()
begin
// Update status fields
ActiveTaskCount := GetActiveTaskCount();
end;
local procedure GetActiveTaskCount(): Integer
begin
// Return count of active tasks
exit(5); // Placeholder
end;
}
```
This comprehensive implementation provides a robust task scheduling system with error handling, retry mechanisms, and support for both one-time and recurring tasks in Business Central.