← All postsDevelopment ยท 3 November 2026

Writing AL extensions that survive release waves

Microsoft replaces the base application twice a year. An extension that compiled in April and breaks in October was built wrong. The design rules we use for every per-tenant extension: events over code, interfaces over hard references, obsolete tags over deletions, and a dependency graph you can draw.

The promise of extensions is that Microsoft can change the base application and your code keeps working. The promise holds only if the extension was written to keep it. Every release wave we see per-tenant extensions that fail to compile, or compile and misbehave, for reasons that were avoidable when they were written. These are the rules we build to.

1. Subscribe, never assume

All interaction with the base application goes through published events (OnBefore, OnAfter, integration events) and the standard extensibility points. If the thing you need is not published, request it from Microsoft through the AL extensibility process on GitHub rather than working around it; they add hundreds a year. What you do not do is copy a base codeunit's logic into your own and call that instead. It works until the base logic changes, and then it silently does something different from the rest of the system.

Keep event subscribers thin: validate, set a flag, call your own codeunit. The subscriber is the seam; the logic lives behind it where it can be tested.

2. Declare your dependencies, and keep the graph flat

An extension depends on the base application, the system application, and whatever ISV apps it touches, each with a version. The app.json dependency list is the contract. Keep it minimal: depending on an ISV app you only need one field from ties your release cadence to theirs. If you have several extensions of your own, one "foundation" extension that the others depend on is fine; a web of extensions depending on each other in both directions is not, and it is the usual reason an update fails with a dependency error at two in the morning.

3. Never delete; obsolete

A field, table, procedure or enum value that exists in production has data or callers. Removing it is a breaking change. The discipline: mark it ObsoleteState = Pending with a reason and a tag naming the version, ship that, give callers a release to move, then Removed, then (for fields and tables) a later clean-up. The compiler enforces the states; the schema sync will refuse a destructive change you have not staged. We wrote up where the data actually lives and why this matters more in version 29.

4. Use interfaces and enums for anything that varies

Anything with "type" in the name (shipping method, pricing rule, document handler) is an enum with an interface implementation per value, not a case statement. Enums are extensible by other apps; case statements are not. It is the difference between an ISV adding a value and your code handling it, and an ISV adding a value and your code throwing an error on a sales line.

5. Permissions ship with the app

Every extension ships its own permission sets (as AL objects, not data), composed from the base ones where possible, so that a customer installing it gets a working permission model rather than "SUPER for everyone until we sort it out". Agents in version 29 make this sharper: an agent runs under a user with specific permissions, and an extension that assumes SUPER will not work for it.

6. Compile against the next version before it is released

Microsoft publishes the next major's symbols and a preview environment weeks before general availability. Every extension we maintain is compiled against the next major as soon as the preview is available, with warnings treated as errors, and the obsoleted-object warnings triaged. By update day there is nothing left to find. The release wave checklist has the dates and the sequence.

7. Build for the cloud even if you are on-premise

No file system, no DotNet, no direct SQL, no assumptions about the server. The AL cloud ruleset (AppSourceCop or at least PerTenantExtensionCop) switched on and clean. An on-premise customer who moves online in two years should not have to rewrite anything.

8. One customisation extension per customer, in their repository

The customer's bespoke code lives in one app, with one object range, one naming prefix, in a Git repository the customer owns, built by a pipeline (AL-Go for GitHub is the standard). Not in the partner's repository, not in a zip on a consultant's laptop, and not created ad hoc through the in-client Designer. When the customer changes partner, which happens, the handover is a repository URL.

9. Telemetry from day one

Every extension emits custom telemetry (Session.LogMessage) for the things that matter: integration calls, long operations, business rule failures. Pointed at the customer's Application Insights. When something goes wrong at month-end, the answer is in the telemetry, not in a reproduction attempt the following Tuesday.

10. Tests, or it did not happen

A test codeunit per feature, run in the pipeline on every commit, including against the next major. Not for coverage statistics; for the moment Microsoft changes the posting routine and your test tells you before your customer does. The next post covers how.

None of this is clever. It is the difference between an extension that is an asset and one that is a liability at every release. If you have inherited a per-tenant extension and are not sure which it is, our AL code check reviews it against exactly this list.

Talk to us
Want this done in your Business Central?
Book a 30 minute callGet the monthly newsletter

More from the blog