Creates custom API pages (CRUD) and API queries (read-only joins) in AL for Business Central. Use when exposing custom tables, adding business logic to APIs, creating reporting endpoints, or joining multiple tables for external consumption.
72
88%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
| 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 examples52f8705
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.