Business Central Reports — Authoring Standards
Enforces correct and maintainable patterns when working with BC Report objects and their RDLC layouts.
Scope
In scope: AL report object structure, RDLC layout authoring, SetData/GetData index management, page size and body width validation, report rendering modes, BLOB image handling.
Out of scope: Report Extensions, Word/Excel layout authoring details, Telemetry/AppInsights instrumentation.
Input
The agent needs from the user:
- Action: Create new report, or modify an existing one?
- Source table(s): Which tables provide data (header/lines structure?)
- Rendering target: RDLC (default for documents), Word, or Excel
- Paper format: A4 Portrait (default), A4 Landscape, Letter
- Fields to add/modify: What columns/data items?
- RDLC section affected (for modifications): PageHeader, PageFooter, Body, or SetData group?
If the user does not specify, default to: new RDLC report, A4 Portrait, header-lines pattern.
Quick Start — Minimal Report Object
report 50100 "My Report"
{
UsageCategory = ReportsAndAnalysis;
ApplicationArea = All;
DefaultRenderingLayout = RDLCLayout;
dataset
{
dataitem(Header; "Sales Header")
{
column(DocumentNo; "No.") { }
dataitem(Lines; "Sales Line")
{
DataItemLink = "Document No." = field("No.");
DataItemTableView = sorting("Document No.", "Line No.");
column(LineNo; "Line No.") { }
column(Quantity; Quantity) { }
}
}
}
requestpage
{
SaveValues = true;
layout
{
area(content)
{
group(Options)
{
Caption = 'Options';
field(ShowDetails; ShowDetails)
{
ApplicationArea = All;
Caption = 'Show Details';
ToolTip = 'Specifies whether detail lines are printed.';
}
}
}
}
var
ShowDetails: Boolean;
}
rendering
{
layout(RDLCLayout)
{
Type = RDLC;
LayoutFile = 'src/reports/layouts/MyReport.rdlc';
}
}
}
Workflow — Creating or Modifying a Report
When creating a new report, follow this sequence:
- Write the AL object (Rules 1 and 5 below).
- Build or modify the RDLC layout.
- If adding/removing PageHeader or PageFooter fields → apply Rule 2. Read
references/rdlc-sizing-reference.mdNOW for the calculation. - If adding SetData fields → apply Rule 3. Read
references/setdata-index-guide.mdNOW — do not skip this. - If adding Tablix columns → apply Rule 4.
- Before committing → read and run
references/al-report-checklist.md— do not commit without passing all items.
When modifying an existing report, start at step 3, 4, or 5 depending on what changed, then always run step 6.
Rule 1 — AL Report Object Structure
- Always declare
UsageCategoryandApplicationAreaon the object. - Child DataItems must have explicit
DataItemLinkandDataItemTableViewwithsorting(...). - Always declare a
renderingblock with a named layout identifier. - Layout files live under
src/reports/layouts/. - Request page: always
SaveValues = true. Every field must haveApplicationArea,Caption, andToolTip.
Trigger reference
| Trigger | Purpose |
|---|---|
OnPreReport | Validate filters, initialize globals |
OnPostReport | Cleanup, final messages |
OnAfterGetRecord | Per-record logic; skip with CurrReport.Skip() |
OnPreDataItem | Set additional filters on the DataItem |
Rule 2 — RDLC PageHeader / PageFooter Height Sizing
When adding or removing fields in PageHeader or PageFooter, always recalculate and update <Height>.
New Height = Last field Top + Field Height + Bottom margin (≥ 0.167 cm)
Standard row spacing: 0.39 cm between rows | Standard row height: 11 pt ≈ 0.388 cm
❌ Never leave the old <Height> after adding a field — the new row will be clipped silently.
Read references/rdlc-sizing-reference.md for unit conversion tables, a worked calculation example, and the page width reference table.
Rule 3 — SetData Field Order: Always Append at End
CRITICAL — silent data corruption risk. RDLC uses Code.GetData(Index, Group) with 1-based sequential indices. Inserting a new field anywhere except the end shifts all subsequent indices, causing every downstream textbox to display the wrong field — no compile error, no runtime warning.
✅ Always append new fields at the end of the SetData expression.
❌ Never insert in the middle.
After appending, maintain an XML comment index map inside the RDLC:
<!--
SetData Group 2 — Field Index Map
1: CustomerName
2: DocumentDate
3: NewFieldLabel (added 2026-04-30)
4: NewFieldValue (added 2026-04-30)
-->
Read references/setdata-index-guide.md for the full step-by-step process, counting instructions, and textbox wiring examples.
Rule 4 — Body Width Validation
When adding Tablix columns, verify:
Body Width ≤ Page Width − Left Margin − Right Margin
Always read <LeftMargin> and <RightMargin> from the actual RDLC file before calculating — do not use default values.
If the body exceeds the limit, emit this warning and reduce column widths proportionally:
⚠️ WARNING: RDLC Body Width Exceeds Available Page Width
Available: {X} cm | Current Body: {Y} cm | Overflow: {Z} cm
Apply reduction factor = Available / Current to all TablixColumn widths.
Read references/rdlc-sizing-reference.md for the page width reference table and a proportional reduction worked example.
Rule 5 — Rendering Mode
| Rendering type | When to use | Known limitations |
|---|---|---|
RDLC | Pixel-precise layouts, banding, complex headers/footers | See Rules 2–4 |
Word | Simple documents, mail-merge style | No conditional formatting, no carry-overs between pages, totals cannot be retrieved from last record — avoid for complex documents |
Excel | Data exports for analysis | No complex formatting |
Default to RDLC for operational documents (invoices, orders, statements).
Default to Excel for data exports.
Rule 6 — BLOB Image Rendering
When displaying a company logo or any image stored as a BLOB field:
- Always call
CalcFieldson the BLOB field insideOnAfterGetRecordof the DataItem that owns it — not inOnPreReport. - Add the image column to the dataset with
IncludeCaption = false. - In the RDLC, set the image source to
=Fields!LogoField.ValueandMIMEType = "image/png"(or jpeg).
❌ Logo appears on page 1 only or shows as red X on subsequent pages: the BLOB is not recalculated per record. Move CalcFields into OnAfterGetRecord.
dataitem(CompanyInfo; "Company Information")
{
DataItemTableView = sorting("Primary Key");
trigger OnAfterGetRecord()
begin
CalcFields(Picture); // ✅ must be here, not in OnPreReport
end;
column(CompanyPicture; Picture) { IncludeCaption = false; }
}
Rule 7 — Preview vs. Print Layout Consistency
The most common RDLC complaint: report looks correct in Preview but differs when printed or switched to Print Layout mode.
Root causes and fixes:
| Symptom | Likely cause | Fix |
|---|---|---|
| Totals shift or repeat on every page | Wrong group scope for aggregate expression | Move expression into the correct Tablix group row; check Scope parameter of Sum() / Last() |
| Variable shows last value on all pages instead of per-page value | Using Last() at report level instead of group level | Scope the aggregate to the page group, or use a running value reset per page |
| Logo / image shows as red X when printing | BLOB not re-fetched after page break | See Rule 6 — CalcFields in OnAfterGetRecord |
| Controls misaligned in Print Layout | Top / Left values do not account for section height | Recalculate positions after every section height change (Rule 2) |
Always test in Print Layout mode, not only in Preview, before marking a report done.
Known Limitations
TotalPages with multiple documents (Page X of Y)
When printing multiple documents in one batch, Globals!TotalPages counts the total pages of all documents combined, not per document. There is no native fix in RDLC. Workarounds:
- Use a custom VB.NET function in the code block to track page count resets per document group.
- Accept the limitation and remove
TotalPagesfor batch prints.
Job Queue date errors
A report that runs correctly when launched manually may fail with "The date is not valid" when executed from the Job Queue. Cause: SQL datetime columns contain a time component that BC's Date type rejects.
Fix: cast the field explicitly in AL before passing to the report:
column(PostingDate; DT2Date(Rec."Posting Date")) { } // if stored as DateTime
Or filter out invalid dates in OnPreDataItem:
trigger OnPreDataItem()
begin
SetFilter("Posting Date", '>%1', 0D);
end;
Generic render error ("An error occurred while trying to display the report")
BC does not surface RDLC compile errors explicitly. Diagnostic approach:
- Remove recently added controls one by one and re-preview after each removal.
- Check for mismatched
GetDataindices (see Rule 3) — wrong index returns a type mismatch at render time. - Validate that all dataset column names referenced in the RDLC still exist in the AL report object.
- Check for empty
<Value>expressions or expressions referencing a deleted field.
Before Committing
Run references/al-report-checklist.md — covers both the AL object layer and the RDLC layout layer.
Behaviour Rules
-
Never modify an RDLC SetData expression without counting the existing indices first. If you cannot confirm the current field count, read the existing RDLC file before making changes.
-
Never add a field to PageHeader/PageFooter without recalculating
<Height>. Present the calculation to the user:Last Top + Height + margin = New Height. -
Always verify Body Width after adding any Tablix column. Emit the overflow warning if exceeded — do not silently leave an oversized layout.
-
If the user reports "report looks wrong when printed", apply the Rule 7 diagnostic table before proposing code changes. Ask which symptom matches.
-
Default to RDLC for any operational document. Only suggest Word/Excel if the user explicitly asks or the use case is a simple data export.
-
When modifying an existing report, preserve all existing SetData indices. New fields go at the end — this is non-negotiable.
-
Always recommend testing in Print Layout mode, not just Preview. Mention this when delivering a report modification.
-
If the user asks for
Page X of Yin a multi-document batch, warn about the known limitation immediately — before writing any code.