What’s up with streams and positions? In this video, I take a deep dive into stream and how the position functionality works. Check out the video:

In this video, Erik explores a topic that often confuses AL developers: stream positions. Inspired by a mishap where Claude (the AI) got stream handling wrong, Erik digs into how positions work in streams, when they’re available, and what happens to the position as you read, write, and copy data.
What Is a Stream?
A stream is, as the name suggests, a flow of data. Think of a camera streaming video to recording software — data continuously flows from the sensor, gets encoded, and is written to a file. In AL, streams work the same way: you receive or send data through a stream.
There are two types of streams in AL:
- InStream — something you read from
- OutStream — something you write to
Erik notes that the naming conventions around streams in AL are not coherent — the names of functions and parameters can be confusing, though that’s a topic for another day.
Do Streams Always Have a Position?
Here’s the key insight: the position capability doesn’t belong to the stream itself — it belongs to whatever container of data the stream is connected to.
Consider Erik’s camera analogy: you can’t tell the camera to “go back and give me the data from 5 minutes ago.” The camera isn’t storing anything; it’s just streaming data as it comes off the sensor. There’s no position in that kind of stream because you’re just grabbing the next data continuously.
But when a stream is connected to a Blob (which stores data in memory), you can query and set the position, check the length, and even seek backwards.
Working with Stream Positions in Practice
Writing Data to a Blob
To put data into a Blob, you create an OutStream and write to it. Erik also noticed a new syntax feature — you can now use a return-value style for CreateOutStream:
// Traditional syntax
b.CreateOutStream(OutStream);
// New overload syntax
OutStream := b.CreateOutStream();
Erik fills the Blob with 1,000 repetitions of “Hello YouTube” (14 characters each), resulting in 14,000 bytes of data:
b.CreateOutStream(OutStream);
for i := 1 to 1000 do
OutStream.Write('Hello YouTube ');
Reading the Position
After creating an InStream from the Blob, the initial position is 1:
InStream := b.CreateInStream();
Message('Position: %1', InStream.Position); // Position: 1
Seeking to a Specific Position
Because the underlying data container is a Blob (which supports random access), you can set the position and read from any point in the data:
InStream.Position := 7; // Move to position 7 (the "Y" in "YouTube")
InStream.Read(T, 7); // Read 7 characters
Message('Data: %1', T); // Data: YouTube
You can also jump back to the beginning:
InStream.Position := 1;
InStream.Read(T, 7);
Message('Data: %1', T); // Data: Hello Y
Position Property: Getter and Setter Syntax
Erik points out an AL syntax peculiarity — the position property works both as a getter and a setter using the same pseudo-procedure style. These two lines are equivalent:
InStream.Position(7); // Set position to 7
InStream.Position := 7; // Same thing, different syntax
How Read Affects Position
When you read data, the position automatically advances. If your target variable is only 4 characters wide, the read operation is smart enough to read only 4 bytes:
InStream.Position := 7;
// T is declared as Text[4]
InStream.Read(T);
Message('Data: %1, Position: %2', T, InStream.Position);
// Data: YouT, Position: 11
The read started at position 7 and advanced by 4, landing at position 11.
CopyStream and Position
The CopyStream function copies data from an InStream to an OutStream. The parameter order can be confusing — think of it as an assignment statement:
// Think of it as: OutStream := InStream
CopyStream(OutStream, InStream);
It’s to and from, not from and to.
After a CopyStream operation, the InStream’s position moves past the end of the data. With 14,000 characters in the Blob, the position becomes 14,001:
CopyStream(OutStream, InStream);
Message('Position: %1', InStream.Position); // Position: 14001
This position is technically invalid — it’s past the last byte of data. At this point, InStream.EOS (End of Stream) returns true.
You can see this pattern in the source code for the Item Card extension’s import action, which uses CopyStream to transfer uploaded file data into a Blob field:
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;
}
End of Stream vs. No More Data (Yet)
For a Blob-backed stream, once you hit end of stream, you’re done — the data is finite and fully stored in memory. But for streams connected to external sources (like a device or network connection), end of stream might be temporary. You could check EOS, find it’s true, then check again later and find more data has arrived, making EOS return false again. In such cases, there may not even be a meaningful length or position.
Real-World Example: Blob Streams in Action
The source code demonstrates a practical use of streams with Blobs. The table extension adds a Blob field to the Item table:
tableextension 52100 "Item Description" extends Item
{
fields
{
field(52101; "Description Blob"; Blob)
{
Caption = 'Desc. Blob';
DataClassification = ToBeClassified;
}
}
}
And the page extension shows the common pattern of reading from and writing to Blob fields using streams — creating an InStream to load data and an OutStream to save it:
// Reading from a Blob
CalcFields("Description Blob");
"Description Blob".CreateInStream(InS);
InS.ReadText(Txt);
// Writing to a Blob
CalcFields("Description Blob");
"Description Blob".CreateOutStream(OutS);
OutS.WriteText(data);
Summary
Stream positions in AL can be a source of confusion, but the key takeaways are:
- Position is a capability of the data container, not the stream itself. Blobs support positions; live data sources may not.
- Reading advances the position by the number of bytes read.
- You can seek backwards on Blob-backed streams by setting the position.
- After reading all data (or after
CopyStream), the position moves one past the end, andEOSreturnstrue. CopyStreamparameters are in “to, from” order — think of it like an assignment:OutStream := InStream.- The Read function respects the size of the target variable, reading only as many bytes as the variable can hold.