agentleFS
javiarmestojaviarmesto/ALDC-AL-Development-Collection/.claude/skills/skill-api/SKILL.md

skill-api

Skill115 starsChanged 10 days agoMITView on GitHub

AL API development patterns for Business Central. Use when creating OData/REST API pages, HttpClient integrations, webhook implementations, or any external system integration via API.

The file

Show its rules and text

We split this skill into the rules it gives an agent

19 rules. About 0 are common, 11 are distinctive, and 8 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈11

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 8

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    Document in .claude/plans/{req_name}.architecture.md or a dedicated API design section.

    line 334
  2. Distinctive:

    Create header API page (Pattern 1)

    line 339
  3. Distinctive:

    Create subpage(s) for lines/children (Pattern 2)

    line 340
  4. Distinctive:

    Add bound actions for entity operations (Pattern 3)

    line 341
  5. Distinctive:

    Add unbound actions if needed (Pattern 4)

    line 342
  6. Distinctive:

    Add error handling triggers (Pattern 7)

    line 343
  7. Distinctive:

    Add keys for filterable fields:

    line 348
  8. Very specific:

    Create role-based permission sets (full access + read-only) for the API pages.

    line 370
  9. Distinctive:

    Test CRUD operations (create, read, update, delete)

    line 374
  10. Distinctive:

    Test bound actions (post, release, reopen)

    line 375
  11. Very specific:

    Test $filter, $select, $expand query options

    line 376
  12. Distinctive:

    Test error responses (missing required fields, blocked customer, invalid state)

    line 377
  13. Very specific:

    Test permission sets (read-only user cannot POST/PATCH/DELETE)

    line 378
  14. Very specific:

    Test If-Match / ETag for optimistic concurrency on PATCH and DELETE

    line 379
  15. Distinctive:

    Do NOT modify base BC objects — create API pages as extensions only

    line 393
  16. Distinctive:

    Do NOT expose internal implementation details (codeunit internals, temp tables) in API responses

    line 394
  17. Very specific:

    Do NOT skip APIVersion — every API page MUST have an explicit version

    line 395
  18. Very specific:

    Do NOT create breaking changes on stable versions — use beta for previewing changes, then promote

    line 396
  19. Very specific:

    Do NOT skip error handling — OnInsertRecord, OnModifyRecord, OnDeleteRecord must validate

    line 397

≈ “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.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Skill: AL API Development

Purpose

Design and implement RESTful API pages for Business Central: API page v2.0 patterns, OData conventions, versioning, bound/unbound actions, webhooks, header-lines navigation, and performance-optimized endpoints.

When to Load

This skill should be loaded when:

  • A new API page (PageType = API) needs to be designed or implemented
  • Existing BC data must be exposed to external consumers (Power Platform, mobile apps, 3rd-party)
  • Custom bound or unbound actions are needed on API endpoints
  • An API versioning strategy or deprecation plan is required
  • Webhook subscriptions need to be configured for change notifications
  • API performance or filtering needs optimization

Core Patterns

Pattern 1: API Page v2.0 (Standard CRUD Endpoint)

page 50100 "Contoso Sales Orders API"
{
    APIVersion = 'v2.0';
    APIPublisher = 'contoso';
    APIGroup = 'sales';

    EntityCaption = 'Sales Order';
    EntitySetCaption = 'Sales Orders';
    EntityName = 'salesOrder';               // singular — used in URL for single entity
    EntitySetName = 'salesOrders';           // plural — used in URL for collection

    PageType = API;
    SourceTable = "Sales Header";
    SourceTableView = where("Document Type" = const(Order));
    DelayedInsert = true;                    // required — defers insert until all fields set
    ODataKeyFields = SystemId;               // required — use SystemId for stable GUIDs

    layout
    {
        area(Content)
        {
            repeater(Group)
            {
                field(id; Rec.SystemId)
                {
                    Caption = 'Id';
                    Editable = false;
                }
                field(number; Rec."No.")
                {
                    Caption = 'Number';
                    Editable = false;
                }
                field(orderDate; Rec."Order Date")
                {
                    Caption = 'Order Date';
                }
                field(customerNumber; Rec."Sell-to Customer No.")
                {
                    Caption = 'Customer Number';
                }
                field(customerName; Rec."Sell-to Customer Name")
                {
                    Caption = 'Customer Name';
                    Editable = false;
                }
                field(totalAmountIncludingVAT; Rec."Amount Including VAT")
                {
                    Caption = 'Total Amount Including VAT';
                    Editable = false;
                }
                field(status; Rec.Status)
                {
                    Caption = 'Status';
                    Editable = false;
                }
                field(lastModifiedDateTime; Rec.SystemModifiedAt)
                {
                    Caption = 'Last Modified Date Time';
                    Editable = false;
                }
            }
        }
    }
}

Resulting endpoint:

GET  /api/contoso/sales/v2.0/companies({companyId})/salesOrders
GET  /api/contoso/sales/v2.0/companies({companyId})/salesOrders({id})
POST /api/contoso/sales/v2.0/companies({companyId})/salesOrders
PATCH /api/contoso/sales/v2.0/companies({companyId})/salesOrders({id})
DELETE /api/contoso/sales/v2.0/companies({companyId})/salesOrders({id})

Property rules:

  • ODataKeyFields = SystemId — always use SystemId for stable, immutable keys
  • DelayedInsert = true — mandatory on API pages (lets BC set defaults before committing)
  • SourceTableView — pre-filter the source if the table serves multiple document types
  • Field names use camelCase (OData convention): customerNumber, not Customer_Number
  • Editable = false on computed/system fields to prevent consumer confusion

Pattern 2: Header-Lines with Navigation Property

Expose parent-child relationships via part subpages:

// Add inside the header API page's repeater:
part(salesOrderLines; "Contoso Sales Order Lines API")
{
    Caption = 'Lines';
    EntityName = 'salesOrderLine';
    EntitySetName = 'salesOrderLines';
    SubPageLink = "Document Type" = field("Document Type"),
                  "Document No." = field("No.");
}
// Lines API page (subpage)
page 50101 "Contoso Sales Order Lines API"
{
    APIVersion = 'v2.0';
    APIPublisher = 'contoso';
    APIGroup = 'sales';

    EntityCaption = 'Sales Order Line';
    EntitySetCaption = 'Sales Order Lines';
    EntityName = 'salesOrderLine';
    EntitySetName = 'salesOrderLines';

    PageType = API;
    SourceTable = "Sales Line";
    DelayedInsert = true;
    ODataKeyFields = SystemId;

    layout
    {
        area(Content)
        {
            repeater(Group)
            {
                field(id; Rec.SystemId) { Editable = false; }
                field(lineNumber; Rec."Line No.") { Editable = false; }
                field(lineType; Rec.Type) { Caption = 'Type'; }
                field(itemNumber; Rec."No.") { Caption = 'Item Number'; }
                field(description; Rec.Description) { Caption = 'Description'; }
                field(quantity; Rec.Quantity) { Caption = 'Quantity'; }
                field(unitPrice; Rec."Unit Price") { Caption = 'Unit Price'; }
                field(lineAmount; Rec."Line Amount") { Caption = 'Line Amount'; Editable = false; }
            }
        }
    }
}

Consumer usage:

# Get order with lines expanded
GET /salesOrders({id})?$expand=salesOrderLines

# Get lines for a specific order
GET /salesOrders({id})/salesOrderLines

# Add a line to an order
POST /salesOrders({id})/salesOrderLines
{ "lineType": "Item", "itemNumber": "ITEM-001", "quantity": 10 }

Pattern 3: Bound Actions (Operate on Entity)

Bound actions trigger business logic on a specific entity:

// Inside the API page's actions area:
actions
{
    area(Processing)
    {
        // POST /salesOrders({id})/Microsoft.NAV.post
        action(post)
        {
            ApplicationArea = All;
            Caption = 'Post';

            trigger OnAction()
            var
                SalesPost: Codeunit "Sales-Post";
            begin
                Rec.TestField(Status, Rec.Status::Released);
                SalesPost.Run(Rec);
            end;
        }

        // POST /salesOrders({id})/Microsoft.NAV.release
        action(release)
        {
            ApplicationArea = All;
            Caption = 'Release';

            trigger OnAction()
            var
                ReleaseSalesDoc: Codeunit "Release Sales Document";
            begin
                ReleaseSalesDoc.PerformManualRelease(Rec);
            end;
        }

        // POST /salesOrders({id})/Microsoft.NAV.reopen
        action(reopen)
        {
            ApplicationArea = All;
            Caption = 'Reopen';

            trigger OnAction()
            var
                ReleaseSalesDoc: Codeunit "Release Sales Document";
            begin
                ReleaseSalesDoc.PerformManualReopen(Rec);
            end;
        }
    }
}

Consumer call:

POST /salesOrders({id})/Microsoft.NAV.post
Content-Type: application/json

Pattern 4: Unbound Actions (Standalone Operations)

Unbound actions are not tied to a specific entity — use a virtual/dummy source table:

page 50102 "Contoso Utility API"
{
    APIVersion = 'v2.0';
    APIPublisher = 'contoso';
    APIGroup = 'utilities';

    EntityName = 'utilityFunction';
    EntitySetName = 'utilityFunctions';

    PageType = API;
    SourceTable = "Company Information";    // read-only singleton as base
    SourceTableTemporary = true;
    InsertAllowed = false;
    ModifyAllowed = false;
    DeleteAllowed = false;

    layout
    {
        area(Content)
        {
            repeater(Group)
            {
                field(companyName; Rec.Name) { Editable = false; }
            }
        }
    }
    actions
    {
        area(Processing)
        {
            // POST /utilityFunctions/Microsoft.NAV.calculateShipping
            action(calculateShipping)
            {
                ApplicationArea = All;
                Caption = 'Calculate Shipping';

                trigger OnAction()
                var
                    ShippingMgt: Codeunit "Contoso Shipping Management";
                    Weight: Decimal;
                    DestCode: Code[20];
                begin
                    Evaluate(Weight, GetActionContext().GetText('weight'));
                    DestCode := CopyStr(GetActionContext().GetText('destinationCode'), 1, 20);
                    SetActionResponse(CreateJsonResponse(
                        ShippingMgt.CalculateCost(Weight, DestCode)));
                end;
            }
        }
    }
}

When you need versioning/deprecation, webhooks, or trigger-level error handling, load references/api-advanced-patterns.md.

XML Documentation for Public Procedures

Any public procedure that other modules call carries XML doc comments. This covers API pages (above), and equally the library codeunits invoked by API logic or by other codeunits — anything outside the unit's own boundary.

/// <summary>
/// Evaluates whether the customer qualifies as VIP based on sales volume
/// and persists the result on Customer."VIP Customer".
/// </summary>
/// <param name="CustomerNo">The customer number to evaluate. Exits silently if blank or not found.</param>
procedure EvaluateCustomer(CustomerNo: Code[20])
begin
    // ...
end;
  • <summary> (required) — what the procedure does and why a caller would invoke it.
  • <param name="..."> (required for each non-trivial parameter) — what value to pass and constraints.
  • <returns> (required when there is a return value) — what the value means.
  • local and internal procedures: doc is optional.

This surface is what IntelliSense presents to consumers and what AL's missing-documentation diagnostics flag.

Workflow

Step 1: Design API Contract

Before implementing, define:

  1. Resource model — entities, relationships, navigation properties
  2. Operations — which HTTP methods per resource (GET/POST/PATCH/DELETE)
  3. Actions — custom operations (bound: per entity, unbound: global)
  4. Filtering — which fields consumers can $filter on (add corresponding keys)
  5. Versioning — initial version and deprecation plan
  6. Authentication — OAuth 2.0 scope, permission sets needed

Document in .claude/plans/{req_name}.architecture.md or a dedicated API design section. PAUSE — wait for user approval before implementing.

Step 2: Implement API Pages

  1. Create header API page (Pattern 1)
  2. Create subpage(s) for lines/children (Pattern 2)
  3. Add bound actions for entity operations (Pattern 3)
  4. Add unbound actions if needed (Pattern 4)
  5. Add error handling triggers (Pattern 7)
  6. Build: al_build

Step 3: Optimize for Performance

Add keys for filterable fields:

tableextension 50100 "Contoso Sales Header Ext" extends "Sales Header"
{
    keys
    {
        key(APICustomerDate; "Sell-to Customer No.", "Order Date") { }
        key(APIStatus; Status, "Order Date") { }
    }
}

Key OData query patterns:

  • Projection: ?$select=number,customerNumber — reduces payload
  • Filtering: ?$filter=customerNumber eq 'C00001' and orderDate ge 2025-01-01 — server-side
  • Expansion: ?$expand=salesOrderLines — inline children
  • Delta links: initial GET returns @odata.deltaLink; subsequent call with $deltatoken returns only changes
  • Pagination: ?$top=50&$skip=100

Step 4: Generate Permission Sets

Create role-based permission sets (full access + read-only) for the API pages. Follow skill-permissions.md for the hierarchy pattern. Minimum: one set granting X on all API pages + RIMD on table data, one read-only set with R only.

Step 5: Test

  • Test CRUD operations (create, read, update, delete)
  • Test bound actions (post, release, reopen)
  • Test $filter, $select, $expand query options
  • Test error responses (missing required fields, blocked customer, invalid state)
  • Test permission sets (read-only user cannot POST/PATCH/DELETE)
  • Test If-Match / ETag for optimistic concurrency on PATCH and DELETE

References

Constraints

  • This skill covers API page design, implementation patterns, and OData conventions
  • Do NOT modify base BC objects — create API pages as extensions only
  • Do NOT expose internal implementation details (codeunit internals, temp tables) in API responses
  • Do NOT skip APIVersion — every API page MUST have an explicit version
  • Do NOT create breaking changes on stable versions — use beta for previewing changes, then promote
  • Do NOT skip error handling — OnInsertRecord, OnModifyRecord, OnDeleteRecord must validate
  • Permission set hierarchy → skill-permissions.md | Performance deep-dive → skill-performance.md | API testing → skill-testing.md

Download now (ZIP)

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
What did you use it for? e.g. Reviewing pull requests in a Rails app

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.