Help us improve
Share bugs, ideas, or general feedback.
From al-dev-toolkit
AL coding conventions — variable naming, declaration order, self-reference, error labels, file organization. Read before writing or reviewing AL code.
npx claudepluginhub andreipopaarggo/al-dev-toolkit --plugin al-dev-toolkitHow this skill is triggered — by the user, by Claude, or both
Slash command
/al-dev-toolkit:al-coding-styleThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
| Scope | Prefix | Example |
Guides technical evaluation of code review feedback: read fully, restate for understanding, verify against codebase, respond with reasoning or pushback before implementing.
Share bugs, ideas, or general feedback.
| Scope | Prefix | Example |
|---|---|---|
| Global | (none) | Customer, totalAmount, CannotPostErr |
| Local | _ | _Customer, _totalAmount, _CannotPostErr |
| Temporary | Temp | Temp_SalesLine, _Temp_SalesLine |
| Parameter | p | pCustomerNo, pSalesHeader |
| Return value (named) | r | rSuccess, rAmount |
Global variables (codeunit/table-level var block) get NO prefix. Labels follow the same prefix rules as other variables.
Declare variables and Labels at the narrowest scope where they are used. If something is referenced by only one procedure, put it in that procedure's var block — not in the object-level var.
| Used from | Declare where |
|---|---|
| Multiple procedures in the object | Object-level var (global) |
| One procedure only | That procedure's var (local) |
This applies to variables, Labels, and temp records. A Label used by only one procedure moves to that procedure's local var and picks up the _ prefix per the prefix rules above.
// WRONG — RatingCalcErr used only inside CalculateRating, should be local
codeunit 50100 "Rating Management"
{
var
RatingCalcErr: Label 'Rating cannot be calculated for %1', Comment = '%1 = Customer No.';
procedure CalculateRating(pCustomerNo: Code[20]): Decimal
begin
Error(RatingCalcErr, pCustomerNo);
end;
}
// RIGHT — local to the one procedure that uses it
procedure CalculateRating(pCustomerNo: Code[20]): Decimal
var
_RatingCalcErr: Label 'Rating cannot be calculated for %1', Comment = '%1 = Customer No.';
begin
Error(_RatingCalcErr, pCustomerNo);
end;
| Type | Casing | Examples |
|---|---|---|
| Object types (Record, Codeunit, Page, etc.) | Capital | _Customer, pSalesHeader |
| Primitive types (Decimal, Boolean, Text, etc.) | lowercase | _totalAmount, pPostingDate |
Variables in var blocks must be ordered by type — complex/object types first, then simple types:
Record → Report → Codeunit → XmlPort → Page → Query → Notification → BigText → DateFormula → RecordId → RecordRef → FieldRef → FilterPageBuilder → then simple types (Text, Code, Integer, Decimal, Boolean, Date, Label, etc.)
Use named return values (r prefix) for complex functions that build up or operate on the return value. Simple one-liner functions can use exit(value).
// Named return for complex function
procedure CalculateCustomerBalance(pCustomerNo: Code[20]) rBalance: Decimal
// Simple function — standard return is fine
local procedure IsEnabled(): Boolean
table 50100 "ACME Customer Rating"
page 50100 "ACME Customer Rating Card"
codeunit 50100 "ACME Rating Management"
Captions must never include the mandatory affix. The affix is part of the object/field name, not the caption.
// GOOD: Name has affix, Caption does not
field(50100; "Auto-Assign Lot No. KRL"; Boolean)
{
Caption = 'Auto-Assign Lot No.';
}
Blank captions — a single space like Caption = ' ' — exist only for UI layout (e.g., intentionally empty column headers) and are not user-facing text. They must be marked Locked = true so translators skip them and the AL compiler stops flagging AA0228.
// WRONG — blank caption without Locked triggers AA0228
field(50100; "Spacer KRL"; Text[10])
{
Caption = ' ';
}
// RIGHT
field(50100; "Spacer KRL"; Text[10])
{
Caption = ' ', Locked = true;
}
This is the only case where Locked = true is allowed on a caption — regular user-facing captions must stay translatable.
this.)Always use this. when calling procedures within the same object.
procedure PostDocument(var pDocumentHeader: Record "Sales Header")
begin
this.ValidateDocument(pDocumentHeader);
this.CalculateTotals(pDocumentHeader);
end;
Use 2-space indentation consistently.
begin..end (AA0005)Only use begin..end to enclose compound statements (2+ statements). Single-statement blocks must not be wrapped:
// WRONG
if _Customer.Find() then begin
exit(true);
end;
// RIGHT
if _Customer.Find() then
exit(true);
// WRONG — unchecked Find
_Customer.Get(pCustomerNo);
// RIGHT — checked with error
if not _Customer.Get(pCustomerNo) then
Error(_RecordNotFoundErr, _Customer.TableCaption(), _Customer.FieldCaption("No."), pCustomerNo);
Name.ObjectType.al (e.g., CustomerRating.Table.al)ALL error messages must use Label variables with TableCaption() / FieldCaption() placeholders — never hardcoded table/field names.
var
_RecordNotFoundErr: Label 'Table %1 does not contain %2 = %3.', Comment = '%1 = Table name, %2 = Field name, %3 = Value';
begin
Error(_RecordNotFoundErr, _Customer.TableCaption(), _Customer.FieldCaption("No."), pCustomerNo);
end;
Label guidelines:
Locked = true for user-facing messages)Comment to explain all placeholders for translatorsComment Property — Placeholders OnlyThe Comment property on Labels, ToolTips, and Captions has exactly one job: explaining placeholders (%1, %2, ...) to translators. If the string has no placeholders, omit Comment entirely. Never emit a placeholder stub like Comment = '%' — it adds noise and misleads translators.
// WRONG — ToolTip has no placeholders, Comment is a meaningless stub
ToolTip = 'Specifies the value of the Internal Inv. Curr. Factor field.', Comment = '%';
// RIGHT — no placeholders → no Comment
ToolTip = 'Specifies the value of the Internal Inv. Curr. Factor field.';
// RIGHT — Comment required because placeholders exist
_RecordNotFoundErr: Label 'Table %1 does not contain %2 = %3.',
Comment = '%1 = Table name, %2 = Field name, %3 = Value';
Every .al edit must be followed by a build before the turn ends: call #ms-dynamics-smb.al/al_build({scope:"current"}) then #ms-dynamics-smb.al/al_get_diagnostics({scope:"current", severities:["error","warning"]}) to retrieve the typed diagnostic list. Drive both errors and warnings on the files you touched to zero — CodeCop (AA0xxx), AppSource (AS0xxx), and compiler warnings count as must-fix unless the user has explicitly accepted one. Do not declare a change done with an unbuilt or warning-laden file.
Exception: when you are dispatched as a coder subagent with [DISPATCH_CONTEXT: orchestrated] in your prompt, the orchestrator runs the build after you return — do NOT build yourself.
These critical rules are detailed in dedicated skills — agents must read them as part of Required Reading:
ToBeClassified in production)