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

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 explicitCopyStreamfunction 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:
- Call
CalcFieldson the Blob field to ensure its data is loaded. - Create an InStream connected to the Blob field using
CreateInStream. - Use the built-in pump
ReadTextto 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:
- Call
CalcFieldson the Blob field. - Create an OutStream connected to the Blob field using
CreateOutStream. - Use the built-in pump
WriteTextto 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/Readon InStream,WriteText/Writeon OutStream, and functions likeDownloadFromStream/UploadIntoStream. When you need to connect two streams (an InStream to an OutStream), useCopyStream.
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.