ELI5 Using a Report as an Email Body

This is one of the most fantastic ELI5 videos I have done. Cover multiple AL technologies, from reports, over temp blobs, streams and emails with a sprinkle of RecordRefs added. Check it out:


In this video, Erik walks through the complete process of using a Business Central report as the body of an email. While Business Central has many powerful components, figuring out how to combine them — emails, reports, streams, and record references — can seem daunting. Erik breaks it down step by step, showing that it only takes about nine lines of AL code to accomplish this.

Sending a Basic Email from AL

Before tackling reports, Erik starts with the fundamentals: how to send an email from AL code. The key building blocks are two codeunits from the System.Email namespace:

  • Codeunit “Email” — A code library that provides the Send function
  • Codeunit “Email Message” — An object that holds the email’s recipients, subject, body, and other data

The Email.Send method has four overloads, but the two most commonly used are:

  1. Send(EmailMessage) — Sends with default account selection
  2. Send(EmailMessage, EmailScenario) — Sends using a specific email scenario

Erik recommends always using the scenario-based overload. Email scenarios allow users to configure which email account is used for different purposes — for example, sales invoices go out from sales@yourcompany.com while reminders go from accounting@yourcompany.com. You configure this in Business Central under Email Accounts by assigning scenarios to specific accounts. The Email Scenario enum is also extensible, so if you’re building a custom app, you can create your own scenarios.

The EmailMessage.Create function is a multi-purpose call that sets up the recipient, subject, and body all at once. To send a simple text email, you only need two lines of code:

EmailMsg.Create('demo@hougaard.com', 'Invoice from ' + CompanyName(), 'Hello YouTube');
Email.Send(EmailMsg, "Email Scenario"::"Sales Invoice");

Sending HTML Email

The Create method has an additional overload with an HtmlFormatted boolean parameter. By passing true, you tell Business Central that the body contains HTML, and it will render it accordingly. For example, you can use <strong> tags to make text bold. This is the foundation for what comes next — injecting a full report as HTML into the email body.

Generating a Report as HTML

The core idea is to use Report.SaveAs to render a report into HTML format, then feed that HTML into the email body. This involves several components working together:

Selecting the Report

Rather than referencing a report by its ID number (e.g., 1306), Erik recommends using the symbolic reference:

Report::"Standard Sales - Invoice"

This approach is more resilient. If the report ever gets renumbered — something that can happen when reorganizing custom objects from the 50,000 range to the 70,000 range — your code won’t break.

Understanding Streams and Temp Blob

Report.SaveAs writes its output to an OutStream. But a stream is just a pipe — it needs something connected at the other end to collect the data. If you try to write to an uninitialized OutStream, you’ll get the error: “The current setting of the right mode properties is incompatible with the operation.”

The solution is Temp Blob — a codeunit that acts as an in-memory container for binary data. (The name comes from “Binary Large Object” prefixed with “Temporary.”) Think of Temp Blob as a tanker truck with an empty storage tank:

  1. You connect a hose (OutStream) to the tank so you can pump data into it
  2. The report generates its output and pumps it through the hose into the tank
  3. You then connect a different hose (InStream) to get data out of the tank
  4. You read the data from the InStream into a text variable
TempBlob.CreateOutStream(OutS);
Report.SaveAs(Report::"Standard Sales - Invoice", '', ReportFormat::Html, OutS, Ref);
TempBlob.CreateInStream(InS);
InS.ReadText(Body);

Filtering the Report with RecordRef

Report.SaveAs requires a RecordRef parameter to pass filters to the report. You can’t pass a strongly typed record variable directly — the compiler will tell you it can’t convert from Record "Sales Invoice Header" to RecordRef. A RecordRef is a generic variable that can point to a record from any table (similar to a pointer in other languages).

The process is:

  1. Find the invoice you want to print
  2. Set a filter on the record so only that single invoice is included
  3. Use RecordRef.GetTable to create a reference that carries the filter along with it
PostedSalesInvoiceHeader.FindLast();
PostedSalesInvoiceHeader.SetRange("No.", PostedSalesInvoiceHeader."No.");
Ref.GetTable(PostedSalesInvoiceHeader);

Note the difference between SetFilter and SetRange: SetFilter accepts a filter expression as a string, while SetRange accepts typed values directly. For setting an exact match, SetRange is the cleaner choice.

Choosing the Right Report Layout

Erik discovered that even though he specified ReportFormat::Html, the initial result came through as a PDF. This is because the report layout matters — RDLC layouts don’t produce clean HTML. To get proper HTML output, you need to use a Word layout. Erik went into Report Layouts in Business Central, selected the Word-based layout for the sales invoice, and set it as the default. After that change, the email body rendered as proper HTML.

The Complete Solution

Here’s the full code — nine lines that grab an invoice, render it as HTML, and send it as an email body:

namespace DefaultPublisher.ReportAsEmailBody;

using Microsoft.Sales.Customer;
using System.Email;
using System.Utilities;
using Microsoft.Sales.History;

pageextension 50100 CustomerListExt extends "Customer List"
{
    trigger OnOpenPage();
    var
        Email: Codeunit Email;
        EmailMsg: Codeunit "Email Message";
        TempBlob: Codeunit "Temp Blob";
        PostedSalesInvoiceHeader: Record "Sales Invoice Header";
        Body: Text;
        OutS: OutStream;
        InS: InStream;
        Ref: RecordRef;
    begin
        PostedSalesInvoiceHeader.FindLast();
        PostedSalesInvoiceHeader.SetRange("No.", PostedSalesInvoiceHeader."No.");
        Ref.GetTable(PostedSalesInvoiceHeader);
        TempBlob.CreateOutStream(OutS);
        Report.SaveAs(Report::"Standard Sales - Invoice", '', ReportFormat::Html, OutS, Ref);
        TempBlob.CreateInStream(InS);
        InS.ReadText(Body);
        EmailMsg.Create('demo@hougaard.com', 'Invoice from ' + CompanyName(), Body, true);
        Email.Send(EmailMsg, "Email Scenario"::"Sales Invoice");
    end;
}

Practical Application: Mail Merge Pattern

Erik shares a pattern he uses frequently in customer projects. Instead of hardcoding email content in an extension, he creates a mail merge approach:

  1. Create a dedicated “mail merge” table with the fields you need
  2. Create an empty report that runs on that table
  3. The customer designs the email body in Word using custom report layouts — no developer involvement needed
  4. At runtime, populate the merge fields in the table, run the report through the same SaveAs/Stream/Email pipeline, and send it

This gives end users full control over how their email bodies look, while the developer only needs to maintain the data pipeline and merge field definitions.

Summary

Sending a report as an email body in Business Central involves orchestrating several components — the Email codeunit, Email Message, Temp Blob, streams, RecordRef, and Report.SaveAs — but the end result is remarkably concise. The key concepts to remember are:

  • Use Email Scenarios so users can control which account sends what
  • Temp Blob is your in-memory storage container for binary data
  • OutStream writes data into Temp Blob; InStream reads it back out
  • RecordRef lets you pass filtered records to Report.SaveAs
  • You need a Word layout on your report to get proper HTML output
  • Reference reports by name (Report::"Standard Sales - Invoice") rather than by number for resilience