6.4 Processing-Only Reports & Report Substitutions
Key Takeaways
- Setting ProcessingOnly = true creates a report dedicated exclusively to batch data modifications, calculations, and scheduled tasks, completely bypassing layout compilation, document rendering, and print spooling.
- The report execution trigger lifecycle follows a deterministic order: OnInitReport -> requestpage triggers -> OnPreReport -> OnPreDataItem -> OnAfterGetRecord (per record) -> OnPostDataItem -> OnPostReport.
- Flow control functions CurrReport.Skip(), CurrReport.Break(), and CurrReport.Quit() control iteration execution across individual records, dataitems, and the entire report.
- Interactive batch processing reports utilize progress dialogs (Dialog.Open, Dialog.Update) that must strictly be guarded with if GuiAllowed() then to prevent fatal runtime exceptions in headless Job Queue sessions.
- The report substitution pattern uses the ReportManagement.OnAfterSubstituteReport event subscriber to redirect standard report invocations to custom report objects without modifying base application code.
6.4 Processing-Only Reports & Report Substitutions
Not all reports in Business Central generate visual output. Processing-Only reports are specialized AL report objects used for high-performance batch data processing, automated database maintenance, and scheduled transactional routines. Furthermore, when partners need to replace standard Microsoft document reports with custom implementations, Business Central provides an event-driven Report Substitution mechanism that redirects report calls without modifying base code.
1. Processing-Only Reports: Batch Operations
When a report is designed solely to modify records, calculate ledger entries, or perform periodic batch updates, the ProcessingOnly property is set to true.
report 50110 "Batch Adjust Item Costs"
{
Caption = 'Batch Adjust Item Costs';
ProcessingOnly = true;
UseRequestPage = true;
UsageCategory = Tasks;
ApplicationArea = Basic, Suite;
dataset
{
dataitem(Item; Item)
{
DataItemTableView = sorting("No.") where(Blocked = const(false));
RequestFilterFields = "No.", "Item Category Code";
trigger OnPreDataItem()
begin
TotalRecords := Item.Count();
CurrentRecord := 0;
if GuiAllowed() then
ProgressDialog.Open('Adjusting item costs...\Progress: @1@@@@@@@@@@@@@@@@@\Current Item: #2###############');
end;
trigger OnAfterGetRecord()
begin
CurrentRecord += 1;
if GuiAllowed() then begin
ProgressDialog.Update(1, Round(CurrentRecord / TotalRecords * 10000, 1));
ProgressDialog.Update(2, Item."No.");
end;
Item.Validate("Unit Cost", Item."Unit Cost" * CostFactor);
Item.Modify(true);
end;
trigger OnPostDataItem()
begin
if GuiAllowed() then
ProgressDialog.Close();
end;
}
}
var
ProgressDialog: Dialog;
CostFactor: Decimal;
TotalRecords: Integer;
CurrentRecord: Integer;
}
Architectural Characteristics of Processing-Only Reports
- No Layout Requirement: The compiler does not generate or validate RDLC, Word, or Excel layouts. Attempting to define a
renderingblock or layout file on a processing-only report results in a compilation error. UseRequestPage: When set totrue, the user can configure filters and options before processing. When set tofalse, the report executes immediately upon invocation—ideal for automated background jobs.- Transactional Integrity: All record modifications executed inside
OnAfterGetRecordparticipate in the active database transaction. If an unhandled error occurs, the entire batch rolls back unless explicitCommit()calls are made.
2. Progress Dialogs & Headless Safety
In interactive desktop sessions, batch processes should provide visual feedback using the AL Dialog data type.
Dialog String Formatting
#1######: Represents a text or value placeholder aligned left.@1@@@@@@: Represents a graphical percentage progress bar where the updated value must be an integer between0and10000(representing 0.00% to 100.00%).\: Line break character inside dialog strings.
Exam Watchout — The
GuiAllowed()Rule: Business Central frequently executes batch reports in background sessions, Job Queues, or API web service calls. InvokingDialog.Open()orMessage()in a non-interactive session throws a fatal runtime exception. Developers must always guard dialog operations withif GuiAllowed() then.
3. Report Trigger Lifecycle & Flow Control
Understanding the exact sequence of trigger execution is crucial for initializing variables, applying dynamic filters, and managing state across report dataitems.
Complete Report Execution Sequence
OnInitReport: Runs once when the report is loaded into memory. Used to set default variable values before the request page opens.requestpageTriggers:OnInit->OnOpenPage-> (User interactions) ->OnQueryClosePage.OnPreReport: Runs after the request page closes but before any data is fetched. Used to verify global preconditions, check licensing, or initialize shared data buffers.OnPreDataItem: Runs once per DataItem before the table cursor is opened. Used to set dynamic filters using AL code (SetRange,SetFilter) or apply sorting keys.OnAfterGetRecord: Runs for every individual record fetched from the DataItem table. Used to perform calculations, evaluate custom business logic, and manipulate export variables.OnPostDataItem: Runs once per DataItem after all its records have been iterated.OnPostReport: Runs once after all DataItems have finished processing. Used for cleanup, logging telemetry signals, or triggering outbound email notifications.
Report Flow Control Statements
AL provides three specialized methods on CurrReport to alter loop execution dynamically:
| Flow Control Method | Behavioral Scope |
|---|---|
CurrReport.Skip() | Skips the current record in the active DataItem. The report engine abandons the rest of OnAfterGetRecord, does not emit a row to the dataset for this record, and proceeds immediately to the next record. |
CurrReport.Break() | Immediately terminates iteration of the active DataItem. The report skips all remaining records in this DataItem, executes OnPostDataItem, and proceeds to the next sibling or parent DataItem. |
CurrReport.Quit() | Immediately terminates execution of the entire report. No further DataItems are processed, OnPostReport is bypassed, and control returns to the caller. |
4. Report Substitution Architecture
In standard Business Central implementations, end users trigger standard document reports (such as Report 1306 Standard Sales - Invoice) from dozens of posted document pages, list actions, and automated posting routines. Modifying every base page action to point to a custom report is impossible in modern extensions.
To solve this, Business Central provides the Report Substitution Pattern via Codeunit 44 ReportManagement.
+-----------------------------------------------------------------------+
| REPORT SUBSTITUTION PATTERN |
| |
| User clicks "Print" on Posted Sales Invoice (Calls Report 1306) |
| │ |
| v |
| Codeunit 44 "ReportManagement" invokes OnAfterSubstituteReport |
| │ |
| v |
| Custom Subscriber intercepts: |
| if ReportId = Report::"Standard Sales - Invoice" then |
| NewReportId := Report::"Contoso Custom Sales Invoice"; |
| │ |
| v |
| Business Central executes Report 50100 instead of Report 1306 |
+-----------------------------------------------------------------------+
Implementing Report Substitution in AL
Developers subscribe to the OnAfterSubstituteReport event published by Codeunit ReportManagement:
codeunit 50102 "Report Substitution Handler"
{
[EventSubscriber(ObjectType::Codeunit, Codeunit::ReportManagement, 'OnAfterSubstituteReport', '', false, false)]
local procedure OnAfterSubstituteReport(ReportId: Integer; var NewReportId: Integer)
begin
if ReportId = Report::"Standard Sales - Invoice" then
NewReportId := Report::"Contoso Custom Invoice";
if ReportId = Report::"Purchase Order" then
NewReportId := Report::"Contoso Purchase Order";
end;
}
- When any codeunit, page action, or user clicks a button calling
Report.Run(Report::"Standard Sales - Invoice", ...), the platform raisesOnAfterSubstituteReport. - The subscriber inspects
ReportId. If it matches the target base report, it reassignsNewReportIdto the custom report object ID. - The platform seamlessly executes the custom report in place of the base report across all standard system entry points.
Report Extensions (reportextension) vs. Report Substitution
| Customization Goal | Recommended Approach |
|---|---|
| Adding 2-3 custom fields and a modified RDLC/Excel layout to a standard invoice | reportextension Object: Keeps the base report architecture intact while adding columns, dataitems, and layouts additively. |
| Completely replacing the entire invoice calculation engine, layout hierarchy, or dataset structure | Report Substitution Pattern: Completely decouples from the standard report by executing an independent custom report object via Codeunit 44. |
A developer is building a batch maintenance report in AL that recalculates customer risk levels across 100,000 records on a scheduled nightly basis. The report should never generate visual output, print documents, or prompt for layout rendering. Which property combination should be configured in the report definition?
A developer authors a batch processing report that iterates through item ledger entries. To show execution progress during interactive user runs, the report includes a progress dialog. What coding practice must be applied to prevent runtime exceptions when the report is executed automatically by the Job Queue in a background session?
An organization wants all standard system actions that execute the base 'Standard Sales - Invoice' (Report 1306) to run their custom report 'Contoso Sales Invoice' (Report 50100) instead, without modifying any standard page actions or posting codeunits. How is this achieved in modern Business Central development?
During the execution of OnAfterGetRecord in an AL report, a developer wants to immediately exit the active DataItem, skip all remaining records in that table cursor, trigger OnPostDataItem, and continue execution with the next sibling or parent DataItem. Which CurrReport method should be invoked?