---
title: ListObject (LO) & ListItem (LI) — Layer 3
aliases: [LO, LI, ListObject, ListItem, list object, collection]
sources:
    [sources/sessions/2026-02-18-listobject-analysis.md, sources/sessions/2026-04-19-ds-macros-and-parameter-flow.md]
last_updated: 2026-04-19
status: draft
---

# ListObject (LO) & ListItem (LI) — Layer 3

ListObjects represent **collections of items** (e.g., list of visits, list of tasks). The item structure is defined in a separate ListItem file.

## Overview

LOs manage collections where:

-   The LO defines the datasource, methods, and item class
-   The LI defines the property structure of each item in the collection
-   Methods on the LO operate on the collection as a whole
-   Data loading can be immediate (with parent) or on-demand

## Creating an LO from Scratch — Complete Pattern

### Step 1: Create DataSource (`src/{Module}/DS/DsLo{Name}_sf.datasource.xml`)

**Declarative pattern** (simple filters):

```xml
<DataSource name="DsLo{Name}" backendSystem="sf" businessObjectClass="Lo{Name}"
            external="false" editableEntity="{SalesforceObject}" schemaVersion="2.0">
  <Attributes>
    <Attribute name="pKey" table="{Object}" column="Id" />
    <Attribute name="name" table="{Object}" column="Name" />
    <!-- Joined fields -->
    <Attribute name="relatedName" table="{Related}" column="Name" />
    <!-- DateTime splits -->
    <DateTimeAttribute dateName="startDate" timeName="startTime"
                       table="{Object}" column="StartDateTime" />
    <!-- Computed/icon fields -->
    <DerivedAttribute name="statusIcon"
                      value="CASE WHEN {Object}.Status = 'Active' THEN 'Active24' ELSE 'Inactive24' END" />
  </Attributes>
  <Entities>
    <Entity name="{Object}" alias="" idAttribute="Id" />
    <Entity name="{Related}" alias="">
      <Join Type="left">
        <SimpleJoin>
          <Condition leftSideValue="{Object}.RelatedId"
                     comparator="eq"
                     rightSideType="Attribute"
                     rightSideValue="{Related}.Id" />
        </SimpleJoin>
      </Join>
    </Entity>
  </Entities>
  <QueryCondition><![CDATA[
    {Object}.OwnerId = '#UserPKey#'
    AND {Object}.IsDeleted = '0'
    #cond#
  ]]></QueryCondition>
  <OrderCriteria>
    <OrderCriterion entity="{Object}" attribute="CreatedDate" direction="DESC" />
  </OrderCriteria>
  <Parameters>
    <Parameter name="cond" treatAs="sqlSnippet" />
    <Parameter name="filterDate" type="INTEGER" />
  </Parameters>
</DataSource>
```

**Key decisions:**

-   Use `readOnly="true"` if list is display-only
-   Use `external="true"` for scripted DS pattern
-   Add `#cond#` parameter with `treatAs="sqlSnippet"` if card methods will inject conditions
-   Declare all parameters that BL methods will pass

### Step 2: Create ListObject (`src/{Module}/BO/Lo{Name}/Lo{Name}.listobject.xml`)

```xml
<ListObject name="Lo{Name}" generateLoadMethod="false" filter="InDatabase"
            paging="true" schemaVersion="1.1">
  <DataSource name="DsLo{Name}" />
  <Item objectClass="Li{Name}" />
  <Methods>
    <Method name="beforeLoadAsync" />
    <Method name="afterLoadAsync" />
    <!-- Card methods (if used in cockpit) -->
    <Method name="getTasksForCard" />
    <Method name="getInfoForCard" />
    <!-- Custom methods -->
    <Method name="setFirstItemAsCurrent" />
  </Methods>
</ListObject>
```

**Key attributes:**

-   `generateLoadMethod="false"` — load logic handled by DS; use `true` if you need framework auto-generation
-   `filter="InDatabase"` — filtering happens at SQL level (preferred for performance)
-   `paging="true"` — enables pagination for large result sets

### Step 3: Create ListItem (`src/{Module}/BO/Lo{Name}/Li{Name}.listitem.xml`)

```xml
<ListItem name="Li{Name}">
  <SimpleProperties>
    <!-- Primary key (required) -->
    <SimpleProperty id="true" name="pKey" type="DomPKey" storable="false"
                    dataSourceProperty="pKey" />
    <!-- DS-mapped properties -->
    <SimpleProperty name="name" type="DomText" storable="false"
                    dataSourceProperty="name" />
    <SimpleProperty name="startDate" type="DomDate" storable="false"
                    dataSourceProperty="startDate" />
    <SimpleProperty name="status" type="DomText" storable="false"
                    dataSourceProperty="issuePhase" />
    <!-- Computed properties (no dataSourceProperty — set in afterLoadAsync or card method) -->
    <SimpleProperty name="formattedDateText" type="DomText" storable="false" />
    <!-- Icon/image properties from DerivedAttribute -->
    <SimpleProperty name="statusIcon" type="DomString" storable="false"
                    dataSourceProperty="statusIcon" />
  </SimpleProperties>
</ListItem>
```

**Key rules:**

-   One property MUST have `id="true"` (the primary key)
-   Use `storable="false"` for read-only datasource properties
-   `dataSourceProperty` value must match an Attribute name in the DS
-   Properties without `dataSourceProperty` are computed in BL
-   Type must be a valid Dom\* type (DomPKey, DomText, DomDate, DomTime, DomString, DomBool, DomMoney, etc.)

### Step 4: Create BL Methods

**BeforeLoadAsync** (`src/{Module}/BO/Lo{Name}/Mv2/LoadAsync/Lo{Name}.BeforeLoadAsync.bl.js`):

```javascript
'use strict';
/**
 * @function beforeLoadAsync
 * @this Lo{Name}
 * @kind listobject
 * @async
 * @namespace CUSTOM
 * @param {Object} context
 * @returns promise
 */
function beforeLoadAsync(context) {
    var me = this;
    var promise = when.resolve(context);
    return promise;
}
```

**AfterLoadAsync** (`src/{Module}/BO/Lo{Name}/Mv2/LoadAsync/Lo{Name}.AfterLoadAsync.bl.js`):

```javascript
'use strict';
/**
 * @function afterLoadAsync
 * @this Lo{Name}
 * @kind listobject
 * @async
 * @namespace CUSTOM
 * @param {Object} context
 * @returns promise
 */
function afterLoadAsync(context) {
    var me = this;
    var promise = when.resolve(context);
    return promise;
}
```

**GetTasksForCard** (card loading method — `src/{Module}/BO/Lo{Name}/Mv2/Lo{Name}.GetTasksForCard.bl.js`):

```javascript
'use strict';
/**
 * @function getTasksForCard
 * @this Lo{Name}
 * @kind listobject
 * @async
 * @namespace CUSTOM
 * @param {DomInteger} numberOfListItems
 * @param {String} cardDate
 * @returns promise
 */
function getTasksForCard(numberOfListItems, cardDate) {
    var me = this;

    var jsonQuery = {};
    var jsonParams = [];
    jsonQuery.params = jsonParams;

    var convertedCardDate = Utils.convertForDBParam(cardDate, 'DomDate');
    jsonQuery.cond = " AND {Object}.Status IN ('Active', 'Pending') AND {Object}.DueDate <= #cardDate# ";
    jsonQuery.params.push({ field: 'cardDate', value: convertedCardDate });

    me.removeAllItems();

    var promise = Facade.getListAsync('Lo{Name}', jsonQuery).then(function (items) {
        var numberOfItems;
        if (!Utils.isDefined(numberOfListItems)) {
            numberOfItems = Utils.isPhone() ? 3 : 5;
        } else {
            numberOfItems = numberOfListItems;
        }

        me.addItems(items, jsonQuery.params);
        me.orderBy({ startDate: 'DESC' });
        items = me.getAllItems();
        me.cardItemCount = items.length;
        me.removeAllItems();
        items = items.splice(0, numberOfItems);
        me.addItems(items, jsonQuery.params);
        return me;
    });

    return promise;
}
```

**GetInfoForCard** (`src/{Module}/BO/Lo{Name}/Mv2/Lo{Name}.GetInfoForCard.bl.js`):

```javascript
'use strict';
/**
 * @function getInfoForCard
 * @this Lo{Name}
 * @kind listobject
 * @namespace CUSTOM
 * @returns String
 */
function getInfoForCard() {
    var me = this;
    var count = me.cardItemCount;
    var visible = me.getAllItems().length;
    return visible + ' / ' + count;
}
```

### Step 5: Wire into Process

**If standalone (loaded directly by Process):**

```xml
<Declaration name="ItemList" type="Lo{Name}" />

<Action name="LoadItems" actionType="LOAD" type="Lo{Name}">
  <Parameters>
    <Input name="filterParam" value="ProcessContext::SomeValue" />
  </Parameters>
  <Return name="ProcessContext::ItemList" />
</Action>
```

**If child of a BO (automatic loading):**

```xml
<!-- In parent BO's .businessobject.xml -->
<ListObjects>
  <ListObject name="loItems" objectClass="Lo{Name}"
              dataSourceProperty="pKey" listProperty="parentPKey"
              loadMode="LoadImmediate" />
</ListObjects>
```

**If cockpit card (lazy load via LOGIC):**

```xml
<!-- EntryAction: CREATE empty -->
<Action name="Card{Name}_GetList" actionType="CREATE" type="Lo{Name}">
  <Return name="ProcessContext::Card{Name}_List" />
</Action>

<!-- Load chain: LOGIC call to card method -->
<Action actionType="LOGIC" name="Card{Name}_LoadItems"
        call="ProcessContext::Card{Name}_List.getTasksForCard">
  <Parameters>
    <Input name="numberOfListItems" value="ProcessContext::CardController.numberOfListItems" />
    <Input name="cardDate" value="ProcessContext::CardDate" />
  </Parameters>
  <TransitionTo action="Card{Name}_GetCardInformation" />
</Action>
```

## Loading Mechanisms

### Facade.getListAsync vs me.loadAsync

| Aspect                | `Facade.getListAsync(loName, jsonQuery)` | `me.loadAsync(jsonQuery)` |
| --------------------- | ---------------------------------------- | ------------------------- |
| **Returns**           | Array of raw items                       | Populated LO instance     |
| **Lifecycle**         | No beforeLoadAsync/afterLoadAsync        | Full lifecycle hooks      |
| **Used in**           | Card methods, custom queries             | Framework initialization  |
| **Parameter control** | Explicit jsonQuery                       | Handled by framework      |
| **Pattern**           | Load → addItems → modify → return        | Load → return             |

### jsonQuery Structure

```javascript
var jsonQuery = {
    params: [
        { field: 'paramName', value: 'convertedValue' },
        { field: 'anotherParam', value: 'value2' },
    ],
    cond: ' AND Field = #paramName# ', // Raw SQL injected via treatAs="sqlSnippet"
};
```

## loadMode for Child ListObjects

| Mode                        | Behavior                                | Use Case                  |
| --------------------------- | --------------------------------------- | ------------------------- |
| `LoadImmediate`             | Loads when parent BO loads              | Essential child data      |
| `LoadOnDemand` / `onDemand` | Loads only when accessed in code        | Optional/large child data |
| _(omitted)_                 | Framework default (typically on-demand) | Legacy patterns           |

## BO vs LO vs LI Comparison

| Aspect               | Business Object (BO)  | ListObject (LO)     | ListItem (LI)      |
| -------------------- | --------------------- | ------------------- | ------------------ |
| **Represents**       | Single entity         | Collection          | Item in collection |
| **File**             | `.businessobject.xml` | `.listobject.xml`   | `.listitem.xml`    |
| **Properties**       | In BO definition      | In separate LI      | In LI definition   |
| **DataSource**       | One record            | Multiple records    | Uses LO's DS       |
| **Create lifecycle** | Yes                   | No                  | No                 |
| **Access**           | `bo.getName()`        | `lo.getAllItems()`  | `item.getName()`   |
| **Use case**         | Detail/form screens   | List screens, cards | List row rendering |

## Collection Operations

```javascript
lo.getAllItems(); // Get all items as array
lo.getItemByPKey(pKey); // Get one by ID
lo.addItem(item); // Add one
lo.addItems(items); // Add array
lo.addItems(items, params); // Add array with parameter context
lo.removeItem(item); // Remove one
lo.removeAllItems(); // Clear all
lo.getItemCount(); // Count
lo.getItemAt(index); // By index
lo.orderBy({ field: 'ASC' }); // Sort items
lo.setFilter(field, val, op); // Set filter
lo.resetFilter(field); // Reset filter
```

## Complete Real Example: LoMyTask

**DS:** `src/Visit/DS/DsLoMyTask_sf.datasource.xml` — Declarative with `#cond#`, joins User (Responsible + Initiator) and Account (What), system macro `#UserPKey#` for owner filter

**LO:** `src/Visit/BO/LoMyTask/LoMyTask.listobject.xml` — `generateLoadMethod="false"`, `filter="InDatabase"`, `paging="true"`

**LI:** `src/Visit/BO/LoMyTask/LiMyTask.listitem.xml` — 24 properties including pKey, text, dates, computed icons, all `storable="false"`

**BL:** `src/Visit/BO/LoMyTask/Mv2/LoMyTask.GetTasksForCard.bl.js` — Builds jsonQuery with cond for status filter + date filter, uses Facade.getListAsync, orders by dueDate/priority, limits to 3 (phone) or 5 (tablet)

**Process wiring:** Application_CockpitProcess creates empty LoMyTask in EntryActions, loads via LOGIC action on CardLoadEvent

## Best Practices

| Do                                                            | Don't                                          |
| ------------------------------------------------------------- | ---------------------------------------------- |
| Use `storable="false"` on LI properties for read-only lists   | Leave storable default (implies write)         |
| Use `filter="InDatabase"` for performance                     | Filter in JavaScript after loading all records |
| Store `cardItemCount` before splicing for accurate "X / Y"    | Count after truncation                         |
| Use `Utils.convertForDBParam()` for all parameter values      | Pass raw values                                |
| Always `removeAllItems()` before `addItems()` in card methods | Append to existing items                       |
| Use `orderBy()` after adding items for consistent sort        | Rely on SQL ORDER BY alone                     |

## Validation rules

The modeler validates ListObject and ListItem as two separate contracts, but the sibling rules are listed jointly here because authors almost always edit them in the same breath. ListObject cross-contract validation is unusual — it checks the bound ListItem's properties against the DataSource, not the LO itself.

### Cross-cutting (every contract)

-   Contract names must be unique workspace-wide.
-   The 5-platform-object deployment cap applies to BOs but is also scanned during LO validation.

### ListObject rules

#### Must

-   Root element is `<ListObject>`.
-   File name starts with `Lo` and ends with `.listobject.xml`. Custom LOs additionally use the customizing prefix in both the file name and the root `@name`.
-   An `<AdvancedSearchAttributes>` container must define at least one `<AdvancedSearchAttribute>` — an empty container is warned.
-   Each `<AdvancedSearchAttribute>` must declare `@type`, and the value must be one of `boolean`, `date`, `distance`, `lookup`, `number`, `selection`, `text`, `time`.
-   A `distance`-type attribute must declare both `latitudeProperty` and `longitudeProperty`.
-   Storable LOs should reference a DataSource (missing DS is warned, not rejected).
-   The bound ListItem's `<SimpleProperty @dataSourceProperty>` must resolve in the DataSource. Mismatches are errors (downgraded to warning for Salesforce external DSs).

#### Must not

-   At most one `<AdvancedSearchAttribute>` of type `Distance` per container — duplicates are rejected.
-   A `distance`-type attribute must not declare `defaultOperator`.

#### Coerced (silently rewritten)

-   The legacy `lookupParameters` element is warned; authors should migrate to `Parameters`.
-   `xmlns="*.xsd"` on the root is stripped during pre-processing.

### ListItem rules

#### Must

-   Root element is `<ListItem>`.
-   File name starts with `Li` and ends with `.listitem.xml`. Custom LIs use the customizing prefix in both the file name and the root `@name`.
-   One (and only one) `<SimpleProperty>` should carry `id="true"` — this is the row primary key used to resolve cross-contract references.

#### Must not

-   A SimpleProperty `@type` cannot start with `__`.
-   A SimpleProperty `@blobTable` cannot reference a temp table (name ending in `_T`).

Internal schema references: `rcg-mobile-dev-agent/wiki/contracts/list-object.md` and `rcg-mobile-dev-agent/wiki/contracts/list-item.md`.

## Cross-References

-   [[datasource]] — DS patterns, macros, parameter flow
-   [[business-objects]] — LOs can be children of a BO via `<ListObject>` declaration
-   [[business-logic]] — BL files implement LO methods (getTasksForCard, etc.)
-   [[processes]] — Process loads LOs and passes parameters
-   [[cockpit-cards]] — Cards use the CREATE + LOGIC card loading pattern
