Skip to main content

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

PropertyTypeDefaultDescription
initialActionstring?nullThe 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.
scanActionstring?nullThe 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.
actionsIAction[][]Array of all action definitions available on this page. Each action has a type discriminator (api, navigate, filter, lookup). See Actions Reference.
filtersFilterDefinition[][]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

WherePropertyExample
Contract lifecyclecontract.initialAction"loadData" -- executes on page load
Contract lifecyclecontract.scanAction"onScan" -- executes on barcode scan
Button componentaction"submitPick" -- executes on tap
Repeater componentonTap"onRowTap" -- executes when a row is tapped
Selector componentonSelect"onFilterChanged" -- executes when selection changes
Footer actionaction"refreshData" -- executes on footer button tap
Drawer itemactionId"navigateToSettings" -- executes on drawer item tap
Input componentaction"onSubmit" -- executes on keyboard submit
ConfirmCommandactionId"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:

  1. Input prompt values -- if the action has an input dialog, the user's input is stored under the inputKey.
  2. Navigation data -- key-value pairs passed via a navigate action's data map.
  3. Filter selections -- current values of filters whose id matches a parameter key.
  4. 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:

  1. The client resolves the action by id from the actions array.
  2. It must be an api action (the Kotlin source casts it to ApiAction).
  3. The client sends a JSON-RPC call to the method specified by the action.
  4. The response data is merged into the page's data store.
  5. 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 scanAction is specified, the client executes that action with the scanned value.
  • If scanAction is null, the client falls back to initialAction.
  • The scanned value is typically available via a scanInput parameter 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:

  1. Page loads -- initialAction fires loadPicks, calling GetPicks on the PICKSERVICE codeunit. The segment filter defaults to "All".
  2. User changes filter -- Selecting "Open" triggers onFilterChanged, re-calling GetPicks with statusFilter = "Open".
  3. User scans a barcode -- scanAction fires onScan, calling FindPick with the scanned value. The server can return a NavigateCommand to go directly to the pick detail, or return updated list data.
  4. User taps a row -- onPickTap navigates to PICK-DETAIL with PickNo interpolated from the tapped row.
  5. User pulls to refresh -- Since header.refresh is true, the client re-executes loadPicks with the current filter value.
  6. User taps the footer button -- The footer "Refresh" button triggers loadPicks directly.