AL Language extension 18.0
AL Language extension 18.0 for Business Central 2026 release wave 2 (BC29): previewed 2026-03-06, released 2026-09-09; 38 changelog entries, 25 GitHub issues linked.
- BC29Business Central
- 2026-03-06preview
- 2026-09-09released
- 38entries
- 25issues
Changelog on the Visual Studio Marketplace · Business Central 2026 release wave 2 · BC29
What changed on this page
- 2026-10-11: added “Snapshot debugging AL tests”
- 2026-10-11: changed “GitHub issues”
- 2026-10-11: removed “inherent-permissions-validation-in-multi-root-workspaces”
- 2026-10-11: removed “miscellaneous-2”
Share packaged resources between apps
Apps can now expose selected packaged resources to other apps. Add private resources to resourceFolders and shared resources to publicResourceFolders in app.json:
{
"resourceFolders": [
".resources"
],
"publicResourceFolders": [
".publicResources"
]
}
Successful builds list every packaged public resource in both the ALC console and the Visual Studio Code output:
Public resources included in the package (2):
schemas/customer.json
templates/invoice.txt
The existing NavApp overloads without an app ID continue to operate in the current app and can access both private and public resources:
AllOwnResources := NavApp.ListResources();
PrivateConfiguration := NavApp.GetResourceAsJson('private-config.json');
The new app ID overloads cross the app boundary and can access only resources declared public by the provider:
PublicResources := NavApp.ListResources(ProviderAppId, 'schemas/*.json');
CustomerSchema := NavApp.GetResourceAsJson(
'schemas/customer.json',
ProviderAppId,
TextEncoding::UTF8);
Apps can also discover providers that expose a matching public resource and then enumerate each provider:
ProviderAppIds := NavApp.ListAppsWithPublicResources('schemas/*.json');
foreach ProviderAppId in ProviderAppIds do begin
PublicResources := NavApp.ListResources(ProviderAppId, 'schemas/*.json');
foreach ResourceName in PublicResources do
ProcessSchema(
ProviderAppId,
ResourceName,
NavApp.GetResourceAsJson(ResourceName, ProviderAppId));
end;
NavApp.ListResources() returns the current app’s combined private and public resources. NavApp.ListResources(AppId) is always public-only, even when AppId is the current app. Attempts to retrieve a private resource through an app ID return the same not-found error as a resource that does not exist.
Query ReadState supports ReadCommitted
AL queries can now use ReadState = ReadCommitted to read only committed data without guaranteeing that rows remain consistent throughout the transaction.
Search symbols installed on the connected environment with al_symbolsearch
The al_symbolsearch language model tool can now search the AL objects installed on the connected Business Central environment, not just your local project and downloaded symbols. Pass filters.source = "environment" to search the objects actually present on the tenant (Base App, System App, and installed apps / per-tenant extensions). Each match includes its owning app (appId, appName, appPublisher, appVersion) so an agent can add the missing app.json dependency and download that app’s symbols before using the object. When a local (workspace) search finds nothing, the tool now hints that the object may be available on the connected environment.
Agents in Business Central move to GPT-5.3-chat
From May through June, Agents in Business Central are upgrading to GPT-5.3-chat as the default model. This update rolls out gradually to environments across all regions.
What’s changing:
- New default model — GPT-5.3-chat replaces the previous model, bringing improved quality and performance for agents in Business Central.
- Model control for coded agents — New methods when coding agents let you control model selection programmatically from AL.
- Model visibility and selection in agent design experience - Administrators can see which language model their agents use and select their preferred model from the available options in the agent configuration page.
What to expect during rollout:
The model update and the new UI for model selection roll out independently. Your environment might receive GPT-5.3-chat before the model selection UI and new methods become available. During this period, agents continue to work as expected with the new model.
Environments in the UK, India, and Australia will receive the update at a later date.
If you want to switch models for your designed agents after the selection UI becomes available, you can do so from the Agents page in Business Central. For coded agents, use the new model selection methods to control which model your agent uses. For those of you having started on the journey of designing and coding agents, then we highly recommend evaluating your agents again once the model has updated, as agent behavior, as well as accuracy, might be impacted.
Audit how your app is reached across boundaries with al graph
The al command-line tool gains a new graph command group that helps you audit how the code in your apps is reachable across accessibility and debuggability boundaries. Build a static call graph across your extension and its dependencies, then ask questions such as: which of my internal APIs are reachable from a public surface, which public entry points expose a specific internal implementation, or what [NonDebuggable] code can be reached from the outside.
First build a graph from source (compiling the apps together so cross-app calls resolve):
al graph extract-whole --corpus C:\src\MyApp --corpus C:\src\MyDependencies --out .\shards --graph .\g.jsonl
Then query it for the boundary you care about.
How is my internal API reached from public entry points? (internal ← public call transitions)
al graph query --graph .\g.jsonl --from access:public --to "access:internal+app:<my-app-guid>" --paths
What is the first public surface that exposes a specific internal target? — the outermost entry points a caller could actually hit:
al graph query --graph .\g.jsonl --to "obj:Codeunit/My Impl#DoSensitive" --frontier access:public
What code that can’t be inspected in the debugger is reachable from a public surface?
debuggable is the single end-effect concept — can a method be stepped into in the AL debugger and have its parameters, locals, and return value inspected? A method is not debuggable (!debuggable) for any of three reasons: its app didn’t opt into debugging (resourceExposurePolicy.allowDebugging, default false), the method is marked [NonDebuggable], or it’s a platform built-in. (Debuggability is independent of cloud/OnPrem scope.)
al graph query --graph .\g.jsonl --from access:public --to "!debuggable+app:<my-app-guid>" --paths
Where does non-debuggable code hand data to code you can debug?
A secret handled inside non-debuggable code stays out of the debugger — until it crosses into a method you can step through, where its value (as a parameter or a return value) becomes inspectable. That crossing — code you can’t inspect flowing into code you can — is expressed directly:
al graph query --graph .\g.jsonl --from !debuggable --to debuggable --paths
For the first place execution becomes inspectable on each path (the nearest debuggable frontier), use --frontier:
al graph query --graph .\g.jsonl --from !debuggable --frontier debuggable
Scope it to a single object to review one boundary at a time:
al graph query --graph .\g.jsonl --from "obj:Codeunit/My Secret Handler+!debuggable" --to "obj:Codeunit/My Secret Handler+debuggable" --paths
Add --format sarif on al graph export to turn any such query into a clickable code-flow (see below).
Selectors combine with , (OR), + (AND) and ! / not: (negate a single term) over primitives such as access:, scope:, kind:, ns:, obj:, app: — plus the named aliases debuggable (the end-effect above), onprem-surface, and elevation. onprem-surface is the OnPrem-gated boundary, broader than scope:onprem because it also covers OnPrem Scope tables (which compile under Cloud scope). Add --exclude "ns:*Test*" to drop test code. Use al graph export --format dgml|graphml|sarif --from <selector> --to <selector> to render any query as an interactive graph in Visual Studio or a clickable SARIF code-flow. Exported nodes carry their source location, so you can jump straight from the graph to the .al file — SARIF steps open at the exact line in Visual Studio or the VS Code SARIF viewer.
New language features
IsolatedStorage.Get with read isolation level
IsolatedStorage.Get now accepts a read isolation level that controls how the key row is locked during the read. For example, IsolationLevel::UpdLock locks the row so it can be safely read, modified, and written back within the same transaction. This feature requires runtime version 18.0 or later.
if IsolatedStorage.Get(Key, DataScope::Module, IsolationLevel::UpdLock, Value) then
IsolatedStorage.Set(Key, NewValue, DataScope::Module);
TestHandlers — lifecycle hooks for test codeunits
The new TestHandlers property on test codeunits lets you attach handler codeunits that receive callbacks at every stage of a test run. Use them for cross-cutting concerns such as logging, database cleanup, performance measurement, or custom test reporting — without modifying each test codeunit individually.
Overview
The feature introduces:
| Element | Description |
|---|---|
ITestHandler interface |
Defines six lifecycle callbacks that handler codeunits must implement. |
TestHandler system enum (ID 2000000023) |
Extensible registry where you map enum values to handler codeunit implementations. |
DefaultTestHandler system enum (ID 2000000024) |
Like TestHandler, but handlers registered here run automatically for every test codeunit without being listed in TestHandlers. |
TestHandlers codeunit property |
Comma-separated list of TestHandler enum value names. Only valid on codeunits with Subtype = Test. |
TestHandlerContext type |
Read-only context object passed to every callback with information about the running test. |
Step 1 — Implement a handler codeunit
Create a codeunit that implements ITestHandler. All six procedures have default (empty) implementations, so you only need to override the ones you care about:
codeunit 50100 MyTestLogger implements ITestHandler
{
procedure OnBeforeTestCodeunitRun(Context: TestHandlerContext)
begin
Message('Starting codeunit %1 (%2)', Context.CodeunitId, Context.CodeunitName);
end;
procedure OnAfterTestCodeunitRun(Context: TestHandlerContext)
begin
Message('Finished codeunit %1, Success = %2', Context.CodeunitId, Context.Success);
end;
procedure OnBeforeTestProcedureRun(Context: TestHandlerContext)
begin
Message(' Starting %1', Context.ProcedureName);
end;
procedure OnAfterTestProcedureRun(Context: TestHandlerContext)
begin
Message(' Finished %1, Success = %2', Context.ProcedureName, Context.Success);
end;
procedure OnBeforeTestCaseRun(Context: TestHandlerContext)
begin
Message(' Case %1', Context.TestCaseName);
end;
procedure OnAfterTestCaseRun(Context: TestHandlerContext)
begin
Message(' Case %1, Success = %2', Context.TestCaseName, Context.Success);
end;
}
Step 2 — Register the handler in the TestHandler enum
Extend the system TestHandler enum and map a value to your implementation codeunit:
enumextension 50100 MyTestHandlers extends TestHandler
{
value(50100; MyLogger)
{
Implementation = ITestHandler = MyTestLogger;
}
}
Step 3 — Subscribe a test codeunit
Set the TestHandlers property on any test codeunit. Multiple handlers are separated by commas and are invoked in the order listed:
codeunit 50200 MyTests
{
Subtype = Test;
TestHandlers = MyLogger;
[Test]
procedure TestSomething()
begin
// ...
end;
}
Default handlers
To run a handler for all test codeunits automatically (without listing it in every TestHandlers property), extend DefaultTestHandler instead:
enumextension 50101 MyDefaultHandlers extends DefaultTestHandler
{
value(50101; GlobalLogger)
{
Implementation = ITestHandler = MyGlobalLogger;
}
}
Default handlers run before any codeunit-specific handlers.
Callback invocation order
For a test codeunit with two test procedures where the second uses [TestDataSource] with two cases, and one handler registered, the callbacks fire in this order:
OnBeforeTestCodeunitRun — once, before any procedure
OnBeforeTestProcedureRun — before procedure 1
OnAfterTestProcedureRun — after procedure 1
OnBeforeTestProcedureRun — before procedure 2
OnBeforeTestCaseRun — before case A of procedure 2
OnAfterTestCaseRun — after case A of procedure 2
OnBeforeTestCaseRun — before case B of procedure 2
OnAfterTestCaseRun — after case B of procedure 2
OnAfterTestProcedureRun — after procedure 2
OnAfterTestCodeunitRun — once, after all procedures
OnBeforeTestCaseRun / OnAfterTestCaseRun are only invoked for procedures that use the [TestDataSource] attribute for data-driven testing.
TestHandlerContext members
| Member | Type | Description |
|---|---|---|
CodeunitId |
Integer |
The ID of the test codeunit being run. Always populated. |
CodeunitName |
Text |
The fully qualified name of the test codeunit. Always populated. |
ProcedureName |
Text |
The name of the test procedure. Empty in codeunit-level callbacks (OnBeforeTestCodeunitRun, OnAfterTestCodeunitRun). |
Success |
Boolean |
Whether the test completed successfully. Only meaningful in OnAfter* callbacks; always false in OnBefore* callbacks. |
TestCaseName |
Text |
The name of the data-driven test case. Only populated in OnBeforeTestCaseRun and OnAfterTestCaseRun. |
Translations with namespaces
We have added a new compiler feature flag, TranslationsWithNamespaces, that includes the object’s namespace in translation IDs to avoid collisions between objects with the same name. When enabled, the translation ID format also changes so the object and property names are visible.
If a readable translation ID would contain non-ASCII characters, the compiler now uses the hashed format so generated translation IDs remain ASCII-only.
The new Ids:
Namespace AL.ExampleNamespace - Page SamplePage - Property Caption
The previous Ids:
Page 1066360047 - Property 2879900210
Improvements to Agentic development
- New AL0926 diagnostic to help coding agents (and developers) that have unintentionally mixed up the order of sections in an object by, for example, putting the
fieldgroupssection before thekeyssection on a table object. The current diagnostics for this case are an AL0104 (‘}’ expected) and an AL0198 (Expected one of the application object keywords) in combination, which might not give full transparency into solving the issue.
GitHub issues
- #8276 Fixed slow or unresponsive debugging on Linux when the debug adapter recursively scanned the process working directory instead of the project’s package cache.
- #8327 Fixed workspace initialization hanging when a local ruleset includes a ruleset from a URL.
- #7811 Fixed AA0237 false positive when a variable references a table declared with TableType = Temporary.
- #8091 Authentication settings in workspace folder settings are now correctly propagated when working in multi-root workspaces.
- #8117 Fixed symbol download reporting that all symbols were already in the cache when a cached dependency declares
propagateDependenciesand one of its own dependencies is missing from.alpackages. Such packages are now downloaded again so their propagated dependencies are fetched as well. - #8146 Fixed a compiler crash (
InvalidCastException) and duplicate misleading diagnostics when a member access is applied to an application object reference, such asCodeunit::MyCodeunit.MemberorDatabase::Customer.Field. The compiler now reports a singleERR_InvalidObjectReferencediagnostic. - #8168 Fixed AA0139 (possible overflow assigning to string with smaller capacity) not being reported on
Rec.Validate(Field, Value)calls, mirroring the existing behavior for directRec.Field := Valueassignments. - #8205 Control Add-In XML comments are not properly handled/shown
- #8218 Fixed AA0137 (unused variable) not being reported for variables declared in multi-variable (comma-separated) declarations.
- #8220 Fixed symbol download, publishing, and debugging failing when a cloud environment falls back to the legacy web endpoint.
- #8223 Fixed cross-tenant sandbox connections when
useVsCodeAuthenticationis enabled. Cross-tenant connections now fall back to interactive or device code login depending on configuration. - #8226 Fixed
altool workspace compilecrashing on Windows withIOException: The handle is invalidwhen stdout is redirected (e.g., in CI/CD pipelines). - #8227 Fixed the MCP server so projects added through
al_addprojectremain available afteral_downloadsymbolsrefreshes the workspace, keeping tools such asal_symbolsearchworking correctly. - #8237 Fixed an issue where
Microsoft.Dynamics.Nav.EditorServices.Hostprocesses could remain running after Visual Studio Code closed. - #8240 Fixed
InvalidCastExceptioninOperationExtensions.GetSymbol()when called onDATABASE::,CODEUNIT::, or similar application object reference expressions in custom code analyzers. - #8241 Fixed a CLR exception in
List.AddRangewhen adding an empty list of interface values. - #8247 Fixed Dictionary.Get(Key, var Value) emitting incorrect C# for Interface and Codeunit value types.
- #8249 Fixed duplicate AL0197/AL0264 diagnostics when
textDocument/didOpenURI casing differs from on-disk casing on Windows. - #8250 Fixed variable resolution in
Directory.App.Props.jsonwhere variables referencing other variables would fail to resolve if they did not sort alphabetically after their dependencies. - #8265 Fixed AL0254 (the sorting field should be part of a key) being reported for a sorting field that is added by a table extension in an app separate from the base table. Because a key that includes such a field cannot be added from your own extension (it is blocked by AL0885/AL0886, which only allow fields from the base table in an extension key), the unactionable AL0254 is now replaced by the new diagnostic AL1430, which keeps the performance signal and points you at the app that owns the field so a key can be added there.
- #8267 Fixed
Specified part does not exist in the packagepackaging error when AL source files have names containing(,), or'.PackageCommon.CreatePackagedUriis now idempotent and no longer double-encodes already percent-encoded path segments. - #8270 Fixed
al_symbolsearchcrashing when dependency symbol descriptors include a table extension key whose target table cannot be resolved. - #8279 Fixed AL0920 false positive when a codeunit implements an internal interface member with
Access = Internal. The warning is now only raised when the implementation could actually widen the member’s visibility beyond the module boundary. - #8290 Fixed AL Explorer source views omitting object details such as codeunit procedures and table methods.
Bug fixes
- Fixed Snapshot Debugging MCP snappoint input and decoded snapshot output being shifted by one line because internal zero-based positions were exposed instead of the one-based lines shown in Visual Studio Code.
- Fixed breakpoints in default interface method implementations not being hit when continuing execution with F5.
- Fixed AL0886 being reported from runtime 18 when a table extension key references a field added by a table extension that is packaged in the same app as the base table.
- AL report layout and analysis view file path properties (
RDLCLayout,WordLayout,ExcelLayout,LayoutFile, andDefinitionFile) now accept both/and\directory separators on all operating systems, so the same AL source compiles identically on Windows and Linux. AL0363 (“The directory separator used in this property value is not compatible with the current operating system.”) is no longer reported; the diagnostic ID is reserved for backwards-compatible suppression syntax. - Fixed an issue where a
casestatement over aTimevariable withTimeliteral labels would cause the compiler to emit invalid C# (case NavTime.Create(...):) that fails with CS0426. The compiler now correctly emits anif/elsechain forTimeliterals, consistent with the existing behavior forDateandDecimal. - Fixed an issue where certain AppSourceCop upgrade rules would crash with
AD0001when a table or tableextension defines two keys with the same name. - Fixed an issue where publishing an extension would launch the browser even when publishing failed.
- Fixed the agent extension template so that toggling the agent’s State inside the embedded Agent Setup Part correctly enables the Update action on the parent setup page, unblocking activation.
- Fixed an issue where AL sources declaring
dotnet { assembly(mscorlib) }or referencing BCL types such asDotNet System.Int64failed on Linux (and other CoreCLR hosts) withAL0451: An assembly named 'System.Private.CoreLib' could not be found, even when the .NET runtime directory was supplied via-assemblyprobingpaths. The defaultAssemblyLocatornow resolves BCL assemblies that ship with the running .NET runtime, and the compatibility check no longer filters out .NET 10 composite ReadyToRun BCL DLLs. - Fixed an issue where the “Add namespace” code action for CodeCop rule AA0247 was not offered when none of the resolution strategies worked. Strategies using the
$(parentfolder)placeholder, the publisher name, or project name are now normalized into a valid namespace, and the action falls back toDefault.Namespacewhen no valid name can be derived, so the action is always available. - Fixed CodeCop rules AA0181 and AA0233 handling for explicit
Findsearch modes.Find('-')andFind('+')are recognized as dataset operations, whileFind('=')andFind('<>')remain single-record operations. - CodeLens element for code coverage now uses the user-selected default run profile when running the related tests.
- Fixed an issue where an enum value or option member used as a field’s
InitValuewas not detected by Find All References and was not updated when renaming that enum value or option member. - Fixed a compiler issue where pages with user-defined GetDecimalString methods could result in C# compilation errors.
- Fixed an issue where the generated AL table proxies for CDS/Dataverse integration referenced base tables by their bare short name. When two referenced apps defined a table with the same name (for example
FS Customer Assetin both the Base Application and Field Service Integration), the reference was ambiguous and the compile failed with AL0275. Generatedextends,TableRelation, andCalcFormulareferences are now fully namespace-qualified. - Added the
IsDirtyproperty to the Record API. - Applied additional validation and filtering when loading rule sets from external
http/httpsURLs. - #8289 Fixed an issue where the AL Language extension entered a perpetual “Loading workspace” state when opening a multi-root workspace whose folder roots contained no
app.json(for example, a documentation folder combined with a folder whose AL projects live only in nested subfolders). The client now skips the progress notification when no AL workspace folder can be activated, and the language server completes the progress explicitly when the active-workspace request is not backed by a manifest. - Fixed an issue where a multi-project workspace would not load all dependencies.
ALTool changes
- Added
runtestscommand to run AL test codeunits directly from the command line without the MCP server, enabling CI/CD test automation scenarios. runtestsnow emits structured, machine-parseable JSON on stdout by default (pass/fail/skip counts and per-method results), keeping stdout free of log noise; pass--rawfor the previous human-readable console output.- Cloud targets continue to use AAD/OAuth by default. For headless on-premises and container scenarios,
publishappandruntestsnow read missing connection values fromBC_SERVER_URL,BC_SERVER_INSTANCE, andBC_SERVER_PORT; whenBC_SERVER_USERNAMEandBC_SERVER_PASSWORDare also set, UserPassword authentication is selected automatically. Explicit command-line options and cloud targets continue to take precedence. - Added
--noauthsupport toal LaunchMcpServerso headless MCP scenarios can bypass server-side auth prompting while still using the configured connection details for publish, test, and symbol operations. - Added
al launchlspserver, a standard LSP server over stdio that any LSP-capable AI agent or editor can consume for full AL language intelligence (completions, hover, go-to-definition, find references, rename, formatting, inlay hints, etc.). Distributed as part of the AL dotnet tool nuget package; see AL Language Server for agentic development below for usage. - Added
altool launchprofilingmcpproxy, a stdio MCP proxy that connects an AI agent to a Business Central server’s Performance Profiling MCP context to capture.alcpuprofileCPU profiles from a slow session; the target session id is supplied per tool call. See Performance Profiling MCP below for usage. - Added
altool launchsnapshotmcpproxy, a stdio MCP proxy that connects an AI agent to a Business Central server’s Snapshot Debugging MCP context to capture an AL snapshot from the next session that reproduces a failing scenario — no debugging session or up-front session id required. See Snapshot Debugging MCP below for usage. - Added
--errorlogdirectorytoaltool workspace compile. When set, each project emits a structured error log (<project>_<timestamp>.errorLog.json) into the specified directory, enabling AL code-analysis alert tracking for the workspace-compilation build path. When omitted, no error logs are written. runtestscan now execute a batch of codeunits over a single connection via--testgroups <file>, a JSON file of[{ "codeunitId": 134001, "testMethods": ["MethodA"] }, ...]where each group pairs a codeunit with the methods to run in it (an empty or omittedtestMethodsruns all of that codeunit’s methods). The whole batch runs with one authentication and one server session, avoiding the per-codeunit connect cost of invokingruntestsonce per codeunit.--testgroupscannot be combined with thecodeunitIdargument or--testmethods.
Breaking Changes
Removed al.editorServicesPath configuration property
The al.editorServicesPath configuration property has been removed. The extension now always uses the standard bin/ folder for all binaries. If you have this property configured in your settings, you should remove it as it will no longer have any effect.
Visual Studio Code Editor changes
Automatic .NET runtime acquisition
The AL Language extension now uses automatic .NET runtime acquisition through the .NET Install Tool for VS Code. When the extension starts, it automatically downloads and installs the required .NET 10.0 runtime if it’s not already present on your system.
Private marketplace environments must make the .NET Install Tool available so VS Code can install the AL Language extension dependency. If runtime acquisition fails after activation, AL Language falls back to a compatible ASP.NET Core runtime already installed through the system dotnet command.
This change significantly reduces the extension size by eliminating platform-specific runtime copies for Windows, macOS, and Linux.
Code analyzers now deployed in bin folder
Code analyzers (CodeCop, AppSourceCop, UICop, PerTenantExtensionCop) are now deployed directly in the bin/ folder alongside other extension binaries, avoiding duplication. Previously, analyzers were in a separate bin/Analyzers subfolder.
The unused al.editorServicesPath configuration property has been removed as the extension now always uses the standard bin/ folder for all binaries.
Visual Studio Code authentication is now enabled by default
The useVsCodeAuthentication setting now defaults to true. Authentication tokens are acquired through Visual Studio Code’s built-in authentication instead of the extension’s interactive or device code login.
This applies to all debug configurations (launch and attach) and the al.useVsCodeAuthentication workspace setting. If you need to use the legacy interactive or device code login, set useVsCodeAuthentication to false in your launch.json or workspace settings.
Restrict global symbol downloads to a specific minor version
The new al.globalSourcesEnforceMinorVersion setting controls how the AL extension resolves package versions when downloading symbols from global sources.
By default (false), the extension downloads the latest available version within the same major version as specified in app.json. When set to true, resolution is restricted to versions matching both the major and minor version, downloading the highest available patch within that minor. This applies to both direct dependencies and propagated dependencies.
"al.globalSourcesEnforceMinorVersion": true
MCP Server: Add projects at runtime with al_addproject
The --projects parameter for al LaunchMcpServer is now optional. The server can start without any projects loaded and projects can be added dynamically at runtime using the new al_addproject tool.
al LaunchMcpServer --port 5000
Once the server is running, agents can call al_addproject with the path to an AL project folder (containing an app.json file) to load it into the workspace. All other tools (al_compile, al_build, al_symbolsearch, etc.) will return an actionable error if invoked before any projects have been added.
MCP Server: File logging support
The MCP server now supports file-based logging for diagnostics and troubleshooting. Use the --logfile and --loglevel arguments to enable logging:
al LaunchMcpServer --port 5000 --logfile C:\logs\almcp.log --loglevel Verbose
Available log levels are Debug, Verbose, Normal, Warning, and Error. Use --nolog to disable logging entirely. If --logfile is not specified, logging defaults to ~/.al-mcp/almcp.log (portable across Windows, Linux, and macOS).
Allocate free AL object IDs with al_getnextobjectid
A new tool, al_getnextobjectid, is now available across the three agent-facing surfaces — Visual Studio Code’s Language Model tools (Copilot Chat), the AL MCP server (al LaunchMcpServer), and the AL LSP server (al launchlspserver). Agents can call it during scaffolding to allocate free object IDs for any application object (Table, Page, Codeunit, Report, Query, XmlPort, Enum, PermissionSet, and the matching *Extension types) without having to read app.json themselves or risk colliding with existing objects in the project.
Inputs:
objectType(required) — e.g."Codeunit","PageExtension". The tool’s error message lists every supported value if an unknown type is passed.count(optional, default1, max50) — return up to N free IDs in one call.rangeStart/rangeEnd(optional) — narrow allocation to an explicit range; the range is intersected with the project’sidRangesfromapp.json. When omitted, the project’s manifest ranges are used in full.
The result includes the allocated IDs, the resolved ranges, and an outOfRange indicator (with the next ID past the configured ranges) when fewer IDs were available than requested. The scan is syntax-only and project-scoped — IDs declared in installed app dependencies or other extensions published to the same dev sandbox are not considered, so the tool always emits an advisory warning describing this limitation.
Performance Profiling MCP: capture CPU profiles from a running session
A new Performance Profiling MCP surface lets an AI agent capture .alcpuprofile CPU profiles from a slow Business Central session and analyze the hot AL code paths. Unlike the troubleshooting MCP — which targets the session you are actively debugging — profiling is a standalone scenario: the target session id is supplied per tool call (the agent reads it from the prompt, e.g. “why is session 10 slow?”), so no debugging session is required.
Command line (altool launchprofilingmcpproxy)
The AL dotnet tool now includes a launchprofilingmcpproxy command — a stdio MCP server that forwards an agent’s profiling tool calls (schedule_profiling, stop_schedule_and_get_profiles, get_profile_for_activity) to a Business Central server’s Profiling MCP context. The connection is configured once from launch.json-style parameters; the target session id is a per-call tool argument.
# On-premises
altool launchprofilingmcpproxy --environmenttype OnPrem --server http://localhost --serverinstance BC --port 7047 --authentication UserPassword
# Cloud (sandbox)
altool launchprofilingmcpproxy --environmenttype Sandbox --environmentname sandbox --tenant contoso.onmicrosoft.com
Credentials come from the environment and are never stored in the configuration. For cloud connections the proxy authenticates non-interactively (it never opens a browser) and resolves a token in this order: (1) the BC_ACCESS_TOKEN environment variable — in Visual Studio Code agent mode the extension injects your VS Code sign-in token here automatically, and for headless/CI hosts you set it yourself; (2) a token cached by a prior interactive altool auth login. For on-premises hosts, BC_SERVER_USERNAME/BC_SERVER_PASSWORD supply UserPassword (Basic) authentication, and Windows authentication uses the current Windows identity with no credentials. For a multitenant on-premises server, pass --tenant to target a specific tenant. The same --logfile, --loglevel, and --nolog diagnostics as the other commands are supported.
Visual Studio Code (agent mode)
The extension now registers a Business Central Profiling MCP Server as an MCP server definition provider, so GitHub Copilot agent mode can profile a session directly — with no manual mcp.json setup. The connection is derived from the active AL project’s launch.json (cloud environment/tenant or on-premises server/instance), and authentication reuses your Visual Studio Code sign-in. Ask the agent a question that names the slow session (for example, “Business Central is slow for session 10, can you find out why?”) and it starts the capture, asks you to reproduce the slow scenario, stops, and analyzes the resulting profile.
Delivery of the captured profiles
The proxy serves the captured schedule archive over MCP resources/read (it advertises the resources capability) and also writes each captured .alcpuprofile / archive to a local profile folder, returning the file path in the tool result — so MCP hosts that do not surface embedded binary resource content (such as the GitHub Copilot CLI) can still read the profile from disk. Auto-captured profiles are grouped in per-schedule subfolders under a dedicated schedules folder, and removed once past a retention window (1 day by default, configurable with --profileretentiondays); cleanup is call-driven (proxy startup and the next capture), not a background timer. The profile folder location is configurable with --profilefolder (default %TEMP%/bc-profiling). A new analyze_profiles_in_folder tool analyzes profiles you place in the user subfolder — which the proxy never auto-deletes — for profiles obtained out of band. These local files can contain sensitive business data, so handle them securely and in line with your privacy requirements.
Session memory usage
Alongside the CPU samples, each captured .alcpuprofile now carries the profiled session’s estimated total managed memory and object count, and the agent presents them in a human-readable form so you can see at a glance whether memory is a factor. This is on by default, with nothing extra to turn on.
Recovering the target session from a schedule id
stop_schedule_and_get_profiles and get_profile_for_activity no longer require targetSessionId when the agent passes a scheduleId armed by an earlier schedule_profiling call in the same session — the proxy remembers which session each schedule was armed on and routes the call automatically. targetSessionId remains accepted and takes precedence when supplied.
Snapshot Debugging MCP: capture AL snapshots from a running session
A new Snapshot Debugging MCP surface lets an AI agent capture an AL snapshot from a Business Central session that reproduces a failing scenario and then debug the recorded execution offline. Snapshot debugging arms the server and records the next session that reproduces the problem: the agent initializes a debugging context (optionally scoped to a specific user), you reproduce the scenario, and the agent stops and collects the snapshot. There is often no target session yet, so a session id isn’t required up front; if you already have a running session in mind, its id can be supplied to attach to that session directly.
Command line (altool launchsnapshotmcpproxy)
The AL dotnet tool now includes a launchsnapshotmcpproxy command — a stdio MCP server that forwards an agent’s snapshot tool calls (initialize_snapshot_debugging, stop_snapshot_debugging, get_snapshot_status) to a Business Central server’s Snapshot Debugging MCP context. The connection is configured once from launch.json-style parameters.
# On-premises
altool launchsnapshotmcpproxy --environmenttype OnPrem --server http://localhost --serverinstance BC --port 7047 --authentication UserPassword
# Cloud (sandbox)
altool launchsnapshotmcpproxy --environmenttype Sandbox --environmentname sandbox --tenant contoso.onmicrosoft.com
Credentials come from the environment and are never stored in the configuration. For cloud connections the proxy authenticates non-interactively (it never opens a browser) and resolves a token in this order: (1) the BC_ACCESS_TOKEN environment variable — in Visual Studio Code agent mode the extension injects your VS Code sign-in token here automatically, and for headless/CI hosts you set it yourself; (2) a token cached by a prior interactive altool auth login. For on-premises hosts, BC_SERVER_USERNAME/BC_SERVER_PASSWORD supply UserPassword (Basic) authentication, and Windows authentication uses the current Windows identity with no credentials. For a multitenant on-premises server, pass --tenant to target a specific tenant. The same --logfile, --loglevel, and --nolog diagnostics as the other commands are supported.
Visual Studio Code (agent mode)
The extension now registers a Business Central Snapshot MCP Server as an MCP server definition provider, so GitHub Copilot agent mode can capture a snapshot directly — with no manual mcp.json setup. The connection is derived from the active AL project’s launch.json (cloud environment/tenant or on-premises server/instance), and authentication reuses your Visual Studio Code sign-in. Ask the agent to investigate a failing scenario (for example, “a user hits an error posting a sales order — capture a snapshot so we can debug it”); it initializes the context, asks you to reproduce the scenario, then stops and returns the recorded snapshot.
Delivery of the captured snapshot
stop_snapshot_debugging returns the recorded snapshot archive either inline (a base64-encoded .zip) or, for larger captures, as an MCP resources/read resourceUri the proxy serves on demand — so MCP hosts that do not surface embedded binary resource content can still read the archive by its URI. The archive can contain sensitive business data, so handle it securely and in line with your privacy requirements.
Environment moves and restarts
A snapshot that is still being captured may not survive the environment being moved or restarted. If that happens, get_snapshot_status reports the context as not found — just initialize a new snapshot and reproduce the scenario again.
AL Language Server for agentic development (al launchlspserver)
The AL dotnet tool now includes a new launchlspserver command. This starts a standard Language Server Protocol (LSP) server over stdio, allowing AI agents and editors that speak LSP to get full AL language intelligence without requiring a Visual Studio Code client.
al launchlspserver --projects "C:\Users\MyUser\MyALProject"
Once running, the server responds to standard LSP requests including completions, hover, go-to-definition, find references, document symbols, rename, formatting, inlay hints, folding ranges, and type hierarchy. Projects and per-project options can also be supplied at runtime via the LSP initialize request’s initializationOptions.
Per-workspace configuration can additionally be sourced from a <rootUri>/.vscode/settings.json — the same file the AL Visual Studio Code extension reads. Recognized al.* keys (packageCachePath, assemblyProbingPaths, ruleSetPath, enableCodeAnalysis, codeAnalyzers) override CLI defaults at startup, and LSP initializationOptions from the client take final precedence. JSONC comments and trailing commas are tolerated. Use --settingspath to point at a specific settings.json (any Visual Studio Code scope — user, workspace, or folder) when the LSP host doesn’t pass the workspace’s actual rootUri, or when one user account works across multiple AL workspaces.
For multi-app workspaces, --workspacefile accepts a Visual Studio Code .code-workspace file directly: its folders populate the projects loaded at startup and its inline settings block contributes the same al.* keys. This lets a single file drive both project layout and configuration, matching how Visual Studio Code itself opens the workspace.
The command supports the same compilation and logging options as launchmcpserver:
[projects] Optional. AL project paths to load at startup.
--packagecachepath Paths to the package cache folders.
--assemblyprobingpaths Paths to probe for dependent assemblies.
--ruleset Path to the ruleset file.
--outfolder Output folder for compilation artifacts.
--codeanalyzers Code analyzers to enable.
--settingspath Optional. Path to a settings.json (any VS Code scope
— user, workspace, or folder) whose al.* keys
override CLI defaults at startup. When omitted,
auto-discovered from the initialize request's
rootUri (walking ancestors).
--workspacefile Optional. Path to a VS Code .code-workspace file. Its
`folders` extend the projects loaded at startup
(merged with the positional projects argument), and
its inline `settings` block contributes al.* keys
as workspace-level config. --settingspath, when also
supplied, overrides the inline settings.
--logfile Path to the log file. Defaults to ~/.al-mcp/almcp.log.
--loglevel Log level: Debug, Verbose, Normal (default), Warning, Error.
--nolog Disable logging entirely.
--disableTelemetry Disable telemetry collection.
To use the AL Language Server with Claude Code or another LSP-capable AI agent, configure the server as an LSP plugin pointing to altool launchlspserver. See the Claude Code plugin documentation for how to create a plugin, and the LSP server reference for the full .lsp.json schema.
AppSourceCop
- AS0116, AS0119, AS0121: Fixed moved objects validation for scenarios where the source and destination apps have no direct dependency on each other. The moved objects cache now correctly resolves table extensions by including all transitive dependencies when building reference symbols.
- AS0125: An existing app can now adopt namespaces. Adding a namespace changes the translation identifier of every translated property, which previously blocked the uptake. The rule is no longer reported when the baseline object has no namespace, the new one has one, and nothing else changed. Moving an object to a different namespace, removing its namespace, or renaming it in the same version as the uptake is still reported.
- AS0148: An interface method was made required without first being marked
[RequiredPending]. - AS0149: Adding or removing a required method on an interface may break dependent extensions at runtime.
- AS0152: Removing a codeunit used as a default implementation may break dependent extensions at runtime.
AL Language changes
Default interface method implementations
Interfaces can now include method bodies that serve as default implementations. Codeunits implementing the interface can omit these methods to inherit the default behavior, or provide their own implementation to override it. This feature requires runtime version 18.0 or later.
interface IPaymentMethod
{
// Default implementation — implementors can omit or override
procedure GetTransactionFee(): Decimal
begin
exit(0);
end;
// Abstract — must be implemented
procedure ValidateAmount(Amount: Decimal): Boolean;
}
codeunit 50100 CashPayment implements IPaymentMethod
{
// GetTransactionFee is inherited from the interface default
procedure ValidateAmount(Amount: Decimal): Boolean
begin
exit(Amount > 0);
end;
}
Default methods are dispatched through the interface variable, not inherited by the implementing codeunit’s public API. Calling GetTransactionFee() on a Codeunit CashPayment variable will fail — use an Interface IPaymentMethod variable instead.
The new [RequiredPending] attribute marks a default interface method as becoming required in a future version. When applied, the compiler reports:
interface IPaymentMethod
{
[RequiredPending('Will become mandatory in v28.0', '28.0')]
procedure GetTransactionFee(): Decimal
begin
exit(0);
end;
}
New AppSourceCop rules support the transition lifecycle:
- AS0148: Flags methods whose default body was removed (made required) without first being marked
[RequiredPending]. - AS0149: Warns when interface method changes alter the runtime identifier, which may break dependent extensions until recompiled.
New properties on ModuleInfo
The ModuleInfo type now exposes additional app manifest metadata. These properties require runtime version 18.0 or later:
Help— Returns the help link of the specified application.EULA— Returns the End User License Agreement (EULA) link of the specified application.PrivacyStatement— Returns the privacy statement link of the specified application.ContextSensitiveHelpUrl— Returns the context sensitive help URL with the user’s locale resolved from the application’s supported locales.
procedure ShowAppMetadata()
var
appInfo: ModuleInfo;
begin
NavApp.GetCurrentModuleInfo(appInfo);
Message('Help: %1\EULA: %2', appInfo.Help, appInfo.EULA);
end;
ToolTip and ToolTipML properties are now available on pages
The ToolTip and ToolTipML properties can now be set at the page object level. When a navigation action has RunObject = page X and the page has a ToolTip or ToolTipML set, the action will inherit the page’s tooltip if no tooltip is explicitly specified on the action itself. This mirrors the existing behavior for reports.
page 50100 MyPage
{
ToolTip = 'Opens the My Page page.';
// ...
}
Report layout subtype on Word layouts
A new Subtype property has been introduced on Word layouts where the subtype can be specified as Default, HeaderFooter, Theme or Body. Notice that layouts of subtype Theme must be implemented as template files with the dotx file extension. The Body subtype follows the same file type constraints as Default (a docx file): use Default for existing layouts, and Body for layouts that support composite layouts with selectable header/footer and theme parts. A body layout can set HeaderFooterPart and ThemePart to reference layouts with the HeaderFooter and Theme subtypes.
report 50000 "Report With Parts"
{
DefaultRenderingLayout = DefaultPart;
dataset { ... }
requestpage {...}
rendering
{
layout(DefaultPart)
{
Type = Word;
Subtype = Body;
HeaderFooterPart = StandardHeaderFooter;
ThemePart = StandardTheme;
LayoutFile = 'Reports\Layouts\DefaultPart.docx';
}
layout(StandardHeaderFooter)
{
Type = Word;
Subtype = HeaderFooter;
LayoutFile = 'Reports\Layouts\StandardHeaderFooter.docx';
}
layout(StandardTheme)
{
Type = Word;
Subtype = Theme;
LayoutFile = 'Reports\Layouts\StandardTheme.dotx';
}
}
}
Default header/footer and theme parts for composite Word layouts
Two new report properties, DefaultHeaderFooterPart and DefaultThemePart, let a report specify the default header/footer and theme parts used by its Word layouts. Following the same pattern as DefaultRenderingLayout, each property references a layout declared in the report’s rendering section: DefaultHeaderFooterPart must point to a layout with the HeaderFooter subtype, and DefaultThemePart to a layout with the Theme subtype. Referencing a layout with a different subtype is reported as an error.
report 50000 "Report With Parts"
{
DefaultRenderingLayout = DefaultPart;
DefaultHeaderFooterPart = StandardHeaderFooter;
DefaultThemePart = StandardTheme;
dataset { ... }
requestpage { ... }
rendering
{
layout(DefaultPart)
{
Type = Word;
Subtype = Body;
LayoutFile = 'Reports\Layouts\DefaultPart.docx';
}
layout(StandardHeaderFooter)
{
Type = Word;
Subtype = HeaderFooter;
LayoutFile = 'Reports\Layouts\StandardHeaderFooter.docx';
}
layout(StandardTheme)
{
Type = Word;
Subtype = Theme;
LayoutFile = 'Reports\Layouts\StandardTheme.dotx';
}
}
}
Report custom XML parts in Microsoft Word layouts now contain CompanyMetadata
The custom XML parts in Microsoft Word layouts have been extended with a new CompanyMetadata element in the BCReportInformation element for use with composite layout rendering.
Data-driven tests
When writing tests, you can now create data-driven tests by using the TestDataSource attribute. It has two arguments: the codeunit that generates the test cases and a text literal for the dataset identifier. The test method must take exactly one parameter of type interface ITestContext (or an interface that extends it).
[TestDataSource(Codeunit::"Late Fee Test Data Source", 'LateFeeTestData.json')]
procedure LateFeeTest(Context: interface "ILate Fee Test Context")
begin
VerifyLateFee(Context);
end;
The referenced codeunit must implement the ITestDataSource interface. This interface has a single function, GetDataRows, where the first argument is the text literal from the TestDataSource attribute. The second argument is a type that provides context about the test codeunit. Each item in the returned list is a test case.
procedure GetDataRows(DataSetIdentifier: Text; context: DataSourceContext): List of [interface ITestContext]
begin
end;
Each test case is based on the ITestContext interface, which has a single function that returns the identifier of the test case. This identifier must be deterministic and remain the same for each run of the test function.
interface ITestContext
{
procedure Identifier(): Text;
}
Support for BigInteger field migration
The compiler now supports gradual migration between Integer and BigInteger field types:
1. Field type changes (Integer → BigInteger): Changing an existing table field from Integer to BigInteger is now supported when targeting runtime version 18.0 (Fall 2026) or later. A new AppSourceCop rule (AS0141) warns about the potential consequences for dependent extensions that reference the field.
2. Property context warnings: When referencing a BigInteger field in property contexts (TableRelation, CalcFormula WHERE clauses, etc.), the compiler emits warnings for narrowing conversions (BigInteger → Integer) to alert about potential overflow at runtime.
Key behaviors:
- Field upgrades: Integer → BigInteger changes are allowed in upgrade scenarios
- Property warnings: BigInteger → Integer,Option,Enum conversions in WHERE clauses emit warnings about potential overflow
- CalcFormula support: LOOKUP, MAX, and MIN operations allow Integer ↔ BigInteger conversions with appropriate warnings for narrowing conversions
Miscellaneous
- The compiler now warns when a table (AL0914) or table extension (AL0915) declares a large number of normal fields, which may limit extensibility due to SQL column limits.
- Word report layouts now always include extended custom XML parts for Word Add-in compatibility, simplifying layout generation and improving consistency. The compiler feature WordAddInCompatibleLayouts is now deprecated.
- Table extension keys can now mix both base table fields and extension fields in key properties (KeyFields, SumIndexFields, SqlIndex, IncludedFields) to create indexes that span table and table extension fields.
alnow matches command names case-insensitively, so for exampleal Compileworks the same asal compile.- Added four new system flow fields on Normal and Temporary tables:
SystemCreatedByUserName(Text[50]),SystemCreatedByFullName(Text[80]),SystemModifiedByUserName(Text[50]), andSystemModifiedByFullName(Text[80]). These fields provide the username and full name of the user who created or last modified a record, complementing the existingSystemCreatedBy/SystemModifiedByGUID fields.
Snapshot debugging AL tests
- Added
run_teststo the Snapshot Debugging MCP proxy so agents can execute selected AL tests in an isolated session attached to the armed snapshot, then collect the test capture throughstop_snapshot_debugging. - Added optional
snapshotVerbosityto Snapshot Debugging MCP initialization.SnapPointremains the default;Fullcaptures every method context and encountered non-IP-protected AL source object for any attached session and should be used only for short, targeted reproductions.
Source: Visual Studio Marketplace, ms-dynamics-smb.al changelog, Microsoft’s text unchanged. Dates from the gallery’s upload records. Merged from the stable and pre-release changelogs, which differ for this version.