Install
$ agentstack add skill-emgreppi-business-central-ai-skill-creating-bc-custom-api ✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
About
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 | |-------------|-------------|---------| | ` | Object ID from app.json idRanges | 50100 | | | Company prefix from app.json | SL, ABC | | | Source table name | "Rental Machine" | | | Singular entity name (camelCase) | rentalMachine | | | Plural entity name (camelCase) | rentalMachines | | | APIPublisher value | contoso | | | APIGroup value | rental | | | Primary key field | "No." | | , | 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 " API "
{
APIGroup = '';
APIPublisher = '';
APIVersion = 'v2.0';
EntityName = '';
EntitySetName = '';
EntityCaption = '';
EntitySetCaption = '';
PageType = API;
SourceTable = ;
DelayedInsert = true;
ODataKeyFields = SystemId;
layout
{
area(Content)
{
repeater(General)
{
field(id; Rec.SystemId)
{
Caption = 'ID';
Editable = false;
}
field(; Rec.)
{
Caption = '';
}
field(; Rec.)
{
Caption = '';
}
field(; Rec.)
{
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(Lines; " API Lines")
{
EntityName = 'Line';
EntitySetName = 'Lines';
SubPageLink = = field();
}
}
}
Subpage (Lines):
page " API Lines"
{
APIGroup = '';
APIPublisher = '';
APIVersion = 'v2.0';
EntityName = 'Line';
EntitySetName = 'Lines';
PageType = API;
SourceTable = ;
DelayedInsert = true;
ODataKeyFields = SystemId;
layout
{
area(Content)
{
repeater(General)
{
field(id; Rec.SystemId) { Editable = false; }
field(; Rec.) { }
field(lineNo; Rec."Line No.") { }
// ... other line fields ...
}
}
}
}
Step 4: Create API Query (Read-Only Joins)
query " API "
{
QueryType = API;
APIPublisher = '';
APIGroup = '';
APIVersion = 'v2.0';
EntityName = '';
EntitySetName = '';
elements
{
dataitem(; )
{
column(id; SystemId) { }
column(; ) { }
column(; ) { }
dataitem(; )
{
DataItemLink = = .;
SqlJoinType = InnerJoin; // or LeftOuterJoin
column(lineNo; "Line No.") { }
column(; ) { }
column(; ) { }
}
}
}
}
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///api////companies()/
Example:
https://api.businesscentral.dynamics.com/v2.0/contoso.com/Production/api/contoso/rental/v2.0/companies(xxx)/rentalMachines
Schema 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 " 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
- Microsoft: Developing Custom API
- Microsoft: API Page Type
- Microsoft: API Query Type
- GitHub: APIV2 Examples
- Kauffmann: Schema Version 2.0
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: emgreppi
- Source: emgreppi/Business-Central-AI-Skill
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.