creating-bc-custom-api
Skill1 starsChanged 9 months agoMITView on GitHub
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.
In this repository12 other files it gives its agents.
- AGENTS.mdAGENTS.md
- authenticating-with-oauthSkill
- binding-event-subscribersSkill
- building-cue-rolecentersSkill
- consuming-bc-standard-apiSkill
- controlling-al-accessSkill
- handling-bc-filesSkill
- implementing-al-interfacesSkill
- implementing-telemetrySkill
- integrating-azure-servicesSkill
- optimizing-bc-performanceSkill
- writing-git-commitsSkill
The file
We split this skill into the rules it gives an agent
3 rules. About 0 are common, 2 are distinctive, and 1 is very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈2
Distinctive · estimate
This author's own choices. Worth a look.
1
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 24
Final: All CRUD operations work (GET 200, POST 201, PATCH 200, DELETE 204)
- Very specific: line 235
Add ?$schemaversion=1.0 for old behavior.
- Distinctive: line 262
Alphanumeric only: - no special characters in field names
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Skill: Creating BC Custom APIs
Key Concept
| 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.
Validation Gates
- After Step 2: API page compiles, GET returns 200 with fields
- After Step 3: Subpage works, navigation property expands correctly
- Final: All CRUD operations work (GET 200, POST 201, PATCH 200, DELETE 204)
Note: OData UI endpoints deprecated BC30 (2027). Always use API pages.
Placeholder Reference
| 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" |
Procedure
Step 1: Decide API Type
Use API Page when:
- Need CRUD operations (Create, Read, Update, Delete)
- Exposing a single table
- Need to execute business logic on insert/modify
Use API Query when:
- Read-only access sufficient
- Need to join multiple tables
- Reporting/analytics endpoint
Step 2: Create API Page (CRUD)
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;
}Step 3: Add Navigation Property (Lines/Details)
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 ...
}
}
}
}Step 4: Create API Query (Read-Only Joins)
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>) { }
}
}
}
}Required Properties
| 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 |
API URL Structure
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)/rentalMachinesSchema Version and OData Features (BC v24+)
Enum Values (schemaversion 2.0)
Starting BC v24, enum values are returned as XML-encoded names:
Return Order→Return_x0020_Order
Add ?$schemaversion=1.0 for old behavior.
IN Operator (schemaversion 2.1)
?$schemaversion=2.1&$filter=status in ('Open', 'Released')Without it → BadRequest_MethodNotImplemented.
Filter inside $expand
?$expand=lines($filter=type eq 'Item')Parentheses required around nested options.
Multi-level Expand
?$expand=lines($expand=item($expand=category))Best Practices
- Always use
ODataKeyFields = SystemId- stable across renames - Use
EntityCaption/EntitySetCaption- for localization at/entityDefinitions - camelCase for API names -
rentalMachinenotRentalMachine - Alphanumeric only - no special characters in field names
- Version your APIs -
v1.0,v2.0, orbeta - Include
SystemModifiedAt- enables delta sync with$filter DelayedInsert = true- prevents partial record creation
CRUD Control
Make API read-only:
page <ID> "<PREFIX> API Read Only"
{
PageType = API;
InsertAllowed = false;
ModifyAllowed = false;
DeleteAllowed = false;
}Troubleshooting
| 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.
References
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
External Documentation
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

