Everything you need before writing a line of AL: VS Code, the AL Language extension, a sandbox, app.json, launch.json, symbols, ID ranges, naming, code analysers and source control, finishing with the first two objects of a real extension.
This is part 1 of AL development from scratch, a ten-part series that takes you from an empty folder to a working, shippable Business Central extension. Every part builds the same small app, Pallet Manager, so by the end you will have written every common object type for a real reason rather than as an isolated example.
This part is all about getting set up properly. It is the least exciting part of the series and the one that saves you the most time later. Most of the problems people hit in their first month of AL (symbols that will not download, objects that clash with someone else's, a sandbox that loses its data every time they publish) come from skipping something here.
Pallet Manager is a per-tenant extension for a warehouse that ships on several kinds of pallet. It lets the business:
The objects, all with the prefix BBX and in the ID range 50100 to 50149, are an enum, a table, a table extension on Item, four pages, two page extensions, four codeunits, a report, a query, an XMLport and two permission sets. If you want a plain-English description of what each object type is for before diving in, read every object type in Business Central, explained.
Ctrl+Shift+X) by searching for "AL Language". It gives you the compiler, IntelliSense, the debugger, the code analysers and the commands that start with "AL:".Useful but optional: the "AL Object Designer" or "AZ AL Dev Tools" community extensions, which help with navigation and code generation. They are not needed for anything in this series.
Open VS Code, press Ctrl+Shift+P to open the Command Palette and run AL: Go!. It asks for:
PalletManager);AL: Go! creates three things: app.json (the extension's manifest), .vscode/launch.json (where to publish it) and a sample HelloWorld.al file that adds a message to the customer list. Delete HelloWorld.al once you have seen it work; you do not want it in a real app.
The compiler does not care where your .al files are inside the project folder, but people do. Pick a layout on day one and stick to it. A simple layout that works well for small and medium apps is one folder per object type:
PalletManager/
.vscode/
launch.json
settings.json
.alpackages/ (downloaded symbols, never committed)
src/
Enum/
PalletMaterial.Enum.al
Table/
PalletType.Table.al
TableExt/
Item.TableExt.al
Page/
PalletTypeList.Page.al
PalletTypeCard.Page.al
PageExt/
ItemCard.PageExt.al
Codeunit/
PalletMgt.Codeunit.al
PermissionSet/
PalletView.PermissionSet.al
Translations/
app.json
.gitignore
If you use namespaces (covered below), a common convention is to make the folders mirror them, so a small app like this one can keep the simple layout, and a larger app with BusinessBoxed.PalletManager.Setup and BusinessBoxed.PalletManager.Shipping namespaces would have src/Setup and src/Shipping folders.
Larger apps are usually better organised by feature (a folder per business area, with the objects for that area inside it). Either works. What does not work is every file at the top level with names like Table1.al.
The file name convention Microsoft recommends is the object name without the prefix, then the object type, then .al: PalletType.Table.al, ItemCard.PageExt.al. One object per file. (See naming objects in Business Central development for the full set of conventions and the arguments they stop.)
app.json describes your extension: who made it, what version it is, what it depends on and which object IDs it may use. AL: Go! fills most of it in. This is what Pallet Manager's looks like once tidied up:
{
"id": "6f1c2d3e-4b5a-4c7d-9e8f-0a1b2c3d4e5f",
"name": "Pallet Manager",
"publisher": "Business Boxed Limited",
"version": "1.0.0.0",
"brief": "Pallet types, default pallets per item and pallet requirements.",
"description": "Keeps a list of pallet types, sets a default pallet and units per pallet on each item, and calculates pallet requirements.",
"privacyStatement": "",
"EULA": "",
"help": "",
"url": "https://bbxd.net",
"logo": "",
"dependencies": [],
"screenshots": [],
"platform": "29.0.0.0",
"application": "29.0.0.0",
"idRanges": [
{ "from": 50100, "to": 50149 }
],
"resourceExposurePolicy": {
"allowDebugging": true,
"allowDownloadingSource": true,
"includeSourceInSymbolFile": true
},
"features": [ "TranslationFile" ]
}
The properties that matter on day one:
| Property | What it does | Example / default |
|---|---|---|
id | A GUID that uniquely identifies the app. Business Central uses it, not the name, to know which app is which. Never change it after the first deployment, and never copy it from another project. | Generated by AL: Go! |
name | The name shown in Extension Management. | "Pallet Manager" |
publisher | Who made it. Shown in Extension Management and used, with the name and version, in the .app file name. | "Business Boxed Limited" |
version | Four-part version: major.minor.build.revision. You must increase it every time you deploy a new version to the same environment. | "1.0.0.0" |
idRanges | The object IDs this app is allowed to use. The compiler rejects any object outside the range. | 50100 to 50149 |
dependencies | Other apps this one needs, with their id, name, publisher and minimum version. Microsoft's Base Application and System Application are covered by application, so you only list other apps here. | [] |
application | The minimum version of Microsoft's application apps you build against. Your app will not install on an environment older than this. | Match your sandbox's major version |
platform | The minimum platform version. | Match your sandbox's major version |
runtime | The version of the AL language runtime you compile for. A newer runtime gives you newer language features but needs a newer server. AL: Go! sets it to match the platform you chose. | Set by AL: Go! |
features | Compiler features to switch on. TranslationFile generates an XLIFF file of every caption, label and tooltip so the app can be translated. | ["TranslationFile"] |
resourceExposurePolicy | Whether others can debug your app, download its source, and see source in its symbols. For a per-tenant extension you own, allowing all three is usually sensible: it helps whoever supports the customer next. | All false unless set |
There are many more properties (logo, privacy statement, target, internals visibility and so on). They are all covered in what is app.json?.
launch.json tells VS Code which environment to publish to and how. For an online sandbox called "Sandbox" it looks like this:
{
"version": "0.2.0",
"configurations": [
{
"name": "Pallet Manager sandbox",
"type": "al",
"request": "launch",
"environmentType": "Sandbox",
"environmentName": "Sandbox",
"startupObjectType": "Page",
"startupObjectId": 22,
"breakOnError": "All",
"launchBrowser": true,
"schemaUpdateMode": "Synchronize"
}
]
}
| Setting | What it does | Example / default |
|---|---|---|
environmentType | Sandbox or Production. Use Sandbox. You publish to production through Extension Management or a pipeline, never with F5. | "Sandbox" |
environmentName | The name of the environment as shown in the admin centre. Get this wrong and you publish to a different sandbox from the one you are looking at. | "Sandbox" |
tenant | The Microsoft Entra tenant ID or domain. Needed if your user can sign in to more than one tenant (common for partners). | Omit for your own tenant |
startupObjectType / startupObjectId | What opens in the browser after publishing. Point it at the object you are working on so you are not clicking through menus every time. | Page 22 (Customer List) |
breakOnError | Whether the debugger stops on errors. | "All" |
launchBrowser | Whether to open the browser after publishing. | true |
schemaUpdateMode | What happens to table data when your table definitions change between publishes. See below; this one matters. | "Synchronize" |
Leave it on Synchronize. Switch to Recreate deliberately for one publish when you need to, then switch back.
Your code refers to Microsoft's objects (Item, Customer, Sales Header) and the compiler needs to know what they look like. It gets this from symbol packages: .app files for the System Application, Base Application, Business Foundation and Application apps, downloaded from your environment into the .alpackages folder.
Run AL: Download symbols from the Command Palette. It signs you in (the first time), connects to the environment in launch.json and downloads the symbols that match your app.json versions. After that IntelliSense knows every table, field and procedure in the base app, and you can jump to Microsoft's code with F12 (Go to Definition) to see how they did something.
If symbols will not download, the cause is almost always one of: the wrong environment name, the application version in app.json being newer than the environment, or a user without enough permissions.
Each publish replaces the previous version of your app in that sandbox. You do not need to increase the version number while developing; you do when you deploy to a test or production environment.
Every object except a few (interfaces, profiles, page customisations, control add-ins and entitlements) has a number as well as a name. For a per-tenant extension, the customer's licence covers the range 50,000 to 99,999. Partners usually carve that range up so that two apps for the same customer never overlap: this app gets 50100 to 50149, the next one 50150 to 50199, and so on. Write the allocation down somewhere the whole team can see.
Object names have a maximum length of 30 characters, including the prefix. They must be unique among all objects of that type in the environment, which is why every object in your app starts with a short prefix or ends with a suffix. Pallet Manager uses BBX. Fields you add to Microsoft's tables get the prefix too ("BBX Default Pallet Type"), because another app could add a field called "Default Pallet Type" to Item. Apps published to AppSource must register their affix with Microsoft; for a per-tenant extension it is just good sense.
A namespace is a logical grouping for objects, a bit like a folder path. Microsoft's Customer table, for example, lives in the namespace Microsoft.Sales.Customer, and the Item table in Microsoft.Inventory.Item. Namespaces arrived in AL in 2023, and Microsoft's own apps, including the Base Application, now use them throughout.
Namespaces are optional. An object without a namespace goes into the global namespace and works exactly as AL always has. You will find plenty of working apps with no namespaces at all. We use them in every complete example in this series because they are the modern standard and how Microsoft writes its own code, so you will meet them as soon as you open Microsoft's symbols. Whether you use them in your own apps is your choice; if you leave them out, just delete the namespace and using lines from the examples and everything else stays the same.
The namespace declaration must be the first statement in the file, and there is one per file. After it come any using directives, which name the other namespaces whose objects this file refers to. Then comes the object:
namespace BusinessBoxed.PalletManager;
using Microsoft.Inventory.Item;
tableextension 50100 "BBX Item" extends Item
{
fields
{
field(50100; "BBX Default Pallet Type"; Code[20])
{
Caption = 'Default Pallet Type';
DataClassification = CustomerContent;
TableRelation = "BBX Pallet Type";
}
}
}
The using Microsoft.Inventory.Item; line is what lets the file refer to Item. The "BBX Pallet Type" table needs no using, because it is in the same namespace as this file. All objects in one namespace can see each other.
The usual pattern is Publisher.App. Pallet Manager uses BusinessBoxed.PalletManager, and its test app (part 10) uses BusinessBoxed.PalletManager.Test. A larger app might go one level deeper by business area, such as BusinessBoxed.PalletManager.Setup, but for a small app one namespace is plenty. Do not create a namespace per object; that just adds using lines without making anything clearer.
There are two ways to refer to an object in another namespace:
Item: Record Item;. This is what you will do almost all the time.Item: Record Microsoft.Inventory.Item.Item;. Useful for a one-off reference, or when two namespaces you are using both contain an object with the same name.To find an object's namespace, hover over its name or press F12 (Go to Definition) on it in VS Code: the definition opens from the symbols with the namespace at the top. When the compiler says "the name ... does not exist in the current context" for an object you know exists, a missing using is the first thing to check.
A namespace does not replace the BBX prefix. Object names still have to be unique within their type across the environment, and apps published to AppSource still have to use a registered affix. So in this series every object has both: a namespace for organisation, and a prefix for uniqueness.
The AL Language extension includes analysers that check your code against Microsoft's rules as you type. Turn them on in .vscode/settings.json:
{
"al.enableCodeAnalysis": true,
"al.codeAnalyzers": [
"${CodeCop}",
"${UICop}",
"${PerTenantExtensionCop}"
],
"al.backgroundCodeAnalysis": true
}
Treat the warnings as things to fix, not noise to ignore. A new project with no warnings is easy to keep clean; a project with 400 warnings never gets cleaned up.
Run git init in the project folder before you write your first object, and push it to Azure DevOps or GitHub the same day. Add a .gitignore that excludes the things that should never be committed:
.alpackages/ .alcache/ .snapshots/ *.app rad.json *.g.xlf.bak
Commit the generated translation file (Translations/*.g.xlf) if you translate the app; otherwise many teams ignore it too. The reasons to do this on day one rather than "when it is finished": you can see exactly what changed when something breaks, you can go back, and the code belongs to the business rather than to one person's laptop. Part 10 covers building and deploying from the repository.
Rather than a hello world message, start with the first two real objects of Pallet Manager: an enum for the pallet material and a skeleton of the pallet type table. Part 2 explains every line of the table in detail and part 4 covers enums properly; for now the aim is to get something publishing.
Create src/Enum/PalletMaterial.Enum.al. Like every complete file in this series, it starts with the namespace line; leave it out if you have decided not to use namespaces.
namespace BusinessBoxed.PalletManager;
enum 50100 "BBX Pallet Material"
{
Extensible = true;
Caption = 'Pallet Material';
value(0; " ")
{
Caption = ' ', Locked = true;
}
value(1; Wood)
{
Caption = 'Wood';
}
value(2; Plastic)
{
Caption = 'Plastic';
}
value(3; Metal)
{
Caption = 'Metal';
}
value(4; Cardboard)
{
Caption = 'Cardboard';
}
}
Then src/Table/PalletType.Table.al:
namespace BusinessBoxed.PalletManager;
table 50100 "BBX Pallet Type"
{
Caption = 'Pallet Type';
DataClassification = CustomerContent;
fields
{
field(1; Code; Code[20])
{
Caption = 'Code';
ToolTip = 'Specifies the code that identifies the pallet type.';
NotBlank = true;
}
field(2; Description; Text[100])
{
Caption = 'Description';
ToolTip = 'Specifies a description of the pallet type.';
}
field(3; Material; Enum "BBX Pallet Material")
{
Caption = 'Material';
ToolTip = 'Specifies what the pallet is made of.';
}
}
keys
{
key(PK; Code)
{
Clustered = true;
}
}
}
There is no page yet (that is part 5), but you can still see the table. In launch.json set "startupObjectType": "Table" and "startupObjectId": 50100, then press F5. Business Central opens the table in a basic list where you can add a couple of records. It is a developer's view, not something users should ever see, but it proves the whole chain works: compile, publish, run.
Ctrl+Alt+F1 in the browser): shows which page and table you are looking at, every field including hidden ones, the filters applied, and which extensions have changed the page. The first thing to open when someone says "where does this field come from?"You now have a project that compiles, publishes to a sandbox and is in source control, with a prefix, an ID range, a namespace and the analysers switched on. In part 2 we build the pallet type table properly: every table property, every field data type and every field property you are likely to need.
Next: part 2: building a table, fields and data types · Back to the series contents
Want help with your first extension? Book a 30-minute call.