Thursday, January 15, 2026

Business Central SaaS Extension Design: Implementing Plant Tracking with AL Event Subscribers

 

Extending Business Central SaaS: Plant Tracking Using AL Extensions 

1️⃣ Problem Statement

In many manufacturing and service-oriented organizations, sales transactions need to be associated with a Plant (or operational unit).

Out of the box, Microsoft Dynamics 365 Business Central SaaS does not provide a standard Plant dimension on Sales documents that automatically flows through:

  • Sales Orders

  • Posted Sales Shipments

  • Posted Sales Invoices

  • Customer Ledger Entries

Challenges

  • No standard Plant master table

  • No Plant selection on Sales Orders

  • No automatic propagation during posting

  • SaaS environment prohibits base object modification

MicroCloud360 needed a clean, upgrade-safe, SaaS-compliant extension to address this requirement.


2️⃣ Functional Requirements

FR-01: Plant Master

  • Maintain a list of Plants

  • Each Plant contains:

    • Plant Id (uppercase, unique)

    • Plant Name

FR-02: Sales Order Integration

  • Add Plant Id field to Sales Order header

  • Field must be optional

  • Users can select only valid Plant Ids (dropdown lookup)

FR-03: Posting Behaviour

When a Sales Order is posted:

  • If Plant Id is blank → do nothing

  • If Plant Id is populated → copy it to:

    • Sales Shipment Header

    • Sales Invoice Header

    • Customer Ledger Entry

FR-04: Visibility

Plant Id must be visible on:

  • Posted Sales Shipment

  • Posted Sales Invoice

  • Customer Ledger Entries


3️⃣ High-Level Architecture Diagram


Explanation

  • Implemented as a pure AL extension

  • No modification of base application objects

  • Uses:

    • Custom tables

    • Table extensions

    • Page extensions

    • Event subscribers

  • Fully SaaS and upgrade safe


4️⃣ Data Model Diagram



Tables Involved

TablePurpose
Plant (Custom)Master data
Sales Header (36)Capture Plant Id
Sales Shipment Header (110)Persist Plant Id
Sales Invoice Header (112)Persist Plant Id
Customer Ledger Entry (21)Financial traceability

Relationship Concept

Plant ↑ Sales Header ↓ Posted Sales Docs ↓ Customer Ledger Entry

5️⃣ Posting Flow Diagram



Flow Description

Sales Order (Plant selected) ↓ Sales-Post Codeunit ↓ Sales Shipment Header ↓ Sales Invoice Header ↓ Customer Ledger Entry

Plant Id flows only if populated.


6️⃣ Event-Driven Design (Why This Matters)

Business Central SaaS does not allow direct modification of standard posting codeunits.

MicroCloud360 Approach

  • Subscribe to standard posting events

  • Inject logic after standard processing

  • Avoid breaking changes during upgrades

Events Used

EventPurpose
OnAfterInsertShipmentHeaderCopy Plant Id
OnAfterInsertInvoiceHeaderCopy Plant Id
Cust. Ledger Entry OnAfterInsertPopulate ledger

This follows Microsoft’s recommended SaaS extension pattern.


7️⃣ Key Code Snippets

Plant Id on Sales Header

field(50500; "Plant Id"; Code[10]) { TableRelation = "Plant"."Plant Id"; trigger OnValidate() begin "Plant Id" := UpperCase("Plant Id"); end; }

Posting Event Subscriber (Example)

[EventSubscriber(ObjectType::Codeunit, Codeunit::"Sales-Post", 'OnAfterInsertInvoiceHeader', '', false, false)] local procedure OnAfterInsertInvoiceHeader( var SalesHeader: Record "Sales Header"; var SalesInvHeader: Record "Sales Invoice Header") begin if SalesHeader."Plant Id" <> '' then SalesInvHeader."Plant Id" := SalesHeader."Plant Id"; end;

8️⃣ Testing & Results

Test Scenarios

ScenarioResult
Plant creationSaved in uppercase
Sales Order without PlantPosts successfully
Sales Order with PlantPlant copied to all posted records
Ledger reviewPlant Id visible

Outcome

  • No posting errors

  • No performance impact

  • No base code changes

  • Fully SaaS compliant


9️⃣ Best-Practice Notes

AL & SaaS Best Practices Used

  • One object per file

  • Unique object IDs

  • Shared field ID across tables

  • Event subscribers instead of code modification

  • No COMMIT statements

  • Optional business logic (non-blocking)

Naming Conventions

ItemConvention
PrefixCompany prefix (MicroCloud360)
Object namesMeaningful & < 30 chars
Field IDsConsistent across tables
Extension nameBusiness-driven

Upgrade Safety

  • Uses published events only

  • No breaking dependencies

  • Ready for future BC releases


🔚 Technical Implementation 

Recommended .al objects

Here is a clean, safe ID plan inside 50500–50599:

Tables

  • 50500 = table "MC360 Plant"

TableExtensions

  • 50501 = Sales Header Ext

  • 50502 = Sales Shipment Header Ext

  • 50503 = Sales Invoice Header Ext

  • 50504 = Cust. Ledger Entry Ext

PageExtensions

  • 50520 = Sales Order Ext

  • 50521 = Customer Ledger Entries Ext

  • 50522 = Posted Sales Shipment Ext

  • 50523 = Posted Sales Invoice Ext

Pages

  • 50510 = Plant List

  • 50511 = Plant Card

Codeunit

  • 50540 = Plant Posting Subs

PermissionSet

  • 50550 = MC360 PLANT


Files to be created under src folder

├── Tables/MC360Plant.Table.al

├── TableExtensions/MC360SalesHeaderExt.TableExt.al

├── TableExtensions/MC360SalesShipmentHeaderExt.TableExt.al

├── TableExtensions/MC360SalesInvoiceHeaderExt.TableExt.al

├── TableExtensions/MC360CustLedgerEntryExt.TableExt.al

├── PageExtensions/MC360SalesOrderExt.PageExt.al

├── PageExtensions/MC360CustLedgerEntriesExt.PageExt.al

├── PageExtensions/MC360PostedSalesShipmentExt.PageExt.al

├── PageExtensions/MC360PostedSalesInvoiceExt.PageExt.al

├── Pages/MC360PlantList.Page.al

├── Pages/MC360PlantCard.Page.al

└── PermissionSets/MC360Plant.PermissionSet.al


src/Tables/MC360Plant.Table.al

table 50500 "MC360 Plant" { Caption = 'Plant'; DataClassification = CustomerContent; fields { field(1; "Plant Id"; Code[10]) { Caption = 'Plant Id'; DataClassification = CustomerContent; NotBlank = true; trigger OnValidate() begin "Plant Id" := UpperCase("Plant Id"); end; } field(2; "Plant Name"; Text[30]) { Caption = 'Plant Name'; DataClassification = CustomerContent; } } keys { key(PK; "Plant Id") { Clustered = true; } } }

src/TableExtensions/MC360SalesHeaderExt.TableExt.al

tableextension 50500 "MC360 Sales Header Ext" extends "Sales Header" { fields { field(50500; "Plant Id"; Code[10]) { Caption = 'Plant Id'; DataClassification = CustomerContent; TableRelation = "MC360 Plant"."Plant Id"; trigger OnValidate() begin "Plant Id" := UpperCase("Plant Id"); end; } } }

src/TableExtensions/MC360SalesShipmentHeaderExt.TableExt.al

tableextension 50501 "MC360 Sales Shpt. Header Ext" extends "Sales Shipment Header" { fields { field(50500; "Plant Id"; Code[10]) { Caption = 'Plant Id'; DataClassification = CustomerContent; } } }

src/TableExtensions/MC360SalesInvoiceHeaderExt.TableExt.al

tableextension 50502 "MC360 Sales Inv. Header Ext" extends "Sales Invoice Header" { fields { field(50500; "Plant Id"; Code[10]) { Caption = 'Plant Id'; DataClassification = CustomerContent; } } }

src/TableExtensions/MC360CustLedgerEntryExt.TableExt.al

tableextension 50503 "MC360 Cust. Ledg. Entry Ext" extends "Cust. Ledger Entry" { fields { field(50500; "Plant Id"; Code[10]) { Caption = 'Plant Id'; DataClassification = CustomerContent; } } }

src/PageExtensions/MC360SalesOrderExt.PageExt.al

pageextension 50500 "MC360 Sales Order Ext" extends "Sales Order" { layout { addlast(General) { field("Plant Id"; Rec."Plant Id") { ApplicationArea = All; ToolTip = 'Select the Plant Id for this sales order (optional).'; } } } }

src/PageExtensions/MC360CustLedgerEntriesExt.PageExt.al

pageextension 50501 "MC360 Cust. Ledger Entries Ext" extends "Customer Ledger Entries" { layout { // In standard objects this is usually the repeater control name. // If your symbols show a different repeater/container name, replace Control1 accordingly. addlast(Control1) { field("Plant Id"; Rec."Plant Id") { ApplicationArea = All; ToolTip = 'Shows the Plant Id carried from the originating sales document.'; } } } }

src/PageExtensions/MC360PostedSalesShipmentExt.PageExt.al

pageextension 50502 "MC360 Posted Sales Shipment Ext" extends "Posted Sales Shipment" { layout { addlast(General) { field("Plant Id"; Rec."Plant Id") { ApplicationArea = All; ToolTip = 'Shows the Plant Id copied from the Sales Order during posting.'; } } } }

src/PageExtensions/MC360PostedSalesInvoiceExt.PageExt.al

pageextension 50503 "MC360 Posted Sales Invoice Ext" extends "Posted Sales Invoice" { layout { addlast(General) { field("Plant Id"; Rec."Plant Id") { ApplicationArea = All; ToolTip = 'Shows the Plant Id copied from the Sales Order during posting.'; } } } }

src/Pages/MC360PlantList.Page.al

page 50510 "MC360 Plant List" { PageType = List; SourceTable = "MC360 Plant"; Caption = 'Plants'; ApplicationArea = All; UsageCategory = Lists; layout { area(content) { repeater(General) { field("Plant Id"; Rec."Plant Id") { ApplicationArea = All; ToolTip = 'Specifies the Plant Id.'; } field("Plant Name"; Rec."Plant Name") { ApplicationArea = All; ToolTip = 'Specifies the Plant Name.'; } } } } actions { area(Processing) { action(Card) { Caption = 'Card'; ApplicationArea = All; Image = EditLines; RunObject = page "MC360 Plant Card"; RunPageLink = "Plant Id" = field("Plant Id"); } } } }

src/Pages/MC360PlantCard.Page.al

page 50511 "MC360 Plant Card" { PageType = Card; SourceTable = "MC360 Plant"; Caption = 'Plant'; ApplicationArea = All; UsageCategory = Administration; layout { area(content) { group(General) { field("Plant Id"; Rec."Plant Id") { ApplicationArea = All; ToolTip = 'Specifies the Plant Id.'; } field("Plant Name"; Rec."Plant Name") { ApplicationArea = All; ToolTip = 'Specifies the Plant Name.'; } } } } }

src/PermissionSets/MC360Plant.PermissionSet.al

permissionset 50500 "MC360 PLANT" { Caption = 'MC360 Plant Tracking'; Assignable = true; Permissions = tabledata "MC360 Plant" = RIMD, table "MC360 Plant" = X; }

src/Codeunits/MC360PlantPostingSubs.Codeunit.al

codeunit 50504 "MC360 Plant Posting Subs" { Subtype = Normal; // Copy Sales Header Plant Id -> Sales Shipment Header [EventSubscriber(ObjectType::Codeunit, Codeunit::"Sales-Post", 'OnAfterInsertShipmentHeader', '', false, false)] local procedure OnAfterInsertShipmentHeader(var SalesHeader: Record "Sales Header"; var SalesShptHeader: Record "Sales Shipment Header") begin if SalesHeader."Plant Id" <> '' then SalesShptHeader."Plant Id" := SalesHeader."Plant Id"; end; // Copy Sales Header Plant Id -> Sales Invoice Header [EventSubscriber(ObjectType::Codeunit, Codeunit::"Sales-Post", 'OnAfterInsertInvoiceHeader', '', false, false)] local procedure OnAfterInsertInvoiceHeader(var SalesHeader: Record "Sales Header"; var SalesInvHeader: Record "Sales Invoice Header") begin if SalesHeader."Plant Id" <> '' then SalesInvHeader."Plant Id" := SalesHeader."Plant Id"; end; // Copy Posted Sales Invoice Plant Id -> Cust. Ledger Entry [EventSubscriber(ObjectType::Table, Database::"Cust. Ledger Entry", 'OnAfterInsertEvent', '', false, false)] local procedure CustLedgEntryOnAfterInsert(var Rec: Record "Cust. Ledger Entry"; RunTrigger: Boolean) var SalesInvHeader: Record "Sales Invoice Header"; begin if (Rec."Document No." = '') or (Rec."Plant Id" <> '') then exit; // Works when CLE Document No. = Posted Sales Invoice No. if SalesInvHeader.Get(Rec."Document No.") then begin if SalesInvHeader."Plant Id" <> '' then begin Rec."Plant Id" := SalesInvHeader."Plant Id"; Rec.Modify(false); end; end; end; }
Place this file under:
src/Codeunits/MC360PlantPostingSubs.Codeunit.al


And expand your app.json idRange to cover this object too (you already have 50500–50599, so you’re fine).


Wednesday, January 14, 2026

NAV NAS vs Business Central Job Queue – Conceptual Comparison

 Below is a clear, side-by-side comparison of NAV NAS vs Business Central Job Queue, written in plain language, plus a visual flow diagram you can embed in your blog. This is ideal for developers and consultants transitioning from NAV to Business Central.

NAV NAS vs Business Central Job Queue – Conceptual Comparison


Overview

Background processing in Microsoft Dynamics NAV and Microsoft Dynamics 365 Business Central serves the same purpose—running tasks without user interaction—but the architecture and execution model are fundamentally different.

Understanding this difference is critical during NAV → Business Central upgrades.


NAV NAS (Navision Application Server)

How It Works

  • NAS is a separate Windows service

  • Runs with startup parameter:

    JOBQUEUE
  • Continuously polls the Job Queue Entry table

  • Executes reports and codeunits headlessly

  • Requires explicit handling of UI logic (GUIALLOWED())

Key Characteristics

  • One NAS instance per role

  • Manual installation and configuration

  • Tight coupling to classic NAV architecture

  • Heavily dependent on C/AL patterns

  • Common source of upgrade complexity

Typical Use Cases

  • Adjust Cost – Item Entries

  • Data synchronization jobs

  • Periodic batch processing

  • Legacy automation scenarios


Business Central Job Queue (Modern Model)

How It Works

  • Fully built into the Business Central Server

  • No separate NAS service

  • Uses Job Queue Dispatcher

  • Runs tasks using background sessions

  • Cloud-ready and SaaS-compliant

Key Characteristics

  • Automatically managed by the platform

  • Scales horizontally in SaaS

  • Native support for AL extensions

  • Strong logging and retry mechanisms

  • No GUI handling required

Typical Use Cases

  • Background processing in SaaS

  • Scheduled AL codeunits

  • Integration polling jobs

  • Event-driven automation


Side-by-Side Comparison Table

AspectNAV NASBusiness Central Job Queue
Execution ModelSeparate Windows ServiceBuilt-in Server Component
DeploymentManualAutomatic
UI HandlingGUIALLOWED() requiredNot applicable
ArchitectureC/AL, classicAL, extension-based
Cloud Support❌ Not supported✅ Native
ScalabilityLimitedHigh
MonitoringBasic logsAdvanced telemetry
Upgrade ComplexityHighLow
SaaS Compatibility

Visual Execution Flow Comparison

NAV NAS Flow

Job Queue Entry ↓ Job Queue Setup (Active?) ↓ NAS Service (JOBQUEUE) ↓ Execute Report / Codeunit ↓ Job Queue Log Entry

Business Central Job Queue Flow

Job Queue Entry ↓ Job Queue Dispatcher ↓ Background SessionExecute AL Codeunit ↓ Telemetry & Logs

Key Architectural Shift (Most Important Difference)

NAV NAS

  • External

  • Polling-based

  • Manual lifecycle

  • Legacy-friendly

Business Central

  • Internal

  • Event- and session-based

  • Platform-managed

  • Cloud-first

👉 This shift removes the need for:

  • NAS installation

  • Service monitoring

  • GUI suppression logic

  • Manual recovery processes


What This Means During an Upgrade

When upgrading from NAV to Business Central:

  • NAS logic must be refactored

  • Codeunit 1 customisations must move to management codeunits

  • Job Queue entries must be reviewed for SaaS compliance

  • Long-running jobs may need splitting or async design

This is one of the most important technical transitions in a NAV → Business Central upgrade.


Practical Migration Guidance

NAV PatternBusiness Central Equivalent
NAS ServiceJob Queue Dispatcher
GUIALLOWED()Background Session
Codeunit 1 logicAL Management Codeunits
Manual schedulingPlatform-managed scheduling

Summary

While both systems solve the same problem—background processing—they belong to entirely different architectural eras.

  • NAV NAS = legacy, service-based, on-prem oriented

  • Business Central Job Queue = modern, cloud-native, extension-driven

Understanding this difference helps avoid bad upgrade decisions and ensures a clean, future-proof Business Central solution.

Developing Business Central Extensions : Naming conventions and VS Code launch.json strategy for multi-app repo

 Here’s a battle-tested, simple structure for one repo hosting many Business Central extensions, plus a clean naming/ID/range/versioning standard that won’t hurt you later when you add CI/CD.


Recommended repo structure (one repo, many apps)

Option A (recommended): One folder per extension under /apps

Works great with pipelines later, easy to clone anywhere.

bc-extensions/

  README.md

  .gitignore

  .editorconfig                 (optional)

  .azure-pipelines/             (later)

 

  apps/

    CustomerExt-01/

      src/                      (AL project lives here)

        app.json

        .vscode/

          launch.json

          settings.json

        HelloWorld.al           (or your objects)

        tables/

        pageextensions/

        codeunits/

      docs/                     (optional: specs)

        overview.md

      test/                     (optional: test app later)

 

    VendorExt-01/

      src/

        app.json

        .vscode/...

        ...

 

  tools/                        (optional)

    scripts/

Why this is best

  • Each app is self-contained.
  • Developers open apps/<AppName>/src and they’re productive immediately.
  • CI/CD can build all apps or only changed apps.

Option B: Monorepo with shared “workspace” and common VS Code config

Only do this if your team is mature with AL tooling; otherwise it adds complexity.


VS Code launch.json strategy for multi-app repo

You have two clean choices:

Choice 1 (simplest): each app has its own .vscode/launch.json

  • Each apps/<AppName>/src/.vscode/launch.json targets the correct sandbox.
  • No confusion—open that app’s src folder and publish.

Choice 2 (more advanced): one shared .vscode at repo root

  • Use variables / multiple configurations, but it’s easier to mess up.
  • I’d avoid until CI/CD is in.

Naming standards

Repo name

  • bc-extensions (if it hosts multiple)
  • OR bc-<customer>-extensions (if dedicated to one customer)

App folder name

Use: PascalCase + short suffix:

  • CustomerExt-01
  • SalesEnhancements-01
  • IntegrationPack-01

app.json name

Match folder:

  • "name": "CustomerExt-01"

app.json publisher

Use one publisher consistently (important later for upgrades):

  • "publisher": "MicroCloud 360"

Avoid “Default Publisher” once you start real work.


App IDs (GUIDs)

Rule

  • One unique GUID per extension app
  • Never change it after first release (changing ID breaks upgrade path)

Practical approach

  • Let AL generate it once.
  • Store it forever in app.json.

Tip: Keep a small registry in /README.md or /docs/apps.md listing:

  • App Name
  • App ID (GUID)
  • Range
  • Purpose

Example:

App

App ID

Range

CustomerExt-01

xxxxxxxx-xxxx-...

50100–50149


Object range strategy (IDs)

You need a clean plan so multiple apps never collide.

Option A (simple and scalable): allocate blocks per app

Example allocation (easy to remember):

  • CustomerExt-01: 50100–50149
  • VendorExt-01: 50150–50199
  • SalesExt-01: 50200–50249
  • IntegrationExt-01: 50250–50299

This works well if you keep apps small.

Option B (more enterprise): allocate blocks per domain

  • Customers: 50100–50999
  • Vendors: 51000–51999
  • Sales: 52000–52999

Then each app inside the domain takes a smaller block.

Recommendation for you

Since you’re building multiple discrete extensions: Option A (block per app) is the cleanest.


Versioning standard (app.json version)

Use Semantic-ish versioning

MAJOR.MINOR.PATCH.BUILD

  • MAJOR: breaking change / major release (rare)
  • MINOR: new feature
  • PATCH: bugfix
  • BUILD: optional increment for internal builds

Examples:

  • 1.0.0.0 first release
  • 1.0.1.0 bugfix release
  • 1.1.0.0 new features
  • 2.0.0.0 breaking change

Practical rule (easy to follow)

  • For each change you deploy to sandbox: bump PATCH
  • For new features: bump MINOR
  • For hotfixes: bump PATCH
  • Ignore BUILD unless CI uses it later

Runtime / platform / application fields

Keep runtime aligned with your BC version

If your SaaS is current (v25/26/27 etc), typical runtime is 13+, 14+, 15+, 16+, 17+ depending on the environment.

Rule:

  • Don’t guess—use what your sandbox expects (your earlier issue was caused by runtime mismatch).

Recommended approach

  • After creating the project, download symbols successfully, then keep:
    • "platform" and "application" consistent with your environment symbols.
  • If symbols download is ok, your platform/application is typically fine.

Git branching / workflow (simple but correct)

Branches

  • main (protected ideally)
  • feature branches per change:
    • feature/customer-fields
    • bugfix/customer-card-tooltip

Commit message standard

  • CustomerExt-01: Add MC Reviewed fields
  • VendorExt-01: Fix validation in posting

Tagging (later)

When you release a version:

  • tag: CustomerExt-01/v1.1.0

.gitignore for multi-app repo

At repo root:

# AL generated artifacts (in any app)

**/.alpackages/

**/.alcache/

**/.snapshots/

**/*.app

 

# VS Code - keep only shared config if you want

**/.vscode/*

!**/.vscode/launch.json

!**/.vscode/settings.json

 

# OS

.DS_Store

Thumbs.db


“Open the right folder” rule (avoids confusion)

When working on an app:
Open folder:

  • apps/CustomerExt-01/src

Not the repo root, not /apps, not src of another app.


Suggested “starter template” per app (clean folders)

Inside each src:

src/

  app.json

  .vscode/

    launch.json

    settings.json

  tableextensions/

  pageextensions/

  codeunits/

  permissionsets/

  profiles/              (optional)

How to develop Business Central SaaS Extension from Scratch (Repo-backed, Azure DevOps)

 Business Central SaaS Extension from Scratch (Repo-backed, Azure DevOps)

Prerequisites (must be done once per laptop)

  1. Install Visual Studio Code.
  2. Install Git for Windows.
  3. In VS Code → Extensions, install:
    • AL Language (Microsoft)
    • Azure Repos (Microsoft) (optional but helpful)
  4. Make sure you can sign in to:
    • Azure DevOps
    • Business Central SaaS tenant (Microsoft Entra ID / Azure AD)

Part A — Create the Azure DevOps Repo (Empty)

Step A1 — Create a new project (only if needed)

If you already have a DevOps Project, skip this.

  1. Azure DevOps → New Project
  2. Name: e.g. bc-bcextension-01
  3. Visibility: Private
  4. Create

Step A2 — Create an empty Git repository

  1. Go to ReposFiles
  2. Select New repository
  3. Type: Git
  4. Name: e.g. bc-bcextension-01
  5. Do NOT initialize with README
  6. Do NOT add .gitignore
  7. Create

Result: Repo page shows: “repo is empty”.


Part B — Clone Repo to Your PC

Step B1 — Create a local working folder

Example:

  • Create folder: C:\BC\

Step B2 — Clone the empty repo

Choose either method:

Option 1 (simple): Clone in VS Code

  1. In Azure DevOps repo page → click Clone in VS Code
  2. Pick local folder:
    • C:\BC\bc-bcextension-01
  3. VS Code opens the repo

Option 2: Clone using Git command line

  1. Copy HTTPS clone URL from Azure DevOps
  2. Run in terminal:
  3. cd C:\BC
  4. git clone <paste-clone-url> bc-bcextension-01
  5. Open VS Code → File → Open Folder → C:\BC\bc-bcextension-01

Result: Local folder contains a .git folder and nothing else.


Part C — Add .gitignore (Before generating AL project)

Step C1 — Create .gitignore at repo root

Create file here:

  • C:\BC\bc-bcextension-01\.gitignore

Paste this content:

# Business Central / AL generated artifacts

.alpackages/

.alcache/

.snapshots/

*.app

 

# VS Code (keep only shared config)

.vscode/*

!.vscode/launch.json

!.vscode/settings.json

 

# OS files

.DS_Store

Thumbs.db

Step C2 — Commit and push .gitignore

In VS Code Source Control:

  1. Stage .gitignore
  2. Commit message: Add gitignore
  3. Push / Sync

Repo now has 1 commit.


Part D — Create the AL Project (Scaffold) in the Repo

Key rule (IMPORTANT)

The folder you select for AL: Go! must NOT exist yet.

We will generate the AL project into a new folder called src.

Step D1 — Run AL: Go!

  1. Open VS Code with repo root:
    • C:\BC\bc-bcextension-01
  2. Press: Ctrl + Shift + P
  3. Run: AL: Go!
  4. When it asks for a folder location:
    • Enter/select:
    • C:\BC\bc-bcextension-01\src
    • Do NOT create src manually. Let AL create it.

Result: VS Code creates src folder and generates project files inside.


Part E — Configure the BC SaaS Connection (launch.json)

Step E1 — Open the generated launch.json

File path:

  • C:\BC\bc-bcextension-01\src\.vscode\launch.json

Replace content with your SaaS sandbox config (example):

{

  "version": "0.2.0",

  "configurations": [

    {

      "name": "BC SaaS Sandbox",

      "type": "al",

      "request": "launch",

      "server": "https://businesscentral.dynamics.com",

      "tenant": "7f9c2a8e-4b6d-4c3a-9e51-0d2a9b8c6f41",

      "environmentName": "Sandbox",

      "authentication": "MicrosoftEntraID",

      "startupObjectType": "Page",

      "startupObjectId": 22,

      "breakOnError": "All",

      "launchBrowser": true

    }

  ]

}

Note: "tenant": "7f9c2a8e-4b6d-4c3a-9e51-0d2a9b8c6f41" provided here is only for illustration; a valid tenant id must be provided here.

This ensures it uses SaaS + Entra ID, not On-Prem.


Part F — Set app.json Correctly (Runtime, IDs, name)

Step F1 — Edit app.json

File path:

  • C:\BC\bc-bcextension-01\src\app.json

Set these fields clearly:

  • name = your extension name
  • publisher = your company/publisher
  • idRanges = your object range
  • runtime must match your target version

Example:

{

  "id": "YOUR-GUID-HERE",

  "name": "BCExtension-01",

  "publisher": "MicroCloud 360",

  "version": "1.0.0.0",

  "brief": "Business Central Extension 01",

  "description": "Business Central SaaS Extension",

  "dependencies": [],

  "idRanges": [

    { "from": 50100, "to": 50149 }

  ],

  "runtime": "17.0",

  "features": [ "NoImplicitWith" ]

}

(Use a new GUID for each app—AL usually generates it for you.)


Part G — Download Symbols (Required before coding properly)

Step G1 — Download Symbols

  1. Open the AL project folder:
    • In VS Code, ensure you are working inside src
  2. Ctrl + Shift + P
  3. Run: AL: Download Symbols

Result:

  • .alpackages gets populated
  • Errors like “cannot find Customer List” disappear

Part H — Build + Publish to BC SaaS Sandbox

Step H1 — Publish from VS Code

  1. Ctrl + Shift + P
  2. Run: AL: Publish

You will be asked to login (browser sign-in) the first time.

Result:

  • Extension is deployed to your Sandbox environment.

Part I — Verify in Business Central

Step I1 — Confirm extension is installed

In BC:

  1. Search: Extension Management
  2. Find your extension under:
    • Installed / Published list (depends on role/permissions)
  3. Confirm version number matches your app.json

Step I2 — If fields/pages not visible

If you added fields to a page extension:

  1. Open the page
  2. ⚙️ Settings → Personalize
  3. Add fields to columns / layout if needed
  4. If still hidden:
    • ⚙️ Settings → Reset personalization

Part J — Commit Code to Repo (Proper workflow)

Step J1 — Commit generated project + config

In VS Code Source Control:

  1. Stage changes (app.json, launch.json, AL files, etc.)
  2. Commit message: Initial AL project scaffold
  3. Push

Now your repo contains the full AL project.


Part K — Work from Another Laptop (Proves repo-backed setup)

On a second laptop:

  1. Install prerequisites (VS Code, Git, AL extension)
  2. Clone repo to local folder
  3. Open repo root in VS Code
  4. Open src
  5. AL: Download Symbols
  6. AL: Publish

Same project works anywhere.

Business Central SaaS Extension Design: Implementing Plant Tracking with AL Event Subscribers

  Extending Business Central SaaS: Plant Tracking Using AL Extensions  1️⃣ Problem Statement In many manufacturing and service-oriented or...