Print a PDF from Business Central, that’s easy, isn’t it?

A viewer asked how to print a PDF from Business Central, that should be easy right? Or maybe not? Check this video for a nice hack on how to do this:

https://youtu.be/J4T40tk44MI

In this video, Erik tackles a deceptively simple question from a subscriber: How do you print a PDF from Business Central? What seems straightforward at first turns out to require a clever hack — a “bait and switch” technique that leverages report management events, a dummy report, and the SingleInstance pattern to inject a PDF into Business Central’s print pipeline. The entire solution clocks in at under 50 lines of code.

The Problem: Printing a PDF Isn’t Built-In

Erik’s initial reaction to the subscriber’s question was to reply “sorry, that’s not possible from within AL.” But before hitting send, he paused and thought — maybe there’s a hack. Business Central exposes parts of the report processing pipeline through events, and Erik suspected he could exploit that to sneak a PDF into the print stream.

The Bait and Switch Concept

For those unfamiliar with the term, “bait and switch” is when you’re lured in by one offer, only to have it swapped for something else. Erik applies the same idea here:

  1. The Bait: A dummy report that Business Central is happy to “print” — it has a dataset, a layout, and produces output
  2. The Switch: At the last moment, using an event subscriber, the dummy report’s output is replaced with the actual PDF you want to print

Key Building Blocks

Report Management Events

Business Central has a codeunit called ReportManagement that exposes several events related to document processing:

  • OnAfterDocumentDownload
  • OnAfterDocumentPrintReady
  • OnAfterDocumentReady
  • OnAfterGetPaperTrayForReport
  • OnCustomDocumentMergerEx (used by Erik’s Simple Report Designer app)

The critical one for this solution is OnAfterDocumentReady, because it provides an OutStream (the TargetStream) — this is the binary stream where we can feed in our own content. It also has a Success boolean we can set to tell Business Central the document is ready.

The SingleInstance Pattern

A codeunit marked with SingleInstance = true behaves differently from normal codeunits. Instead of each variable reference creating a new instance, a single instance is created in memory and shared across all references within the same session. This means data stored in the codeunit from one location (the report’s OnPreReport trigger) is accessible from another location (the event subscriber) — effectively acting as shared memory.

The Solution

The Bait Report

First, Erik creates a minimal report that Business Central will actually process. It needs a dataset (otherwise BC decides there’s nothing to print), a layout, and at least one column:

report 50100 "Print PDF"
{
    UsageCategory = ReportsAndAnalysis;
    ApplicationArea = all;
    DefaultLayout = Word;
    WordLayout = 'bait.docx';

    dataset
    {
        dataitem(Integer; Integer)
        {
            DataItemTableView = where(Number = const(17));
            column(Number; Number) { }
        }
    }
    trigger OnPreReport()
    var
        PrintPDF: Codeunit "Print PDF";
    begin
        if UploadIntoStream('', InS) then
            PrintPDF.PDFToPrint(InS);
    end;

    var
        InS: InStream;
}

The dataset uses the Integer virtual table filtered to a single record (number 17, which Erik calls “the most random number in existence”). The Word layout file (bait.docx) contains placeholder content — it doesn’t matter what’s in it because the output will be replaced.

The key action happens in OnPreReport: the user is prompted to upload a PDF file via UploadIntoStream, and the resulting InStream is passed to the Print PDF codeunit’s PDFToPrint procedure. Note that the local variable InS is kept in the report — this is important because it maintains a reference to the uploaded stream data so it doesn’t get garbage collected.

The Print PDF Codeunit (The Switch)

This is where the magic happens:

codeunit 50100 "Print PDF"
{
    SingleInstance = true;

    var
        _InS: InStream;

    procedure PDFToPrint(var InS: InStream)
    begin
        _InS := InS;
    end;

    [EventSubscriber(ObjectType::Codeunit, Codeunit::ReportManagement, 'OnAfterDocumentReady', '', true, true)]
    local procedure OnAfterDocumentReady(ObjectId: Integer; var TargetStream: OutStream; var Success: Boolean)
    begin
        if ObjectId = Report::"Print PDF" then begin
            CopyStream(TargetStream, _InS);
            Success := true;
        end;
    end;
}

Let’s break down what’s happening:

  • SingleInstance = true — ensures only one instance of this codeunit exists in memory. When the report’s OnPreReport calls PDFToPrint, and when the event subscriber fires, they’re working with the same instance and therefore the same _InS variable.
  • PDFToPrint — receives the InStream reference from the uploaded PDF and stores it in the instance variable _InS.
  • OnAfterDocumentReady — the event subscriber checks if the report being processed is our bait report (Report::"Print PDF"). If it is, it performs the switch: it uses CopyStream to copy the PDF data from _InS into the TargetStream, and sets Success := true to tell Business Central the document is ready.

Understanding CopyStream

The CopyStream function takes two parameters: the destination stream first, then the source stream. As Erik notes, this follows the old C convention for parameter ordering (destination, source) — a pattern that dates back to functions like memcpy and strcpy.

The Flow in Action

  1. The user runs or prints the “Print PDF” report
  2. OnPreReport fires, prompting the user to upload a PDF file
  3. The uploaded PDF’s InStream is passed to the SingleInstance codeunit and stored
  4. Business Central processes the bait report and generates its (irrelevant) output
  5. The OnAfterDocumentReady event fires
  6. The event subscriber detects it’s the bait report, replaces the output stream with the uploaded PDF, and signals success
  7. Business Central prints/previews/emails the PDF as if it were normal report output

An Interesting Discovery Along the Way

During development, Erik attempted an optimization: instead of using the TempBlob pattern he initially tried, he wanted to simply store a second reference to the InStream directly. The idea was that since .NET (which underpins the Business Central server) uses reference-based garbage collection, as long as there’s a reference to the stream data, it should stay in memory. However, this approach didn’t work as expected — the stream wasn’t properly maintained when only a secondary reference was stored in the codeunit. The solution was to keep the primary InStream variable in the report itself (as the InS variable) while passing it by reference to the codeunit.

Why This Matters

Once the PDF is in the report pipeline, it can be connected to any output that Business Central supports:

  • Universal Print — Microsoft’s cloud printing service
  • Print Node — third-party print routing
  • Email — send the PDF via email through BC’s email functionality
  • Any other printer or output method configured in Business Central

The PDF source doesn’t have to be a file upload either. It could come from a SharePoint connector, a web service call, a document attachment, or any other source that can provide an InStream.

Summary

Printing an arbitrary PDF from Business Central is not a built-in capability, but with under 50 lines of AL code and a clever “bait and switch” technique, it becomes surprisingly straightforward. The solution combines three key concepts: a dummy report to act as the bait, the OnAfterDocumentReady event from ReportManagement to perform the switch, and the SingleInstance codeunit pattern to share the PDF data between the report trigger and the event subscriber. It’s a creative hack that opens up practical possibilities for printing PDFs from any source directly through Business Central’s standard print infrastructure.