How to do Streams in Business Central

In this video I show four examples of how to use streams to get data in and out of blob fields:

https://youtu.be/JGCs58H37gg

In this video, Erik walks through the fundamentals of working with streams in AL code for Business Central. Starting with a conceptual explanation of what streams are, he then demonstrates practical examples: reading from and writing to Blob fields, importing files via upload, and exporting data via download — all using streams.

What Are Streams? The Pipe Analogy

Erik introduces streams using a simple but effective analogy: think of a stream as a unidirectional pipe. Water (data) flows in one direction — either into storage or out of storage. There are three key components to understand:

  • Storage — Where the data actually lives. This could be a Blob field, a Temp Blob, a .NET construct, an upload buffer, or something external.
  • Stream (the pipe) — The connection to that storage. Critically, the data does not live inside the stream. The stream is just the conduit. This is a common source of confusion.
  • Pump — The mechanism that moves data through the pipe. Pumps can be built into functions (like ReadText, WriteText, DownloadFromStream), or you can use the explicit CopyStream function when connecting two streams together.

Setting Up: Adding a Blob Field

Building on a previous project that had an HTML editor control on the Item Card, Erik replaces a simple text field with a Blob field. Blobs are ideal when the size of the data is unpredictable — they have virtually no size limit.

tableextension 52100 "Item Description" extends Item
{
    fields
    {
        field(52101; "Description Blob"; Blob)
        {
            Caption = 'Desc. Blob';
            DataClassification = ToBeClassified;
        }
    }
}

An important thing to know about Blob fields: they use lazy loading. When you retrieve a record, Blob fields are not automatically populated with data. You must explicitly call CalcFields before accessing their contents. This is a performance optimization — if you have megabytes of data in a Blob field, you only load it when you actually need it.

Reading from a Blob with InStream

To read data from a Blob field, you use an InStream. The process is:

  1. Call CalcFields on the Blob field to ensure its data is loaded.
  2. Create an InStream connected to the Blob field using CreateInStream.
  3. Use the built-in pump ReadText to extract the data into a text variable.
trigger OnAfterInit()
var
    InS: InStream;
    Txt: Text;
begin
    EditorReady := true;
    CalcFields("Description Blob");
    "Description Blob".CreateInStream(InS);
    InS.ReadText(Txt);
    CurrPage.EditCtl.Load(Txt);
    CurrPage.EditCtl.SetReadOnly(not CurrPage.Editable);
end;

Here, CreateInStream connects the pipe to the Blob storage, and ReadText is the built-in pump that pulls text data through that pipe. There’s also a Read function for binary data, but for text content ReadText is the appropriate choice.

Writing to a Blob with OutStream

To write data into a Blob field, you use an OutStream. The pattern mirrors the read operation:

  1. Call CalcFields on the Blob field.
  2. Create an OutStream connected to the Blob field using CreateOutStream.
  3. Use the built-in pump WriteText to push data into the Blob.
trigger SaveRequested(data: Text)
var
    OutS: OutStream;
begin
    CalcFields("Description Blob");
    "Description Blob".CreateOutStream(OutS);
    OutS.WriteText(data);
end;

So what was previously a single line assignment to a text field becomes three or four lines — but the tradeoff is unlimited storage capacity in the Blob field.

Importing a File Using UploadIntoStream and CopyStream

This is where things get interesting, because it demonstrates the CopyStream pump. When importing a file, the UploadIntoStream function provides an InStream connected to an external upload buffer — a temporary area in server memory where the uploaded file contents reside. But you want to store that data in a Blob field, which requires an OutStream.

Now you have two pipes: one reading from the upload buffer (InStream) and one writing to the Blob (OutStream). You need a pump to move data between them — that’s CopyStream.

action(ImportTxt)
{
    Caption = 'Import Text';
    ApplicationArea = all;
    trigger OnAction()
    var
        FileName: Text;
        InS: InStream;
        OutS: OutStream;
    begin
        if UploadIntoStream('', '', '', FileName, InS) then begin
            CalcFields("Description Blob");
            "Description Blob".CreateOutStream(OutS);
            CopyStream(OutS, InS);
            Modify(true);
            CurrPage.EditCtl.Init();
        end;
    end;
}

Note the parameter order of CopyStream: it’s OutStream first, then InStream. Erik suggests thinking of it like an assignment statement: OutStream = InStream — the destination comes first.

After copying the data, the record is modified to persist the change, and the HTML control is re-initialized to display the newly imported content.

Exporting a File Using DownloadFromStream

Exporting is simpler because DownloadFromStream has a built-in pump. You just need to provide it with an InStream connected to your Blob field, and it handles the rest:

action(ExportTxt)
{
    Caption = 'Export Text';
    ApplicationArea = all;
    trigger OnAction()
    var
        InS: InStream;
        FileName: Text;
    begin
        CalcFields("Description Blob");
        "Description Blob".CreateInStream(InS);
        FileName := Rec.Description + '.html';
        DownloadFromStream(InS, '', '', '', FileName);
    end;
}

The storage is the Blob field, the pipe is the InStream, and the pump is built into DownloadFromStream. The user gets a file download with a sensible filename constructed from the item’s description.

The Complete Page Extension

Here’s the full page extension bringing it all together — reading, writing, importing, and exporting with streams:

pageextension 52100 "Youtube Item Card" extends "Item Card"
{
    layout
    {
        addafter(Item)
        {
            usercontrol(EditCtl; Wysiwyg)
            {
                ApplicationArea = all;
                trigger ControlReady()
                begin
                    CurrPage.EditCtl.Init();
                end;

                trigger OnAfterInit()
                var
                    InS: InStream;
                    Txt: Text;
                begin
                    EditorReady := true;
                    CalcFields("Description Blob");
                    "Description Blob".CreateInStream(InS);
                    InS.ReadText(Txt);
                    CurrPage.EditCtl.Load(Txt);
                    CurrPage.EditCtl.SetReadOnly(not CurrPage.Editable);
                end;

                trigger ContentChanged()
                begin
                    CurrPage.EditCtl.RequestSave();
                end;

                trigger SaveRequested(data: Text)
                var
                    OutS: OutStream;
                begin
                    CalcFields("Description Blob");
                    "Description Blob".CreateOutStream(OutS);
                    OutS.WriteText(data);
                end;
            }
        }
    }
    actions
    {
        addlast(processing)
        {
            action(ImportTxt)
            {
                Caption = 'Import Text';
                ApplicationArea = all;
                trigger OnAction()
                var
                    FileName: Text;
                    InS: InStream;
                    OutS: OutStream;
                begin
                    if UploadIntoStream('', '', '', FileName, InS) then begin
                        CalcFields("Description Blob");
                        "Description Blob".CreateOutStream(OutS);
                        CopyStream(OutS, InS);
                        Modify(true);
                        CurrPage.EditCtl.Init();
                    end;
                end;
            }
            action(ExportTxt)
            {
                Caption = 'Export Text';
                ApplicationArea = all;
                trigger OnAction()
                var
                    InS: InStream;
                    FileName: Text;
                begin
                    CalcFields("Description Blob");
                    "Description Blob".CreateInStream(InS);
                    FileName := Rec.Description + '.html';
                    DownloadFromStream(InS, '', '', '', FileName);
                end;
            }
        }
    }
    trigger OnAfterGetRecord()
    begin
        if EditorReady then begin
            EditorReady := false;
            CurrPage.EditCtl.Init();
        end;
    end;

    var
        EditorReady: Boolean;
}

Summary

Streams in AL revolve around three core concepts:

  • Storage — Where data lives (Blob fields, Temp Blobs, upload buffers, external sources).
  • Streams — Unidirectional pipes that connect to storage. InStream reads data out of storage; OutStream writes data into storage. The stream itself holds no data.
  • Pumps — Move data through the pipes. Built-in pumps include ReadText/Read on InStream, WriteText/Write on OutStream, and functions like DownloadFromStream/UploadIntoStream. When you need to connect two streams (an InStream to an OutStream), use CopyStream.

Remember to always call CalcFields before working with Blob fields, and keep the pipe analogy in mind — it makes the directional confusion between InStream and OutStream much easier to reason about.