Control AL object visibility with Access property (Public, Internal, Protected, Local)
—
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Pending
The risk profile of this skill
internalsVisibleTo apps can access internal objectsNote: BC 2019 Wave 2+ (Runtime 4.0). Compile-time only - RecordRef/FieldRef bypass at runtime.
Output: Objects with correct Access property, app.json with internalsVisibleTo (if multi-app).
Supported objects: Codeunit, Table, Table field, Query, Enum, Interface, PermissionSet
// Internal table - hidden from external apps
table <ID> "<PREFIX> Internal Staging"
{
Access = Internal;
// ... fields
}
// Internal codeunit - helper not exposed to partners
codeunit <ID> "<PREFIX> Internal Helper"
{
Access = Internal;
// ... procedures
}table <ID> "<PREFIX> Order Extension"
{
Access = Public;
fields
{
field(1; "Order No."; Code[20]) { Access = Public; } // Stable API
field(2; "Processing Flags"; Integer) { Access = Internal; } // Same app only
field(3; "Extension Point"; Code[50]) { Access = Protected; } // Table extensions
field(4; "Internal Counter"; Integer) { Access = Local; } // This table only
}
}| Level | Same Table | Table Extension | Same App | Other Apps |
|---|---|---|---|---|
Local | ✓ | |||
Protected | ✓ | ✓ | ||
Internal | ✓ | ✓ | ✓ | |
Public | ✓ | ✓ | ✓ | ✓ |
Designer note: Only Public fields appear in in-client Designer.
In app.json of the core app:
{
"internalsVisibleTo": [
{ "id": "<companion-app-guid>", "name": "Companion App", "publisher": "My Publisher" },
{ "id": "<test-app-guid>", "name": "Test App", "publisher": "My Publisher" }
]
}Use cases: Core + Companion apps, Test apps accessing internals, Modular architecture.
⚠️ NOT a security boundary: Access is compile-time only. RecordRef/FieldRef bypass at runtime. Use permission sets for data security.
codeunit <ID> "<PREFIX> Public API"
{
Access = Public;
procedure ProcessOperation(InputData: Text): Boolean
begin
exit(InternalProcessor.DoProcess(InputData));
end;
var
InternalProcessor: Codeunit "<PREFIX> Internal Processor";
}
codeunit <ID+1> "<PREFIX> Internal Processor"
{
Access = Internal;
procedure DoProcess(InputData: Text): Boolean
begin
// Can be refactored freely - not exposed
end;
}codeunit <ID> "<PREFIX> Secret Manager"
{
Access = Internal;
[NonDebuggable]
procedure GetClientSecret(): Text
begin
// Use IsolatedStorage with DataScope::Module
end;
}| Scenario | Recommended Access |
|---|---|
| Stable API for partners | Public |
| Helper codeunits | Internal |
| Staging/buffer tables | Internal |
| Fields for table extensions | Protected |
| Fields in table triggers only | Local |
| Secret management | Internal + [NonDebuggable] |
Guidelines: Default to Internal • Public = supported contract • Changing Public → Internal is breaking
Limitations: Cannot set on Pages, Reports, XMLports, Control add-ins.