CtrlK
BlogDocsLog inGet started
Tessl Logo

bc-skills/creating-bc-custom-api

Create custom API pages (CRUD) and API queries (read-only joins) in AL

Quality

Does it follow best practices?

Impact

No eval scenarios have been run

SecuritybySnyk

The risk profile of this skill

Overview
Eval results
Files

SKILL.md

name:
creating-bc-custom-api
description:
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.
license:
MIT
metadata:
{"version":"1.0.0"}

Skill: Creating BC Custom APIs

Key Concept

TypeObjectOperationsUse Case
API PagePageType = APICRUD (Read-Write)Expose single table
API QueryQueryType = APIRead-onlyJoin multiple tables

For consuming Microsoft standard APIs, use al-standard-api skill instead.

Validation Gates

  1. After Step 2: API page compiles, GET returns 200 with fields
  2. After Step 3: Subpage works, navigation property expands correctly
  3. 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

PlaceholderDescriptionExample
<ID>Object ID from app.json idRanges50100
<PREFIX>Company prefix from app.jsonSL, ABC
<SourceTable>Source table name"Rental Machine"
<entityName>Singular entity name (camelCase)rentalMachine
<entitySetName>Plural entity name (camelCase)rentalMachines
<publisher>APIPublisher valuecontoso
<group>APIGroup valuerental
<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

PropertyRequiredDescription
APIPublisherYesYour company/publisher name
APIGroupYesLogical grouping
APIVersionYesv2.0 recommended, beta for development
EntityNameYesSingular, camelCase
EntitySetNameYesPlural, camelCase
ODataKeyFieldsYesAlways use SystemId
PageType/QueryTypeYesAPI
DelayedInsertRecommendedtrue 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)/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 OrderReturn_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

  1. Always use ODataKeyFields = SystemId - stable across renames
  2. Use EntityCaption/EntitySetCaption - for localization at /entityDefinitions
  3. camelCase for API names - rentalMachine not RentalMachine
  4. Alphanumeric only - no special characters in field names
  5. Version your APIs - v1.0, v2.0, or beta
  6. Include SystemModifiedAt - enables delta sync with $filter
  7. 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

ErrorCauseFix
400Invalid field nameCheck camelCase, no spaces
404Entity not foundVerify ODataKeyFields = SystemId, entity names
500Server errorCheck OnInsertRecord trigger, BC event log
API not visibleNot publishedRestart 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 API
  • api-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

SKILL.md

tile.json