Video: Customizing Standard Reports

In this video, I show how to customize a standard report in Microsoft Dynamics 365 Business Central.

Or perhaps more specific, how to get to the point where you can customize a standard report.


In this video, Erik walks through the process of customizing a standard report in Business Central. Since there is no “report extension” object type in AL (unlike page extensions or table extensions), the approach involves creating a complete copy of the original report and then using report substitution to replace it. Erik demonstrates this step-by-step using the Customer – Top 10 List report as an example.

The Challenge: No Report Extensions in AL

One of the key challenges with reports in Business Central is that there is no reportextension object type. Unlike pages and tables where you can create extension objects to modify behavior, reports require a different strategy. You need to create a full copy of the report, modify it to your needs, and then substitute it for the original using an event subscriber.

Step 1: Obtain the Original Report Files

A report in Business Central consists of at least two files:

  • The report object — an .al file containing the AL code, dataset definition, and request page
  • The layout file — in this case an .rdlc file (it could also be a Word layout or potentially other formats)

Erik uses a Docker container for development with the NavContainerHelper toolbox. When you create a container, it generates a folder containing all the base application objects. From there, you can locate and copy the files you need. In this case, the two files are:

  • CustomerTop10List.Report.al
  • CustomerTop10List.rdlc

You can also use the AL Object Browser extension in VS Code to find and inspect the report’s AL source code.

Step 2: Copy and Rename the Report

After copying both files into your project, Visual Studio Code picks them up immediately — but you’ll get compilation errors. Two things need to change:

  1. Renumber the object — The original report number (111) falls outside your extension’s ID range. You need to assign a number within your allocated range.
  2. Rename the object — You cannot have two reports with the same name, so give it a distinct name.

It’s also good practice to rename the RDLC layout file to match, so you don’t end up with naming conflicts when exporting objects. Then update the RDLCLayout property in the report to point to the renamed file.

Here’s the report header after renumbering and renaming:

report 54100 "Customer - Top 10 List (YT)"
{
    DefaultLayout = RDLC;
    RDLCLayout = './CustomerTop10List.yt.rdlc';
    Caption = 'Customer - Top 10 List (YT)';
    PreviewMode = PrintLayout;
    UsageCategory = None;
    ...

Notice that UsageCategory is set to None. This is critical — it prevents the custom report from appearing in the Tell Me search alongside the original. Without this, users would see two “Customer Top 10” reports and could get confused.

Step 3: Create the Report Substitution

The key mechanism that makes this work is the OnAfterSubstituteReport event in the ReportManagement codeunit. By subscribing to this event, you can intercept any call to the original report and redirect it to your custom version.

Create a new codeunit with an event subscriber:

codeunit 54100 "Sub Reports YT"
{
    [EventSubscriber(ObjectType::Codeunit, Codeunit::ReportManagement, 'OnAfterSubstituteReport', '', true, true)]
    local procedure MyProcedure(ReportId: Integer; var NewReportId: Integer)
    begin
        if ReportId = Report::"Customer - Top 10 List" then
            NewReportId := Report::"Customer - Top 10 List (YT)";
    end;
}

A few things to note about this code:

  • The NewReportId parameter is passed by reference (var), so assigning a new value to it effectively redirects the report call.
  • Instead of using the hard-coded report number 111, Erik references the report by name using Report::"Customer - Top 10 List". This way, if you ever rename your custom report object, the code still compiles correctly.
  • You could use the number 111 for the original report since Microsoft rarely renumbers standard reports, but using the name-based reference is considered good practice.

How It All Works Together

Once deployed, the substitution is active everywhere the original report is called — whether from the Tell Me search, from a page action, or from AL code that runs the report programmatically. When a user searches for “Customer Top 10,” they see only the original entry (since the custom report has UsageCategory = None), but when they run it, the substitution event fires and the custom report is executed instead.

The Full Report Object

For reference, here is the complete custom report object. At this point it’s identical to the original aside from the ID, name, layout reference, and usage category — but you now have full control to modify the dataset, request page, and layout however you need:

report 54100 "Customer - Top 10 List (YT)"
{
    DefaultLayout = RDLC;
    RDLCLayout = './CustomerTop10List.yt.rdlc';
    Caption = 'Customer - Top 10 List (YT)';
    PreviewMode = PrintLayout;
    UsageCategory = None;

    dataset
    {
        dataitem(Customer; Customer)
        {
            DataItemTableView = SORTING("No.");
            RequestFilterFields = "No.", "Customer Posting Group", "Currency Code", "Date Filter";

            trigger OnAfterGetRecord()
            begin
                Window.Update(1, "No.");
                CalcFields("Sales (LCY)", "Balance (LCY)");
                if ("Sales (LCY)" = 0) and ("Balance (LCY)" = 0) then
                    CurrReport.Skip();
                CustAmount.Init();
                CustAmount."Customer No." := "No.";
                if ShowType = ShowType::"Sales (LCY)" then begin
                    CustAmount."Amount (LCY)" := -"Sales (LCY)";
                    CustAmount."Amount 2 (LCY)" := -"Balance (LCY)";
                end else begin
                    CustAmount."Amount (LCY)" := -"Balance (LCY)";
                    CustAmount."Amount 2 (LCY)" := -"Sales (LCY)";
                end;
                CustAmount.Insert();
                if (NoOfRecordsToPrint = 0) or (i < NoOfRecordsToPrint) then
                    i := i + 1
                else begin
                    CustAmount.Find('+');
                    CustAmount.Delete();
                end;

                TotalSales += "Sales (LCY)";
                TotalBalance += "Balance (LCY)";
                ChartTypeNo := ChartType;
                ShowTypeNo := ShowType;
            end;

            trigger OnPreDataItem()
            begin
                Window.Open(Text000);
                i := 0;
                CustAmount.DeleteAll();
            end;
        }
        dataitem("Integer"; "Integer")
        {
            DataItemTableView = SORTING(Number) WHERE(Number = FILTER(1 ..));
            // ... columns and triggers omitted for brevity
        }
    }

    requestpage
    {
        SaveValues = true;
        layout
        {
            area(content)
            {
                group(Options)
                {
                    Caption = 'Options';
                    field(Show; ShowType) { ... }
                    field(NoOfRecordsToPrint; NoOfRecordsToPrint) { ... }
                    field(ChartType; ChartType) { ... }
                }
            }
        }
    }

    var
        CustAmount: Record "Customer Amount" temporary;
        Window: Dialog;
        CustFilter: Text;
        CustDateFilter: Text;
        ShowType: Option "Sales (LCY)","Balance (LCY)";
        NoOfRecordsToPrint: Integer;
        MaxAmount: Decimal;
        i: Integer;
        TotalSales: Decimal;
        TotalBalance: Decimal;
        ChartType: Option "Bar chart","Pie chart";
        // ... additional variables
}

Recap: The Process Step by Step

  1. Grab the report files — Get both the .al report object and the .rdlc (or Word) layout file from the NavContainerHelper folder or base application source.
  2. Copy them into your project — Place both files in your AL extension project folder.
  3. Renumber and rename — Assign an ID within your extension's range, give the report a unique name, rename the layout file, and update the layout reference.
  4. Set UsageCategory to None — Prevent the custom report from appearing in search results alongside the original.
  5. Create a substitution codeunit — Subscribe to OnAfterSubstituteReport in Codeunit::ReportManagement to redirect calls from the original report to your custom one.
  6. Deploy and test — Press F5 to publish, then verify that running the original report now executes your custom version.

Conclusion

While the lack of a report extension object in AL may seem like a limitation, the report substitution pattern provides a clean and effective way to customize standard reports in Business Central. By creating a copy of the original report, making your modifications, and wiring up the OnAfterSubstituteReport event, you get full control over both the dataset and the layout. This substitution works universally — whether the report is triggered from search, from a page action, or from code — ensuring a consistent experience for your users.