Convention Stack — Complete Rule Set
Full rule catalogue for all five review categories. Each rule includes its source, priority, and the exact check to perform.
Priority sources:
- P1 — AppSource validation requirements (blocks publication)
- P2 — CodeCop / PerTenantExtensionCop analyzer rules
- P3 — alguidelines.dev community standard
- P4 — al-copilot-skills catalogue patterns
Table of contents
- Category 1 — Naming & Structure — NS-01 to NS-09
- Category 2 — Performance Anti-Patterns — PF-01 to PF-07
- Category 3 — Extensibility Contract — EX-01 to EX-05
- Category 4 — SaaS Readiness — SR-01 to SR-06
- Category 5 — AppSource Blockers — summary (full detail in appsource-blockers.md)
Category 1 — Naming & Structure
NS-01 — Object prefix/suffix
Priority: P1 | Severity: 🔴 Blocker
Every custom object (table, page, codeunit, report, query, enum, xmlport) and every custom field on a table extension must have a registered prefix or suffix applied consistently.
// ❌ Wrong
table 50100 "Lead"
field(1; Status; Option)
// ✅ Correct
table 50100 "CTX Lead"
field(1; "CTX Status"; Option)
Source: AppSource checklist
NS-02 — File naming convention
Priority: P3 | Severity: 🔵 Suggestion
File names follow the pattern {ObjectName}.{ObjectType}.al. The object type is spelled out, not abbreviated.
| Object type | Correct file name |
|---|---|
| Codeunit | CTXLeadManagement.Codeunit.al |
| Table | CTXLead.Table.al |
| Page | CTXLeadCard.Page.al |
| TableExtension | CTXCustomerExt.TableExt.al |
| PageExtension | CTXCustomerCardExt.PageExt.al |
Source: alguidelines.dev — File Naming
NS-03 — Object ID within idRanges
Priority: P1 | Severity: 🔴 Blocker
Every object ID must fall within the idRanges declared in app.json. IDs outside the range cause submission rejection.
Check: Extract all object IDs from code, compare against app.json idRanges. Flag any ID outside the range.
Source: AppSource validation
NS-04 — No WITH statements
Priority: P2 | Severity: 🔴 Blocker
WITH statements are deprecated and cause AA0007 CodeCop warnings. They are prohibited when the NoImplicitWith feature is enabled (required from runtime 11.0+).
// ❌ Wrong
with SalesHeader do begin
"Document Type" := "Document Type"::Order;
Insert(true);
end;
// ✅ Correct
SalesHeader."Document Type" := SalesHeader."Document Type"::Order;
SalesHeader.Insert(true);
Source: CodeCop AA0007
NS-05 — Label Locked property
Priority: P2 | Severity: 🟡 Warning
Labels that should not be translated (event IDs, API strings, technical constants) must have Locked = true. Labels without Locked = true appear in translation files and may be incorrectly translated.
// ❌ Wrong — event ID will appear in translation files
EventIdLbl: Label 'CTX-SALES-001';
// ✅ Correct
EventIdLbl: Label 'CTX-SALES-001', Locked = true;
Source: alguidelines.dev — Labels
NS-06 — ObsoleteState requires ObsoleteReason and ObsoleteTag
Priority: P2 | Severity: 🟡 Warning
Every object or field with ObsoleteState = Pending or ObsoleteState = Removed must have both ObsoleteReason (explaining what to use instead) and ObsoleteTag (the version when it will be removed).
// ❌ Wrong
field(10; "Old Field"; Text[50])
{
ObsoleteState = Pending;
}
// ✅ Correct
field(10; "Old Field"; Text[50])
{
ObsoleteState = Pending;
ObsoleteReason = 'Use "New Field" instead. Will be removed in v3.0.';
ObsoleteTag = '3.0';
}
Source: CodeCop AA0072
NS-07 — No hardcoded environment values
Priority: P3 | Severity: 🟡 Warning
No hardcoded company names, environment URLs, user IDs, or tenant IDs in code. These break when the extension is deployed to a different environment.
// ❌ Wrong
if CompanyName = 'CRONUS International Ltd.' then
// ✅ Correct — use setup table or parameter
if CompanyName = CTXSetup."Default Company" then
Source: alguidelines.dev, al-copilot-skills
NS-08 — PascalCase for procedures, camelCase for local variables
Priority: P3 | Severity: 🔵 Suggestion
// ❌ Wrong
local procedure calculate_total(salesHeader: Record "Sales Header"): Decimal
var
TotalAmount: Decimal;
// ✅ Correct
local procedure CalculateTotal(SalesHeader: Record "Sales Header"): Decimal
var
totalAmount: Decimal;
Source: alguidelines.dev — Naming
NS-09 — No suppressWarnings without justification
Priority: P1 | Severity: 🔴 Blocker
#pragma warning disable without a specific rule number and inline justification comment is rejected by AppSource validation.
// ❌ Wrong
#pragma warning disable
// ✅ Correct (if genuinely needed)
#pragma warning disable AA0007 // WITH statement required here for legacy API compatibility — tracked in issue #42
Source: AppSource validation
Category 2 — Performance Anti-Patterns
PF-01 — SetLoadFields missing before Find*
Priority: P3 | Severity: 🟡 Warning
Every FindSet, FindFirst, Get that reads specific fields should be preceded by SetLoadFields listing only those fields. Without it, all fields are loaded from the database — expensive on wide tables.
// ❌ Wrong — loads all 50+ fields of Customer
Customer.SetRange("Country/Region Code", 'ES');
if Customer.FindSet() then
// ✅ Correct
Customer.SetLoadFields("No.", Name, "Balance (LCY)");
Customer.SetRange("Country/Region Code", 'ES');
if Customer.FindSet() then
Exception: Acceptable to omit SetLoadFields when all fields are genuinely needed, or in test code.
Source: al-copilot-skills skill-performance
PF-02 — CalcFields inside repeat..until
Priority: P3 | Severity: 🔴 Blocker (for list pages) / 🟡 Warning (for batch code)
CalcFields inside a loop fires one aggregation query per iteration. On large datasets this causes timeouts.
// ❌ Wrong
if Customer.FindSet() then
repeat
Customer.CalcFields("Balance (LCY)"); // one query per customer
until Customer.Next() = 0;
// ✅ Correct — restructure to avoid CalcFields in loop
// Option: use a Query object joining Customer + Cust. Ledger Entry
// Option: accept that Balance is a display-only FlowField, not for batch logic
Source: al-copilot-skills skill-performance, alguidelines.dev