In this video, I go poking in dark places to figure out what’s new with AL for Microsoft Dynamics Business Central Version 17, aka. 2020 Wave 2.

Links to docs:
https://docs.microsoft.com/en-us/dynamics365/business-central/dev-itpro/developer/methods/devenv-commitbehavior-attribute https://docs.microsoft.com/en-us/dynamics365/business-central/dev-itpro/developer/methods-auto/record/record-setloadfields-method https://docs.microsoft.com/en-us/dynamics365/business-central/dev-itpro/developer/properties/devenv-tabletype-property https://docs.microsoft.com/en-us/dynamics365/business-central/dev-itpro/developer/devenv-temporary-tables
Found after I ended the recording:
https://docs.microsoft.com/en-us/dynamics365/business-central/dev-itpro/developer/devenv-protected-variables
In this video, Erik takes a deep dive into what’s actually new in AL for Business Central version 17 (2020 Wave 2) — but not by reading the official release notes. Instead, he takes a “hacker” approach: decompiling the AL compiler DLL (which is written in C#/.NET) and inspecting what features have been tagged with runtime version 6.0. The result? Several exciting features that weren’t prominently listed in the official “What’s New” documentation at the time of release.
The Official List vs. What’s Really There
Erik starts at the official docs.microsoft.com release plan for 2020 Wave 2, which lists features like:
- Azure Key Vault support for AppSource apps
- XML documentation comments
- Code region compiler directives (a community-submitted idea via BC Ideas)
- Delete extension data
- Obsolete attributes (implicit/explicit)
- Suppress AL warnings
- Business Central Performance Toolkit
Looking at this list, one might conclude there’s not much new in AL for version 17 — or as Erik notes, “runtime 6.0” (since NAV 2018 was runtime version 0, and we’re now at version 6). But Erik suspected there had to be more, so he decided to dig deeper.
The Hacker Approach: Decompiling the Compiler
Since the AL compiler is written in C# and ships as a .NET DLL, Erik ran it through decompilation tools like dotPeek to inspect the source. The key trick: searching for VersionCompatibility.Parse calls that reference version 6.0. Because the compiler knows which features belong to which runtime version (so it can compile for older targets), you can see exactly what’s been tagged as new for runtime 6.0.
He even spotted something tagged with version 6.1 — hinting at features coming in version 17.1.
CommitBehavior Attribute
One of the most exciting discoveries is the new CommitBehavior attribute. This allows you to control how Commit() calls are handled within the scope of a function.
The classic problem: you have code that needs to run as a single transaction, but somewhere in the middle you call a function that contains a Commit(). Previously, working around this was difficult — developers had to resort to hacks to avoid unwanted commits (like those in Codeunit 80 and 90 posting routines).
Now you can decorate your function with [CommitBehavior(CommitBehavior::Ignore)] to silently ignore any commits within its scope, or use CommitBehavior::Error to throw an error if a commit is attempted:
codeunit 50121 "Test Stuff in v17"
{
[CommitBehavior(CommitBehavior::Ignore)]
procedure testfunction()
var
GL : Record "G/L Entry";
begin
GL.SetLoadFields("My ID");
if gl.findset() then
repeat
until gl.next() = 0;
end;
procedure CommittingFunction()
begin
COmmit();
end;
}
In this example, if testfunction calls CommittingFunction, the Commit() inside will be silently ignored because the outer scope specifies CommitBehavior::Ignore. This means you can ensure that “stuff number one” and “stuff number two” remain in the same transaction, even if some intermediate function tries to commit.
SetLoadFields — Performance Optimization for Extension Fields
Another significant discovery is the SetLoadFields method on records. This addresses a fundamental performance challenge with the extension model.
The Problem
When you have a table with multiple extensions, that single logical table might be split across 10 different physical tables in SQL Server. Retrieving one record could mean the server has to join data from all those tables. Starting in version 17, the server tries to be smart about which fields to fetch — but if it guesses wrong and a field is needed later, it has to go back and fetch it, making the operation slower than if it had just joined everything upfront.
The Solution
SetLoadFields lets you tell the server exactly which fields you need before executing a query. This is particularly powerful when you’re working with fields from table extensions in high-performance processing loops.
First, here’s a simple table extension adding a custom field to the G/L Entry table:
tableextension 50100 "My GL Fields" extends "G/L Entry"
{
fields
{
field(50100; "My ID"; Text[100])
{
}
}
}
Then, in your processing code, you can specify that this field should be included in the initial data load:
GL.SetLoadFields("My ID");
if GL.FindSet() then
repeat
// Process records - "My ID" is already loaded, no extra round-trip needed
until GL.Next() = 0;
By calling SetLoadFields before FindSet(), you ensure the server includes your extension field in the initial query. Without this, the server might skip your extension’s table in the initial join, only to discover mid-loop that it needs to go back and fetch the data — resulting in potentially thousands of extra SQL round-trips.
TableType = Temporary
Erik also discovered a new option for the TableType property: Temporary. Previously, the known table types were Normal, CRM, ExternalSQL, Exchange, and Graph (plus the new CDS). Now you can mark a table itself as temporary at the table level.
table 50129 "Very Temporary Table"
{
Caption = 'Very Temporary Table';
DataClassification = ToBeClassified;
TableType = Temporary;
fields
{
field(1; Primary; Code[10])
{
Caption = 'Primary';
DataClassification = SystemMetadata;
}
field(2; Name; Text[100])
{
Caption = 'Naem';
DataClassification = SystemMetadata;
}
}
keys
{
key(PK; Primary)
{
Clustered = true;
}
}
}
And the corresponding page:
page 50129 "SHow the table"
{
ApplicationArea = All;
Caption = 'temporary table';
PageType = List;
SourceTable = "Very Temporary Table";
UsageCategory = Lists;
layout
{
area(content)
{
repeater(General)
{
field(Primary; Primary)
{
ApplicationArea = All;
}
field(Name; Name)
{
ApplicationArea = All;
}
}
}
}
}
According to the documentation, marking a table as TableType = Temporary is equivalent to setting all record variables to temporary in AL code and setting SourceTableTemporary on all pages that use the table. The table is never synchronized to the database — it exists only in memory on the Business Central server.
Important Caveats
Erik tested this and confirmed that each page instance gets its own temporary table scope — records entered on one page instance don’t appear on another. This is not a session-based temporary table. The data disappears when the page closes.
The primary use case is for tables that should never be persisted to the database. By marking the table itself as temporary, you prevent the scenario where someone forgets to mark a record variable as temporary and accidentally writes data to the database.
You can convert an existing Normal table to Temporary, but be careful: the physical table will be deleted from the database when you synchronize the extension.
Other Discoveries
During his exploration, Erik also noted several other changes tagged with runtime 6.0:
- System fields on RecordRef: All tables in version 17 now have
CreatedAt,CreatedBy,ModifiedAt, andModifiedBysystem fields, and you can access these through RecordRef using their internal field numbers. - SendTraceTag replaced by LogMessage: The telemetry function has been renamed.
- Preprocessor symbols in app.json: A new option in the app configuration file.
- Data migration deprecation: Features related to migrating data from v1 to v2 extensions are being deprecated.
- Runtime 6.1 preview: A feature called “generated locked transactions” appears to be coming in version 17.1.
For reference, the app.json used in this exploration targets runtime 6.0 and platform 17.0.0.0:
{
"id": "a6be809b-2c03-4c1c-b717-49a1a0b6f0ab",
"name": "Whats New in v17 Dev",
"publisher": "Default publisher",
"version": "1.0.0.0",
"platform": "17.0.0.0",
"application": "17.0.0.0",
"idRanges": [
{
"from": 50100,
"to": 50149
}
],
"showMyCode": true,
"runtime": "6.0"
}
Summary
The official “What’s New” list for version 17 might seem modest, but by decompiling the AL compiler and doing some detective work, Erik uncovered several significant features that weren’t prominently advertised:
- CommitBehavior attribute — Control or suppress commits within a function’s scope, solving a long-standing pain point with unwanted commits in called functions.
- SetLoadFields — Tell the server exactly which fields you need, dramatically improving performance in extension-heavy environments with processing loops.
- TableType = Temporary — Declare a table as permanently temporary at the table definition level, preventing accidental data persistence.
- New system fields — CreatedAt, CreatedBy, ModifiedAt, ModifiedBy now available on all tables.
- LogMessage replaces SendTraceTag — Updated telemetry API.
The approach of decompiling the compiler to discover undocumented features is a creative technique — and as Erik demonstrated, all these features do have documentation on docs.microsoft.com; they just weren’t highlighted in the release notes at the time. This “hacker edition” exploration reveals that version 17 is a more substantial release for AL developers than the official list might suggest.