Data Contract
The contract object in a page JSON definition controls how the page interacts with AL backend services -- what data to load on startup, how to handle barcode scans, which actions are available, and which filters to display. It is the bridge between the declarative UI layout and the imperative business logic in Business Central.
Defined in Contract.kt as a @Serializable data class.
Contract structure
| Property | Type | Default | Description |
|---|---|---|---|
initialAction | string? | null | The id of an action to execute automatically when the page loads. Also re-executed on pull-to-refresh when header.refresh is true. Must reference an api action. |
scanAction | string? | null | The id of an action to execute when a barcode or QR scan is triggered on this page. Must reference an api action. Falls back to initialAction if not specified. |
actions | IAction[] | [] | Array of all action definitions available on this page. Each action has a type discriminator (api, navigate, filter, lookup). See Actions Reference. |
filters | FilterDefinition[] | [] | Array of filter definitions for the filter panel. See Filters Reference. |
Minimal example
A page that loads data on startup with no filters:
{
"contract": {
"initialAction": "loadData",
"actions": [
{
"id": "loadData",
"type": "api",
"method": "GetData",
"params": []
}
]
}
}
Complete example
{
"contract": {
"initialAction": "loadOrders",
"scanAction": "onScan",
"actions": [
{
"id": "loadOrders",
"type": "api",
"method": "GetOrders",
"params": ["statusFilter"]
},
{
"id": "onScan",
"type": "api",
"method": "ProcessScan",
"params": ["scanInput"]
},
{
"id": "onFilterChanged",
"type": "filter",
"endpoint": "GetOrders",
"params": ["statusFilter"]
},
{
"id": "onOrderTap",
"type": "navigate",
"destination": "ORDER-DETAIL",
"data": {
"OrderNo": "{Document No}"
}
}
],
"filters": [
{
"id": "statusFilter",
"label": "Status",
"mode": "SEGMENT",
"options": ["All", "Open", "Released"]
}
]
}
}
How actions are referenced by ID
Actions are the wiring that connects UI events to backend logic. Every action has a unique id string, and components reference that id to trigger the action.
Reference points
| Where | Property | Example |
|---|---|---|
| Contract lifecycle | contract.initialAction | "loadData" -- executes on page load |
| Contract lifecycle | contract.scanAction | "onScan" -- executes on barcode scan |
| Button component | action | "submitPick" -- executes on tap |
| Repeater component | onTap | "onRowTap" -- executes when a row is tapped |
| Selector component | onSelect | "onFilterChanged" -- executes when selection changes |
| Footer action | action | "refreshData" -- executes on footer button tap |
| Drawer item | actionId | "navigateToSettings" -- executes on drawer item tap |
| Input component | action | "onSubmit" -- executes on keyboard submit |
| ConfirmCommand | actionId | "overwriteLine" -- executes if user confirms |
Resolution
When the client needs to execute an action, it looks up the id in the contract.actions array, determines the action type from the type discriminator, and dispatches accordingly:
"api"-- sends a JSON-RPC request to the service."navigate"-- pushes the destination page onto the navigation stack."filter"-- sends a request with the current filter values."lookup"-- opens a lookup modal populated from a repeater component.
Parameter passing between pages
When a navigate action includes a data map, those key-value pairs become the initial data context of the destination page. The destination page's API actions can reference these keys in their params arrays.
Source page
{
"id": "onOrderTap",
"type": "navigate",
"destination": "ORDER-DETAIL",
"data": {
"OrderNo": "{Document No}",
"OrderType": "{Type}"
}
}
Values in {curly braces} are interpolated from the current data context -- typically the tapped row in a repeater.
Destination page
{
"contract": {
"initialAction": "loadOrder",
"actions": [
{
"id": "loadOrder",
"type": "api",
"method": "GetOrder",
"params": ["OrderNo", "OrderType"]
}
]
}
}
When the destination page loads, initialAction fires automatically. The client passes OrderNo and OrderType (received from navigation data) as parameters to the GetOrder method. In AL, read them with State.GetInput('OrderNo') and State.GetInput('OrderType').
Parameter sources
The client resolves parameter values from multiple sources in this priority order:
- Input prompt values -- if the action has an
inputdialog, the user's input is stored under theinputKey. - Navigation data -- key-value pairs passed via a
navigateaction'sdatamap. - Filter selections -- current values of filters whose
idmatches a parameter key. - Component data -- values from input components, scan fields, or the currently selected repeater row.
Initial data loading
The initialAction property drives the page's startup behavior. When the client navigates to a page that has an initialAction:
- The client resolves the action by
idfrom theactionsarray. - It must be an
apiaction (the Kotlin source casts it toApiAction). - The client sends a JSON-RPC call to the method specified by the action.
- The response data is merged into the page's data store.
- All bound components (repeaters, text fields, badges) update to reflect the new data.
If initialAction is null, the page loads with no data -- it relies on user-initiated actions (button taps, scans) to populate content.
Pull-to-refresh
When header.refresh is true, the user can pull down on the page to trigger a refresh. This re-executes the initialAction with the current parameter values (including any active filter selections).
Scan behavior
The scanAction property defines what happens when the user triggers a hardware barcode scan or uses the scan input on this page.
- If
scanActionis specified, the client executes that action with the scanned value. - If
scanActionisnull, the client falls back toinitialAction. - The scanned value is typically available via a
scanInputparameter key or through an input component bound to the scan field.
Common scan patterns
Pattern 1: Scan to search -- The scan action calls the same method as the initial action but with the scanned value as a filter:
{
"contract": {
"initialAction": "loadItems",
"scanAction": "loadItems",
"actions": [
{
"id": "loadItems",
"type": "api",
"method": "GetItems",
"params": ["scanInput"]
}
]
}
}
Pattern 2: Scan to process -- The scan action calls a different method that processes the scanned barcode (e.g., adding a line to a pick):
{
"contract": {
"initialAction": "loadPickLines",
"scanAction": "processScan",
"actions": [
{
"id": "loadPickLines",
"type": "api",
"method": "GetPickLines",
"params": []
},
{
"id": "processScan",
"type": "api",
"method": "ProcessBarcode",
"params": ["scanInput"]
}
]
}
}
Pattern 3: Scan with confirmation -- The scan action includes a confirmation dialog for destructive operations:
{
"contract": {
"scanAction": "scanAndPost",
"actions": [
{
"id": "scanAndPost",
"type": "api",
"method": "ScanAndPost",
"params": ["scanInput"],
"confirm": {
"title": "Post Receipt",
"message": "Post this item receipt?",
"confirmLabel": "Post",
"cancelLabel": "Cancel"
}
}
]
}
}
Putting it all together
Here is a complete page definition showing how the contract connects to the header, body components, and footer:
{
"pageId": "WAREHOUSE-PICKS",
"service": "PICKSERVICE",
"header": {
"title": "Warehouse Picks",
"refresh": true,
"showFilterInHeader": true
},
"body": [
{
"type": "input",
"props": {
"dataKey": "scanInput",
"placeholder": "Scan or enter order number",
"action": "onScan"
}
},
{
"type": "repeater",
"props": {
"dataKey": "picks",
"key": "No",
"template": "CARD",
"onTap": "onPickTap",
"emptyMessage": "No picks found"
}
}
],
"footer": {
"actions": [
{ "label": "Refresh", "action": "loadPicks", "icon": "refresh", "tone": "HIGH" }
]
},
"contract": {
"initialAction": "loadPicks",
"scanAction": "onScan",
"actions": [
{
"id": "loadPicks",
"type": "api",
"method": "GetPicks",
"params": ["statusFilter"]
},
{
"id": "onScan",
"type": "api",
"method": "FindPick",
"params": ["scanInput"]
},
{
"id": "onFilterChanged",
"type": "filter",
"endpoint": "GetPicks",
"params": ["statusFilter"]
},
{
"id": "onPickTap",
"type": "navigate",
"destination": "PICK-DETAIL",
"data": {
"PickNo": "{No}"
}
}
],
"filters": [
{
"id": "statusFilter",
"label": "Status",
"mode": "SEGMENT",
"options": ["All", "Open", "In Progress"],
"defaultValue": "All"
}
]
}
}
Flow:
- Page loads --
initialActionfiresloadPicks, callingGetPickson thePICKSERVICEcodeunit. The segment filter defaults to "All". - User changes filter -- Selecting "Open" triggers
onFilterChanged, re-callingGetPickswithstatusFilter = "Open". - User scans a barcode --
scanActionfiresonScan, callingFindPickwith the scanned value. The server can return aNavigateCommandto go directly to the pick detail, or return updated list data. - User taps a row --
onPickTapnavigates toPICK-DETAILwithPickNointerpolated from the tapped row. - User pulls to refresh -- Since
header.refreshistrue, the client re-executesloadPickswith the current filter value. - User taps the footer button -- The footer "Refresh" button triggers
loadPicksdirectly.