Data Action
Data actions provide RESTful API access to Business Central tables, enabling AJAX operations and external integrations without writing code.
Overview
A Data action:
- Exposes table data via HTTP methods (GET, POST, PATCH, DELETE)
- Returns JSON-formatted data
- Supports filtering and field selection
- Works with DataItems for multiple tables
When to Use Data Actions
Use Data actions for:
- AJAX calls from JavaScript
- Mobile app backends
- Third-party integrations
- Single-page applications (SPA)
- Real-time data updates
Creating a Data Action
Step-by-Step
- Open the Advanced Portal Designer menu page (search for it in Tell Me) and click Actions
- Click New
- Enter the Web Action code
- Set Type to "Data"
- Configure HTTP methods allowed
- Add DataItems for tables to expose
- Save the action
HTTP Method Configuration
Configure which operations are allowed:
| Field | HTTP Method | Operation |
|---|---|---|
| Allow Read (GET) | GET | Read records |
| Allow Insert (POST) | POST | Create records |
| Allow Modify (PATCH) | PATCH | Update records |
| Allow Delete (DELETE) | DELETE | Remove records |
Security Note
Only enable the methods you need. Disabling unused methods reduces attack surface.
DataItems
DataItems define which tables are exposed through the action.
Adding a DataItem
- In the action card, navigate to the Data section
- Click in the DataItems list
- Add a new DataItem
- Select the table number
- Configure columns to expose
- Set filters if needed
DataItem Properties
| Property | Description |
|---|---|
| Table No. | The BC table to expose |
| View | Filter expression (optional) |
| Columns | Which fields to include |
Configuring Columns
For each DataItem, specify which columns to return:
- Navigate to Columns for the DataItem
- Add fields to include
- Set field order
- Optionally rename fields
Enum and Option Display
Show Enum and Option Fields as Text
When enabled:
- Option fields return text values (e.g., "Order" instead of 1)
- Enum fields return member names
- Easier for frontend consumption
When disabled:
- Returns integer values
- More compact response
- Requires frontend mapping
Making Requests
Important — APD Data actions are not OData. APD has its own simple JSON envelope and its own filter model. There is no
$filterURL parameter. Pagination uses$topand$skipURL parameters. Filtering is configured per DataItem inside BC (see Filtering below).
GET Request (Read)
// Fetch records (all filters configured on the DataItem in BC)
fetch('/wsfn/?action=API-CUSTOMERS')
.then(response => response.json())
.then(data => console.log(data));
// Pagination: top N, skip M
fetch('/wsfn/?action=API-CUSTOMERS&$top=50&$skip=100')
.then(response => response.json())
.then(data => console.log(data));
// Pass a value to a DataItem filter that is configured with FilterType = Parameter
// (here the DataItem's filter references the URL parameter named "customerno")
fetch('/wsfn/?action=API-ORDERS&customerno=10000')
.then(response => response.json())
.then(data => console.log(data));
POST / PATCH / DELETE Requests
POST (Insert), PATCH (Modify) and DELETE bodies are JSON objects keyed by the DataItem's Element Name (the name you set on the DataItem in BC), with an array of records. Child DataItems are nested under their own Element Name.
// Insert one or more customer records (Element Name = "customers")
fetch('/wsfn/?action=API-CUSTOMERS', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
customers: [
{ Name: 'New Customer', City: 'New York' }
]
})
});
// PATCH must include the primary-key field(s) so APD can locate the record
fetch('/wsfn/?action=API-CUSTOMERS', {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
customers: [
{ "No.": "10000", Name: 'Updated Name' }
]
})
});
// DELETE also takes a body with the primary key
fetch('/wsfn/?action=API-CUSTOMERS', {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
customers: [ { "No.": "10000" } ]
})
});
POST/PATCH/DELETE currently do not return a JSON document — they return an empty body on success and an AL error on failure. Re-read with GET if you need the resulting state.
Response Format
Successful Read
The response envelope uses the DataItem's Element Name as the property key (the example below assumes Element Name = customers). $more indicates whether more rows exist beyond $top.
{
"customers": [
{ "No.": "10000", "Name": "Customer A", "City": "New York" },
{ "No.": "10001", "Name": "Customer B", "City": "Los Angeles" }
],
"$more": false
}
When the DataItem has child DataItems, child rows appear nested inside each parent row under the child's Element Name:
{
"orders": [
{
"No.": "PS-1001",
"Sell-to Customer No.": "10000",
"lines": [
{ "Line No.": 10000, "No.": "ITEM-A", "Quantity": 2 },
{ "Line No.": 20000, "No.": "ITEM-B", "Quantity": 1 }
]
}
],
"$more": false
}
Error Response
Errors come back as the standard Business Central API error envelope (e.g. HTTP error from the underlying AL error). Inspect the response status and body on the client.
Filtering
APD Data actions do not parse OData
$filterexpressions. All filters live on the DataItem inside Business Central. The supported filter types are:
| Filter Type | Behaves like | Filter Value |
|---|---|---|
Link |
A flowfield link from a parent DataItem | The field number on the parent that supplies the value |
Const |
SetRange(<field>, <value>) |
A literal value validated against the field type |
Filter |
SetFilter(<field>, <expression>) |
A BC filter expression (e.g. 10000..20000, <>BLOCKED) |
Parameter |
SetFilter(<field>, urlParam) |
The name of a URL parameter — APD reads that parameter at runtime and applies it as a filter |
To accept a value from the URL, add a filter of type Parameter on the DataItem field and set Filter Value to the parameter name. Then the URL ?action=API-ORDERS&customerno=10000 will filter the linked field to 10000.
Security filters (configured under Security Settings) are always applied on top of DataItem filters.
Security
Security Profiles
Data actions respect security profiles:
- Configure in Security Settings
- Control table-level access
- Limit operations per profile
Record-Level Security
For per-user record filtering, use Security Filters (configured in Security Settings under the Advanced Portal Designer menu) rather than DataItem filters. Security Filters automatically apply placeholders like %1 (User ID), %4 (BC Number / NAV Identification), etc. to the table at filter group 6, so users only ever see their own records. See Security Filters for the full placeholder reference.
Required Fields
For POST/PATCH operations, validate required fields in security settings or through triggers.
Using with JavaScript
With APD JavaScript Library
// Using APD_GetAction for simple requests
var result = APD_GetAction('API-CUSTOMERS', 'city=New York');
// Parse the result
var data = JSON.parse(result);
With Fetch API
async function getCustomers() {
const response = await fetch('/wsfn/?action=API-CUSTOMERS');
if (!response.ok) {
throw new Error('API request failed');
}
return await response.json();
}
With AJAX
var xhr = new XMLHttpRequest();
xhr.open('GET', '/wsfn/?action=API-CUSTOMERS', true);
xhr.onload = function() {
if (xhr.status === 200) {
var data = JSON.parse(xhr.responseText);
// Process data
}
};
xhr.send();
Override MIME Type
The default MIME type is appropriate for JSON. Override only if needed:
| Override | Use Case |
|---|---|
| (default) | Standard JSON API |
| application/json | Explicit JSON |
| text/plain | Plain text responses |
Multiple DataItems
You can include multiple tables in one action:
DataItem 1: Customer
DataItem 2: Sales Header (linked to Customer)
DataItem 3: Sales Line (linked to Sales Header)
This creates hierarchical data suitable for master-detail scenarios.
Best Practices
API Naming
- Use clear, RESTful names: API-CUSTOMERS, API-ORDERS
- Include version if needed: API-V2-CUSTOMERS
- Be consistent across your API
Field Selection
- Only expose necessary fields
- Avoid sensitive data in responses
- Consider data volume
Security
- Always require authentication for write operations
- Implement proper security filters
- Validate all input data
Performance
- Use pagination for large datasets
- Apply appropriate filters
- Consider caching for read-only data
Troubleshooting
Empty Response
- Check DataItem configuration
- Verify filters aren't too restrictive
- Ensure columns are configured
Access Denied
- Verify authentication
- Check security profile permissions
- Review table-level security
Invalid JSON
- Check MIME type setting
- Verify no HTML errors in response
- Test with simple request first