Create custom API pages (CRUD) and API queries (read-only joins) in AL
—
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Pending
The risk profile of this skill
| Type | Object | Operations | Use Case |
|---|---|---|---|
| API Page | PageType = API | CRUD (Read-Write) | Expose single table |
| API Query | QueryType = API | Read-only | Join multiple tables |
For consuming Microsoft standard APIs, use al-standard-api skill instead.
Note: OData UI endpoints deprecated BC30 (2027). Always use API pages.
| Placeholder | Description | Example |
|---|---|---|
<ID> | Object ID from app.json idRanges | 50100 |
<PREFIX> | Company prefix from app.json | SL, ABC |
<SourceTable> | Source table name | "Rental Machine" |
<entityName> | Singular entity name (camelCase) | rentalMachine |
<entitySetName> | Plural entity name (camelCase) | rentalMachines |
<publisher> | APIPublisher value | contoso |
<group> | APIGroup value | rental |
<KeyField> | Primary key field | "No." |
<Field1>, <Field2> | Table fields to expose | "Description", "Status" |
Use API Page when:
Use API Query when:
page <ID> "<PREFIX> API <EntityName>"
{
APIGroup = '<group>';
APIPublisher = '<publisher>';
APIVersion = 'v2.0';
EntityName = '<entityName>';
EntitySetName = '<entitySetName>';
EntityCaption = '<Entity Display Name>';
EntitySetCaption = '<Entities Display Name>';
PageType = API;
SourceTable = <SourceTable>;
DelayedInsert = true;
ODataKeyFields = SystemId;
layout
{
area(Content)
{
repeater(General)
{
field(id; Rec.SystemId)
{
Caption = 'ID';
Editable = false;
}
field(<fieldApiName1>; Rec.<KeyField>)
{
Caption = '<Field1 Caption>';
}
field(<fieldApiName2>; Rec.<Field1>)
{
Caption = '<Field2 Caption>';
}
field(<fieldApiName3>; Rec.<Field2>)
{
Caption = '<Field3 Caption>';
Editable = false; // Calculated fields
}
field(lastModifiedDateTime; Rec.SystemModifiedAt)
{
Caption = 'Last Modified';
Editable = false;
}
}
}
}
trigger OnInsertRecord(BelowxRec: Boolean): Boolean
begin
// Optional: Set default values or execute business logic
Rec.Insert(true);
exit(false); // Return false to prevent double insert
end;
}For header-line relationships, add a part to the parent API page:
layout
{
area(Content)
{
repeater(General)
{
// ... header fields ...
}
part(<entityName>Lines; "<PREFIX> API <EntityName> Lines")
{
EntityName = '<entityName>Line';
EntitySetName = '<entityName>Lines';
SubPageLink = <ParentKeyField> = field(<KeyField>);
}
}
}Subpage (Lines):
page <ID+1> "<PREFIX> API <EntityName> Lines"
{
APIGroup = '<group>';
APIPublisher = '<publisher>';
APIVersion = 'v2.0';
EntityName = '<entityName>Line';
EntitySetName = '<entityName>Lines';
PageType = API;
SourceTable = <LineSourceTable>;
DelayedInsert = true;
ODataKeyFields = SystemId;
layout
{
area(Content)
{
repeater(General)
{
field(id; Rec.SystemId) { Editable = false; }
field(<parentKeyApiName>; Rec.<ParentKeyField>) { }
field(lineNo; Rec."Line No.") { }
// ... other line fields ...
}
}
}
}query <ID> "<PREFIX> API <QueryName>"
{
QueryType = API;
APIPublisher = '<publisher>';
APIGroup = '<group>';
APIVersion = 'v2.0';
EntityName = '<entityName>';
EntitySetName = '<entitySetName>';
elements
{
dataitem(<HeaderAlias>; <HeaderTable>)
{
column(id; SystemId) { }
column(<headerField1>; <KeyField>) { }
column(<headerField2>; <Field1>) { }
dataitem(<LineAlias>; <LineTable>)
{
DataItemLink = <LinkField> = <HeaderAlias>.<KeyField>;
SqlJoinType = InnerJoin; // or LeftOuterJoin
column(lineNo; "Line No.") { }
column(<lineField1>; <LineField1>) { }
column(<lineField2>; <LineField2>) { }
}
}
}
}| Property | Required | Description |
|---|---|---|
APIPublisher | Yes | Your company/publisher name |
APIGroup | Yes | Logical grouping |
APIVersion | Yes | v2.0 recommended, beta for development |
EntityName | Yes | Singular, camelCase |
EntitySetName | Yes | Plural, camelCase |
ODataKeyFields | Yes | Always use SystemId |
PageType/QueryType | Yes | API |
DelayedInsert | Recommended | true for API pages |
Custom API endpoint:
https://api.businesscentral.dynamics.com/v2.0/<TenantID>/<Environment>/api/<publisher>/<group>/<version>/companies(<CompanyID>)/<entitySetName>Example:
https://api.businesscentral.dynamics.com/v2.0/contoso.com/Production/api/contoso/rental/v2.0/companies(xxx)/rentalMachinesStarting BC v24, enum values are returned as XML-encoded names:
Return Order → Return_x0020_OrderAdd ?$schemaversion=1.0 for old behavior.
?$schemaversion=2.1&$filter=status in ('Open', 'Released')Without it → BadRequest_MethodNotImplemented.
?$expand=lines($filter=type eq 'Item')Parentheses required around nested options.
?$expand=lines($expand=item($expand=category))ODataKeyFields = SystemId - stable across renamesEntityCaption/EntitySetCaption - for localization at /entityDefinitionsrentalMachine not RentalMachinev1.0, v2.0, or betaSystemModifiedAt - enables delta sync with $filterDelayedInsert = true - prevents partial record creationMake API read-only:
page <ID> "<PREFIX> API Read Only"
{
PageType = API;
InsertAllowed = false;
ModifyAllowed = false;
DeleteAllowed = false;
}| Error | Cause | Fix |
|---|---|---|
| 400 | Invalid field name | Check camelCase, no spaces |
| 404 | Entity not found | Verify ODataKeyFields = SystemId, entity names |
| 500 | Server error | Check OnInsertRecord trigger, BC event log |
| API not visible | Not published | Restart service tier, check compilation |
Feedback loop: Fix issue → Recompile → Test GET first → Then test POST/PATCH/DELETE.
See references/ folder for:
api-page-template.al - Complete AL template for API Page (CRUD), Subpage (Lines), and Read-Only APIapi-query-template.al - Complete AL template for API Query with join examples