17.2 Bound and Unbound OData Actions with [ServiceEnabled]
Key Takeaways
- OData Actions in Business Central allow external applications to invoke business logic, state transitions, and custom processing routines over REST endpoints using HTTP POST requests.
- In AL, an action procedure exposed to REST/OData must be decorated with the [ServiceEnabled] attribute and declared within an API page or web service-exposed codeunit.
- Bound actions operate on a specific existing entity instance (the current Rec), addressed via the entity's primary key URL (e.g., /customCustomers({id})/Microsoft.NAV.blockCustomer).
- Unbound actions are static operations not tied to an individual record instance, invoked at the entity set root (e.g., /customCustomers/Microsoft.NAV.recalculateAllDiscounts) or on a web service codeunit.
- The WebServiceActionContext data type manages HTTP response status and location headers using SetResultCode(WebServiceActionResultCode::Created/Updated/Deleted/None), SetObjectType(ObjectType::Page), and SetObjectId(Page::"Custom Customer API").
17.2 Bound and Unbound OData Actions with [ServiceEnabled]
While standard RESTful operations on API pages handle basic CRUD activities (Create via POST, Read via GET, Update via PATCH, Delete via DELETE), enterprise integrations frequently require invoking complex server-side business routines—such as releasing a sales document, posting an invoice, recalculating customer credit limits, or canceling an order. In Business Central, developers implement these remote procedure calls (RPC) using OData Actions decorated with the [ServiceEnabled] attribute.
1. OData Actions Architecture & Execution Model
An OData Action is an operation exposed through the OData v4 metadata schema ($metadata) that can execute state-changing business logic on the Business Central Server tier.
+-------------------------------------------------------------------------+
| EXTERNAL REST CLIENT |
| (HTTP POST /Microsoft.NAV.{actionName}) |
+-----------------------------------┬-------------------------------------+
│ (JSON Parameter Payload)
▼
+-------------------------------------------------------------------------+
| BUSINESS CENTRAL OData ACTION DISPATCHER |
| |
| 1. Locate Target Object (API Page / Codeunit Web Service) |
| 2. Bind Entity Context (Fetch Rec for Bound; Skip for Unbound) |
| 3. Deserialize Action Parameters from Request Body |
| 4. Initialize WebServiceActionContext Reference |
+-----------------------------------┬-------------------------------------+
│ (Invoke AL Procedure)
▼
+-------------------------------------------------------------------------+
| AL BUSINESS LOGIC & TRANSACTION |
| |
| [ServiceEnabled] |
| procedure ProcessAction(Params...; var ActionContext: ActionContext) |
| - Execute Business Logic (e.g., Post, Release, Validate) |
| - Configure ActionContext (SetResultCode, SetObjectId, AddKey) |
+-----------------------------------┬-------------------------------------+
│ (Construct HTTP Response)
▼
+-------------------------------------------------------------------------+
| HTTP RESPONSE STATUS & HEADERS |
| - HTTP 200 OK (Updated / Returned Object) |
| - HTTP 201 Created (Created Resource + Location Header) |
| - HTTP 204 No Content (None / Completed without Body) |
| - HTTP 400 / 500 (AL Error Dialog converted to JSON Error) |
+-------------------------------------------------------------------------+
Core Rules Governing OData Actions
- HTTP Method: All OData Actions must be invoked using the
POSTHTTP verb.GET,PUT, orPATCHrequests to an action endpoint will return405 Method Not Allowed. - Attribute Decoration: The procedure in AL must be decorated with
[ServiceEnabled]. - Namespace Prefix: In the OData v4 metadata and endpoint URL, Business Central actions are always prefixed with the runtime namespace:
Microsoft.NAV.<ProcedureName>(case-sensitive or matching exact AL procedure casing). - Transaction Boundary: The entire action procedure executes within a single database transaction. If an unhandled error occurs or an
Error('...')statement is encountered, all database changes made during the action are automatically rolled back, and an HTTP400 Bad Requestor500 Internal Server Errorresponse is returned.
2. Implementing Bound OData Actions
A Bound Action is explicitly tied to a specific entity record instance. When the external client calls a bound action, the Business Central server automatically resolves the entity key in the URL and loads that specific record into the page's Rec buffer before executing the procedure.
AL Implementation of a Bound Action on an API Page
page 50130 "Custom Customer API"
{
PageType = API;
Caption = 'customCustomer';
APIPublisher = 'custom';
APIGroup = 'integrations';
APIVersion = 'v2.0';
EntityName = 'customCustomer';
EntitySetName = 'customCustomers';
SourceTable = Customer;
DelayedInsert = true;
ODataKeyFields = SystemId;
layout
{
area(Content)
{
repeater(Group)
{
field(id; Rec.SystemId) { }
field(number; Rec."No.") { }
field(displayName; Rec.Name) { }
field(blocked; Rec.Blocked) { }
}
}
}
[ServiceEnabled]
procedure BlockCustomer(ReasonCode: Code[10]; Comments: Text[100]; var ActionContext: WebServiceActionContext)
var
CustomerPostingMsg: Label 'Customer %1 was blocked via API. Reason: %2, Comments: %3', Locked = true;
begin
// The runtime has already loaded Rec matching the URL SystemId
Rec.TestField(Blocked, Rec.Blocked::" ");
Rec.Validate(Blocked, Rec.Blocked::All);
Rec.Modify(true);
// Configure the HTTP Response Context
ActionContext.SetResultCode(WebServiceActionResultCode::Updated);
end;
}
External HTTP Invocation (Bound Action)
To invoke this bound action, the client sends an HTTP POST to the specific customer entity URI, qualifying the action name with Microsoft.NAV.:
POST https://api.businesscentral.dynamics.com/v2.0/{tenant}/{env}/api/custom/integrations/v2.0/companies(12345678-1234-1234-1234-123456789abc)/customCustomers(d4f5a6b7-8901-2345-6789-abcdef012345)/Microsoft.NAV.blockCustomer
Content-Type: application/json
{
"ReasonCode": "OVERDUE",
"Comments": "Account placed on hold due to unpaid invoices exceeding 90 days."
}
HTTP Response:
HTTP/1.1 200 OK
Content-Type: application/json; odata.metadata=minimal
{
"@odata.context": "https://api.businesscentral.dynamics.com/v2.0/.../$metadata#customCustomers/$entity",
"id": "d4f5a6b7-8901-2345-6789-abcdef012345",
"number": "C00010",
"displayName": "Contoso Ltd.",
"blocked": "All"
}
3. Implementing Unbound OData Actions
An Unbound Action is a global or collection-level operation not tied to a single existing record instance. Unbound actions can be declared on API pages (invoked at the entity set root) or inside Codeunits published as OData Web Services.
AL Implementation of an Unbound Action on an API Page
page 50130 "Custom Customer API"
{
// ... (properties and layout) ...
[ServiceEnabled]
procedure RecalculateAllDiscounts(CustomerCategory: Code[20]; var ActionContext: WebServiceActionContext)
var
Cust: Record Customer;
DiscountMgt: Codeunit "Cust-Order Discount";
begin
Cust.SetRange("Customer Disc. Group", CustomerCategory);
if Cust.FindSet(true) then
repeat
DiscountMgt.UpdateCustomerDiscounts(Cust);
until Cust.Next() = 0;
ActionContext.SetResultCode(WebServiceActionResultCode::None);
end;
}
External HTTP Invocation (Unbound Action)
Notice that the URI addresses the entity set root (customCustomers) rather than an individual entity GUID:
POST https://api.businesscentral.dynamics.com/v2.0/{tenant}/{env}/api/custom/integrations/v2.0/companies(12345678-1234-1234-1234-123456789abc)/customCustomers/Microsoft.NAV.recalculateAllDiscounts
Content-Type: application/json
{
"CustomerCategory": "RETAIL"
}
HTTP Response:
HTTP/1.1 204 No Content
Unbound Actions on Web Service Codeunits
Developers can also publish a Codeunit as an OData Web Service. Every public procedure inside that codeunit becomes an unbound action callable over OData v4:
codeunit 50140 "Integration Service"
{
procedure ProcessPendingShipments(WarehouseLocation: Code[10]): Integer
var
WarehouseMgt: Codeunit "Warehouse Management";
begin
exit(WarehouseMgt.ShipAllPendingOrders(WarehouseLocation));
end;
}
4. Managing HTTP Response Context with WebServiceActionContext
The WebServiceActionContext data type gives AL developers precise control over the HTTP status codes, headers, and entity keys returned to the calling client.
Key Methods on WebServiceActionContext
| Method | Purpose & Usage |
|---|---|
SetResultCode(WebServiceActionResultCode) | Sets the HTTP response status. Takes an enum value: Created (201), Updated (200), Deleted (204), or None (204). |
SetObjectType(ObjectType) | Specifies the metadata object type of the resulting entity (e.g., ObjectType::Page). Required when Created or Updated is used. |
SetObjectId(Integer) | Specifies the object ID of the API Page displaying the entity (e.g., Page::"Custom Customer API"). |
AddEntityKey(FieldNo, Value) | Adds key-value pairs (e.g., Rec.FieldNo(SystemId), Rec.SystemId) to generate the OData @odata.id and Location URI headers. |
Complete Example: Action Creating a New Resource (HTTP 201 Created)
When an action creates a new entity (such as posting an order and generating a Posted Sales Invoice), developers configure WebServiceActionContext so the client receives an HTTP 201 status and the URI of the newly created invoice:
[ServiceEnabled]
procedure CreateContractInvoice(ContractNo: Code[20]; var ActionContext: WebServiceActionContext)
var
SalesHeader: Record "Sales Header";
ContractMgt: Codeunit "Contract Management";
begin
// Execute business routine that creates and inserts a new Sales Header
ContractMgt.CreateInvoiceForContract(ContractNo, SalesHeader);
// Build response context pointing to the newly created Sales Header API
ActionContext.SetResultCode(WebServiceActionResultCode::Created);
ActionContext.SetObjectType(ObjectType::Page);
ActionContext.SetObjectId(Page::"Custom Sales Invoice API");
ActionContext.AddEntityKey(SalesHeader.FieldNo(SystemId), SalesHeader.SystemId);
end;
WebServiceActionResultCode Status Code Mapping
WebServiceActionResultCode::None->204 No Content(Request succeeded; no body returned).WebServiceActionResultCode::Updated->200 OK(Returns updated entity JSON in response body).WebServiceActionResultCode::Created->201 Created(Returns newly created entity JSON +Locationresponse header).WebServiceActionResultCode::Deleted->204 No Content(Resource was successfully deleted).
What is the primary architectural difference between a bound OData action and an unbound OData action in Dynamics 365 Business Central?
An integration developer is authoring an AL procedure on an API Page to allow external systems to trigger the automated release of sales orders. Which attribute must be applied to the AL procedure so that it is exposed as an OData Action in the API metadata?
A developer writes an AL bound action on an API page that generates a new posted shipment record. The developer wants the external REST client to receive an HTTP 201 Created status code along with the Location header of the new shipment. How should the developer configure the WebServiceActionContext parameter?
An external client application needs to invoke a bound OData action named 'postInvoice' on a specific Sales Invoice entity record. Which HTTP method and URL format must the client use?