← Back to list

Building Your First Real AL Objects: Tables, Pages, and a Codeunit in Business Central

In Part 3 I published an empty extension to the local bcserver container just to prove the pipeline worked. It compiled, it deployed, and…

Albert Assaad · 2026-07-06 06:53 · 53 claps · 5.8 min read
#business-central #programming #microsoft #dynamics-365 #microsoft-dynamics-nav
Open on Medium ↗
Wiki topics: 💻 · Programming ☁️ · DevOps & Cloud

Building Your First Real AL Objects: Tables, Pages, and a Codeunit in Business Central

In Part 3 I published an empty extension to the local bcserver container just to prove the pipeline worked. It compiled, it deployed, and it did absolutely nothing. This time we fix that.

By the end of this part you’ll have a table that stores data, a list page to browse it, a card page to create and edit records, and an action button backed by a codeunit that actually changes a record. You’ll find all of it running inside your container, searchable from Tell Me, like any other BC page.

I’m building a small “Project Task” object because it’s just enough to touch every concept without turning into a tutorial about project management. Swap the fields for whatever fits your own head.

1- The Table

A table is where the data lives. Everything else in this part points back to it.

Create a new file in your AL project. I name table files Tab<ID>.<Name>.al, so this one is Tab50120.ProjectTask.al. The object ID 50120 sits inside the free range (50000–99999) that Microsoft leaves open for per-tenant extensions. Pick an ID that no other app in your container already uses, since BC won't let two apps define a table with the same ID.

table 50120 "Project Task"
{
    DataClassification = ToBeClassified;

    fields
    {
        field(1; "No."; Code[20])
        {
            Caption = 'No.';
        }
        field(2; Description; Text[100])
        {
            Caption = 'Description';
        }
        field(3; Status; Enum "Project Task Status")
        {
            Caption = 'Status';
        }
        field(4; "Due Date"; Date)
        {
            Caption = 'Due Date';
        }
        field(5; "Completed On"; Date)
        {
            Caption = 'Completed On';
            Editable = false;
        }
    }

    keys
    {
        key(PK; "No.")
        {
            Clustered = true;
        }
    }
}

A few things worth noticing here:

a- Every field has a numeric ID, a name, and a data type. The IDs only need to be unique within the table.

b- "No." is the primary key, defined down in the keys section. Clustered = true tells SQL Server how to physically order the rows.

c- I used Enum for Status instead of the older Option type. Enums are extensible and the modern recommendation, so let's build the habit now.

d- "Completed On" is Editable = false because the user shouldn't type it. The codeunit in section 4 will set it.

If you save the table now, it won’t compile. The Status field points at an Enum "Project Task Status" that doesn't exist yet, so AL throws an error until you create the enum in the next section. That's expected; don't try to fix it here.

2- The Enum

The Status field references an enum that doesn't exist yet, so the table won't compile until we create it. Make a file Enum50121.ProjectTaskStatus.al:

enum 50121 "Project Task Status"
{
    Extensible = true;

    value(0; Open) { Caption = 'Open'; }
    value(1; "In Progress") { Caption = 'In Progress'; }
    value(2; Completed) { Caption = 'Completed'; }
}

Extensible = true means another extension could add its own status values later without editing this file. That's the whole point of enums over options.

3- The Pages

A table you can’t see is useless. We need two pages: a list to browse records and a card to edit one at a time.

a- The List Page

File Pag50122.ProjectTaskList.al:

page 50122 "Project Task List"
{
    PageType = List;
    ApplicationArea = All;
    UsageCategory = Lists;
    SourceTable = "Project Task";
    CardPageId = "Project Task Card";

    layout
    {
        area(Content)
        {
            repeater(Group)
            {
                field("No."; Rec."No.") { ApplicationArea = All; }
                field(Description; Rec.Description) { ApplicationArea = All; }
                field(Status; Rec.Status) { ApplicationArea = All; }
                field("Due Date"; Rec."Due Date") { ApplicationArea = All; }
            }
        }
    }
}

Two properties make this page findable in the running client:

  • UsageCategory = Lists puts the page in the Tell Me search results.
  • ApplicationArea = All makes the page and its fields visible regardless of which BC application areas are enabled.

CardPageId links the list to the card, so double-clicking a row opens it for editing.

Same situation as the table and enum: this page won’t compile on its own. CardPageId = "Project Task Card" references a card page we haven't built yet, so AL will flag it until you create the card in the next section. Keep going.

b- The Card Page

File Pag50123.ProjectTaskCard.al:

page 50123 "Project Task Card"
{
    PageType = Card;
    ApplicationArea = All;
    SourceTable = "Project Task";

    layout
    {
        area(Content)
        {
            group(General)
            {
                field("No."; Rec."No.") { ApplicationArea = All; }
                field(Description; Rec.Description) { ApplicationArea = All; }
                field(Status; Rec.Status) { ApplicationArea = All; }
                field("Due Date"; Rec."Due Date") { ApplicationArea = All; }
                field("Completed On"; Rec."Completed On") { ApplicationArea = All; }
            }
        }
    }
}

The card has no UsageCategory on purpose. You don't search for a single card from Tell Me; you reach it through the list.

4- The Codeunit and the Action

Now for the part that does something. I want a button on the card that marks a task complete: it flips the status to Completed and stamps today's date into "Completed On".

I’ll put the logic in a codeunit rather than directly in the page. Keeping behavior out of the UI is a habit that pays off the moment you want to call the same logic from somewhere else.

File Cod50124.ProjectTaskMgt.al:

codeunit 50124 "Project Task Mgt."
{
    procedure CompleteTask(var ProjectTask: Record "Project Task")
    begin
        ProjectTask.Status := ProjectTask.Status::Completed;
        ProjectTask."Completed On" := Today();
        ProjectTask.Modify(true);
    end;
}

Modify(true) writes the record back to the database, and the true runs the table's modify triggers and validation. Passing the record var means the codeunit edits the caller's copy directly.

Now add the action to the card page. Drop an actions section into Pag50123.ProjectTaskCard.al, right after the layout block closes:

actions
    {
        area(Processing)
        {
            action(CompleteTask)
            {
                ApplicationArea = All;
                Caption = 'Complete Task';
                Image = Completed;

                trigger OnAction()
                var
                    ProjectTaskMgt: Codeunit "Project Task Mgt.";
                begin
                    ProjectTaskMgt.CompleteTask(Rec);
                    CurrPage.Update(false);
                end;
            }
        }
    }

Walking through the trigger:

a- ProjectTaskMgt is a variable of the codeunit type, declared in the var block of the trigger.

b- ProjectTaskMgt.CompleteTask(Rec) passes the page's current record (Rec) into the procedure.

c- CurrPage.Update(false) refreshes the page so you see the new status and date without reopening the card. The false means don't save again; the codeunit already did.

The full source for this part is on GitHub if you want to compare against your own: github.com/AlbertAssaad/TaskProject.

5- Publish and Find It

Same publish step as Part 3: press F5, or run AL: Publish from the command palette. The extension compiles, deploys to the container, and BC opens in your browser.

Once it loads:

1- Press Alt+Q to open Tell Me, or click the magnifying-glass icon in the top-right of the page. (This is not the same as Ctrl+F Find, which only searches within the current page.) 2- Type "Project Task". The list page appears because of UsageCategory = Lists.

2- Type “Project Task”. The list page appears because of UsageCategory = Lists.

3- Open it, hit New, and create a couple of tasks. Give them a number, a description, and a due date. Leave the status on Open.

4- Open one task to its card and click Complete Task.

The status flips to Completed and Completed On fills with today's date, read-only, exactly as the table and codeunit set it up. That's your own object, with your own logic, running in Business Central.

What’s Next

In Part 5 I’ll stop building separate objects and start modifying Microsoft’s. We’ll add a field to the Customer table with a tableextension, surface it on the Customer Card with a pageextension, and get into events and subscribers, the pattern that makes AL extensions actually production-grade.

If anything didn’t compile, check that your object IDs are in the 50000–99999 range and that the enum file exists before the table tries to reference it. Drop a comment if you hit something and I'll take a look.


메타데이터
post_id
4e1bfe3823f9
slug
building-your-first-real-al-objects-tables-pages-and-a-codeunit-in-business-central-4e1bfe3823f9
url
https://medium.com/@albertassaad/building-your-first-real-al-objects-tables-pages-and-a-codeunit-in-business-central-4e1bfe3823f9
canonical_url
https://medium.com/@albertassaad/building-your-first-real-al-objects-tables-pages-and-a-codeunit-in-business-central-4e1bfe3823f9
author_url
https://medium.com/@albertassaad
status
ok
fetched_at
2026-07-07 05:46:58