Quite often, we spend way more time battling issues that should be simple and straightforward because too many reports are incompatible with report extensions. Check out the video:

In this video, Erik demonstrates a common frustration that AL developers face when working with Report Extensions in Business Central: they often don’t work as expected in real-world scenarios. What should be a simple task — adding a field to an existing report’s dataset — turns into a complex, fragile workaround due to how legacy reports were originally designed.
The Task: Add Description 2 to the Sales Invoice
The scenario is straightforward. A customer uses the “Description 2” field on their sales invoice lines and wants it to appear on the printed invoice. They’ve even offered to handle the layout changes themselves — all they need is the field added to the report’s dataset. Sounds like a five-minute job, right?
The report in question is the Sales Invoice – North America (Report 10074). Erik starts by creating a report extension to extend it.
Setting Up the Report Extension with an Excel Layout
Rather than dealing with a full report layout just to inspect the dataset, Erik uses a handy trick: adding an Excel rendering layout to the report extension. This makes it easy to download and inspect the dataset in a spreadsheet.
reportextension 50100 "My Invoice" extends "Sales Invoice NA" // 10074
{
rendering
{
layout(Excel)
{
Caption = 'Data in Excel for testing';
Type = Excel;
LayoutFile = 'invoice.xlsx';
}
}
}
After compiling, this generates an invoice.xlsx file that can be used to view the raw data coming out of the report. The existing dataset includes fields like Description but not Description 2.
First Attempt: Adding a Column to the Sales Invoice Line
The natural first approach is to add a column directly to the Sales_Invoice_Line data item:
dataset
{
add(Sales_Invoice_Line)
{
column(Description_2; "Description 2") { }
}
}
After compiling and running the report, the result is five lines instead of two in the dataset. The new field appears, but each value ends up in its own separate row rather than being appended to the existing line data. This completely breaks the report output and is unusable — especially for Word layouts where you can’t perform post-processing on the data.
Second Attempt: Adding to the Integer-Based Data Item
Looking more closely at the report structure, Erik discovers there are actually two data items that deal with invoice lines. One is the actual Sales Invoice Line table, and the other is an Integer data item (called SalesInvLine) that loops over a temporary table built up during the report’s execution.
Trying to add the column to this Integer-based element and sourcing the value from the Sales Invoice Line record also fails — it returns the value from the last line for every row, since the record pointer isn’t being advanced in sync with the Integer loop.
Understanding the Report’s Internal Logic
Digging into the source code of the base report reveals the root cause. The report uses a flattening pattern:
- The
Sales Invoice Linedata item iterates over the actual invoice lines - For each line, the
OnAfterGetRecordtrigger inserts a record into a temporary table (TempSalesInvoiceLine) - A nested
Sales Comment Linedata item checks for comments and inserts additional temporary records - The
Sales Comment Linepre-data item always inserts a blank line (for spacing) - Another nested element handles sales line comments similarly
- Finally, the Integer data item loops over the temporary table using its count as the range
This flattening approach was designed for the old copy-loop pattern (printing multiple copies of invoices for email, filing, etc.) — a pattern that virtually no customers use today. But it means the actual data displayed comes from a temporary table that is inaccessible from a report extension due to its protection level.
The Workaround: Mirroring the Internal Logic with a List
Since the temporary table can’t be accessed directly, Erik builds a parallel collection — a List of [Text] — that mirrors exactly what Microsoft’s code does when building up the temporary table. Every time the base report inserts a record into its temp table, the extension adds the corresponding Description 2 value (or a blank) to the list.
reportextension 50100 "My Invoice" extends "Sales Invoice NA" // 10074
{
dataset
{
modify(Sales_Invoice_Header)
{
trigger OnAfterAfterGetRecord()
begin
Clear(D2List);
end;
}
modify(Sales_Invoice_Line)
{
trigger OnAfterAfterGetRecord()
begin
D2List.Add("Sales Invoice Line"."Description 2");
end;
}
modify(Sales_Comment_Line)
{
trigger OnBeforePreDataItem()
begin
D2List.Add('');
end;
}
modify(Sales_Line_Comments)
{
trigger OnAfterAfterGetRecord()
begin
D2List.Add('');
end;
}
modify(SalesInvLine)
{
trigger OnAfterAfterGetRecord()
begin
DescriptionTwo := D2List.Get(Number);
end;
}
add(SalesInvLine)
{
column(DescriptionTwo; DescriptionTwo) { }
}
}
rendering
{
layout(Excel)
{
Caption = 'Data in Excel for testing';
Type = Excel;
LayoutFile = 'invoice.xlsx';
}
}
var
D2List: List of [Text];
DescriptionTwo: Text;
}
The approach works as follows:
- When a new invoice header is processed, the list is cleared
- For each sales invoice line, the Description 2 value is added to the list
- For comment lines and blank spacer lines, an empty string is added to keep the list in sync
- When the Integer data item loops through, the current
Numbervalue is used as an index to retrieve the correct Description 2 value from the list
After setting a breakpoint and verifying the data, Erik confirms that the list contains three entries (two lines plus one blank spacer) matching the three rows in the Integer loop. The downloaded Excel file shows the Description 2 values correctly aligned with their respective invoice lines.
Why This Solution Is Problematic
While the workaround does produce the correct result, Erik is quick to point out its serious drawbacks:
- Fragility: The solution is entirely dependent on mirroring Microsoft’s internal logic. If Microsoft adds another place that inserts into the temporary table, the list will be out of sync and the data will be wrong.
- Complexity: What should have been a simple column addition turned into a half-hour investigation requiring deep knowledge of the report’s internals.
- Maintainability: This is not code anyone would be proud to ship. It’s a hack born of necessity.
- Scalability: If you needed multiple additional fields, you’d need to switch from a simple list to a temporary table of your own, adding even more complexity.
The Root Cause
The fundamental issue is that the vast majority of reports in Business Central were designed long before report extensions existed — and certainly before AL was even a thing. These reports use patterns like temporary table flattening and copy loops that are inherently hostile to extension. The temporary variables are protected and inaccessible, the data flow is indirect, and there are no clean extension points.
The alternative — copying the entire report object — means inheriting and maintaining all of Microsoft’s code, which is equally undesirable.
Conclusion
Report extensions in AL do work in principle, but when the underlying report wasn’t designed with extensibility in mind, even trivial changes become disproportionately difficult. Erik’s Christmas wish is for Microsoft to invest in refactoring these legacy reports to be report-extension-friendly. Until that happens, developers are stuck choosing between fragile workarounds like the one demonstrated here, or taking full copies of report objects they’d rather not maintain. If you’ve found a cleaner approach to this specific problem, Erik would love to hear about it in the comments.