What’s your position on positions in streams?

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:

https://youtu.be/i5FHehFzFGE

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, and EOS returns true.
  • CopyStream parameters 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.