---
title: Business Object (BO) — Layer 2
aliases: [BO, business object, BusinessObject, Bo]
sources:
    [
        sources/sessions/2026-02-18-businessobject-analysis.md,
        sources/sessions/2026-04-20-bo-creation-and-relationships.md,
    ]
last_updated: 2026-04-21
status: draft
---

# Business Object (BO) — Layer 2

Business Objects represent **single entities** (one Account, one Visit, one Task). They map DS attributes to strongly-typed properties, implement business rules through lifecycle hooks, and expose custom methods.

## Creating a BO from Scratch — Complete Pattern

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

```xml
<DataSource name="DsBo{Name}" backendSystem="sf" businessObjectClass="Bo{Name}"
            editableEntity="{SalesforceObject}" schemaVersion="2.0">
  <Attributes>
    <Attribute name="pKey" table="{Object}" column="Id" />
    <Attribute name="name" table="{Object}" column="Name" />
    <!-- Joined fields from related objects -->
    <Attribute name="accountName" table="Account" column="Name" />
    <!-- DateTime splits -->
    <DateTimeAttribute dateName="startDate" timeName="startTime"
                       table="{Object}" column="StartDateTime" />
    <!-- Derived/computed defaults -->
    <DerivedAttribute name="duration" value="'90'" />
    <DerivedAttribute name="salesOrg" value="'#SalesOrg#'" />
  </Attributes>
  <Entities>
    <Entity name="{Object}" alias="" idAttribute="Id" />
    <Entity name="Account" alias="">
      <Join Type="inner">
        <SimpleJoin>
          <Condition leftSideValue="{Object}.AccountId"
                     comparator="eq"
                     rightSideType="Attribute"
                     rightSideValue="Account.Id" />
        </SimpleJoin>
      </Join>
    </Entity>
  </Entities>
  <QueryCondition><![CDATA[
    {Object}.Id = #pKey#
  ]]></QueryCondition>
  <Parameters>
    <Parameter name="pKey" type="TEXT" />
  </Parameters>
</DataSource>
```

**Key decisions:**

-   `editableEntity` specifies which SF object receives writes on save (can differ from read joins)
-   DsBo files are almost always **declarative** (QueryCondition pattern)
-   Standard pattern: load by `#pKey#`, one record returned

### Step 2: Create BusinessObject (`src/{Module}/BO/Bo{Name}/Bo{Name}.businessobject.xml`)

```xml
<BusinessObject name="Bo{Name}" schemaVersion="1.1" generateLoadMethod="true">
  <DataSource name="DsBo{Name}" />
  <SimpleProperties>
    <!-- Primary key (required) -->
    <SimpleProperty name="pKey" type="DomPKey" id="true" dataSourceProperty="pKey" />
    <!-- Persisted properties (read from DS, written on save) -->
    <SimpleProperty name="name" type="DomText" dataSourceProperty="name" />
    <SimpleProperty name="status" type="DomText" dataSourceProperty="status" />
    <!-- Read-only from joined table -->
    <SimpleProperty name="accountName" type="DomText" storable="false"
                    dataSourceProperty="accountName" />
    <!-- Computed properties (no dataSourceProperty — set in afterLoadAsync) -->
    <SimpleProperty name="statusIcon" type="DomString" />
    <SimpleProperty name="isEditEnabled" type="DomBool" />
    <!-- Property with change event -->
    <SimpleProperty name="priority" type="DomText" dataSourceProperty="priority">
      <Events>
        <Event name="onChanged" eventHandler="onPriorityChanged"/>
      </Events>
    </SimpleProperty>
  </SimpleProperties>

  <!-- Child objects (see Relationship Patterns below) -->
  <ObjectLookups>
    <ObjectLookup name="luMeta" objectClass="Lu{Name}Meta"
                  dataSourceProperty="metaPKey" lookupProperty="pKey"
                  loadMode="LoadImmediate" />
  </ObjectLookups>
  <NestedObjects>
    <NestedObject name="boDetail" objectClass="Bo{Name}Detail"
                  dataSourceProperty="pKey" nestingProperty="parentPKey"
                  loadMode="LoadImmediate" />
  </NestedObjects>
  <ListObjects>
    <ListObject name="loItems" objectClass="Lo{Name}Items"
                dataSourceProperty="pKey" listProperty="parentPKey"
                loadMode="LoadImmediate" />
  </ListObjects>

  <Methods>
    <!-- Lifecycle hooks -->
    <Method name="loadAsync" />
    <Method name="saveAsync" />
    <Method name="createAsync" />
    <Method name="beforeLoadAsync" />
    <Method name="afterLoadAsync" />
    <Method name="beforeSaveAsync" />
    <Method name="afterSaveAsync" />
    <Method name="beforeCreateAsync" />
    <Method name="afterCreateAsync" />
    <Method name="beforeInitialize" />
    <Method name="afterInitialize" />
    <Method name="beforeDoValidateAsync" />
    <Method name="afterDoValidateAsync" />
    <!-- Custom methods -->
    <Method name="onPriorityChanged" />
    <Method name="customBusinessMethod" />
  </Methods>
</BusinessObject>
```

### Step 3: Create BL Lifecycle Files

**Directory structure:**

```
Bo{Name}/Mv2/
├── CreateAsync/
│   ├── Bo{Name}.BeforeCreateAsync.bl.js
│   └── Bo{Name}.AfterCreateAsync.bl.js
├── LoadAsync/
│   ├── Bo{Name}.BeforeLoadAsync.bl.js
│   └── Bo{Name}.AfterLoadAsync.bl.js
├── SaveAsync/
│   ├── Bo{Name}.BeforeSaveAsync.bl.js
│   └── Bo{Name}.AfterSaveAsync.bl.js
├── DoValidateAsync/
│   ├── Bo{Name}.BeforeDoValidateAsync.bl.js
│   └── Bo{Name}.AfterDoValidateAsync.bl.js
├── Initialize/
│   ├── Bo{Name}.BeforeInitialize.bl.js
│   └── Bo{Name}.AfterInitialize.bl.js
└── Bo{Name}.CustomMethod.bl.js
```

**AfterLoadAsync** (compute derived properties):

```javascript
'use strict';
/**
 * @function afterLoadAsync
 * @this Bo{Name}
 * @kind businessobject
 * @async
 * @namespace CUSTOM
 * @param {Object} result
 * @param {Object} context
 * @returns promise
 */
function afterLoadAsync(result, context) {
    var me = this;

    // Compute icon from status
    me.setStatusIcon(me.getStatus() === 'Active' ? 'Active24' : 'Inactive24');

    // Set UI visibility flags
    me.setIsEditEnabled(me.getStatus() !== 'Completed');

    // Parse datetime into display format
    var startTime = me.getActualStartTime();
    if (Utils.isDefined(startTime) && !Utils.isEmptyString(startTime)) {
        me.setStartTimeUI(startTime.substring(11, 16));
    }

    var promise = when.resolve(result);
    return promise;
}
```

**AfterCreateAsync** (set defaults for new records):

```javascript
'use strict';
/**
 * @function afterCreateAsync
 * @this Bo{Name}
 * @kind businessobject
 * @async
 * @namespace CUSTOM
 * @param {Object} result
 * @param {Object} context
 * @returns promise
 */
function afterCreateAsync(result, context) {
    var me = this;

    // Set default values for new BO
    me.setStatus('Draft');
    me.setPriority('Normal');
    me.setCreatedDate(Utils.createAnsiDateToday());

    // Load related lookup for defaults
    var promise = BoFactory.loadObjectByParamsAsync('LuDefaults', context.jsonQuery).then(function (luDefaults) {
        if (Utils.isDefined(luDefaults)) {
            me.setDefaultCategory(luDefaults.getCategory());
        }
        return me;
    });

    return promise;
}
```

**AfterDoValidateAsync** (business rule validation):

```javascript
'use strict';
/**
 * @function afterDoValidateAsync
 * @this Bo{Name}
 * @kind businessobject
 * @async
 * @namespace CUSTOM
 * @param {Object} context
 * @returns promise
 */
function afterDoValidateAsync(context) {
    var me = this;
    var messageCollector = context.messageCollector;

    if (Utils.isEmptyString(me.getName())) {
        messageCollector.add({ level: 'error', text: 'Name is required' });
    }
    if (me.getEndDate() < me.getStartDate()) {
        messageCollector.add({ level: 'error', text: 'End date must be after start date' });
    }

    return when.resolve(context);
}
```

### Step 4: Wire into Process

**Load existing BO:**

```xml
<Action name="LoadItem" actionType="LOAD" type="Bo{Name}">
  <Parameters>
    <Input name="pKey" value="ProcessContext::ItemPKey" />
  </Parameters>
  <Return name="ProcessContext::DetailBo" />
</Action>
```

**Create new BO:**

```xml
<Action actionType="CREATE" name="CreateItem" type="Bo{Name}">
  <Parameters>
    <Input name="parentPKey" value="ProcessContext::ParentPKey" />
  </Parameters>
  <Return name="ProcessContext::DetailBo" />
</Action>
```

**Validate and Save:**

```xml
<Action actionType="VALIDATION" name="ValidateItem">
  <Validations>
    <Validation name="ProcessContext::DetailBo" />
  </Validations>
  <TransitionTo action="ValidationDecision" />
</Action>

<Action name="ValidationDecision" actionType="DECISION"
        parameter="ProcessContext::validationResult">
  <Case value="validateOk" action="SaveItem" />
  <CaseElse action="ShowEditView" />
</Action>

<Action name="SaveItem" actionType="SAVE">
  <Parameters>
    <Input name="bo{Name}" value="ProcessContext::DetailBo" />
  </Parameters>
</Action>
```

## BO Lifecycle

```
createAsync  →  BeforeCreateAsync → PKey.next() + init → AfterCreateAsync
                                                               ↓
                                                       BeforeInitialize → AfterInitialize

loadAsync    →  BeforeLoadAsync → DS query → attribute mapping → AfterLoadAsync

saveAsync    →  BeforeDoValidateAsync → AfterDoValidateAsync (messageCollector)
                        ↓ (if valid)
                BeforeSaveAsync → Facade.saveObjectAsync(me) → DS write → AfterSaveAsync
```

## generateLoadMethod / generateCreateMethod

| Attribute                    | Effect                                           |
| ---------------------------- | ------------------------------------------------ |
| `generateLoadMethod="true"`  | Framework auto-generates load (just calls hooks) |
| `generateLoadMethod="false"` | You write custom loadAsync entirely              |
| Neither specified            | Framework decides based on DS                    |

## Property Attributes

| Attribute                   | Purpose                             |
| --------------------------- | ----------------------------------- |
| `id="true"`                 | Marks the primary key property      |
| `dataSourceProperty="name"` | Maps to DS attribute (persisted)    |
| `storable="false"`          | Read-only, not written back on save |
| `storable="true"`           | Explicitly marked for persistence   |
| _(no dataSourceProperty)_   | Computed property, set in BL        |

## Property Events

BOs can react to property value changes:

```xml
<SimpleProperty name="deliveryDate" type="DomDate" dataSourceProperty="deliveryDate">
  <Events>
    <Event name="onChanged" eventHandler="onDeliveryDateChanged"/>
  </Events>
</SimpleProperty>
```

The `eventHandler` must be declared as a Method and implemented in `.bl.js`.

## ACL Pattern (Protected Property Modification)

For fields that shouldn't be freely editable:

```javascript
var aclBo = me.getACL();
aclBo.addRight(AclObjectType.PROPERTY, 'status', AclPermission.EDIT);
me.setStatus('InProgress');
aclBo.removeRight(AclObjectType.PROPERTY, 'status', AclPermission.EDIT);
```

## Child Object Relationship Patterns

### ObjectLookup (Lu\*) — Read-only reference, 1:1

```xml
<ObjectLookup name="luMeta" objectClass="LuVisitMeta"
              dataSourceProperty="metaPKey" lookupProperty="pKey"
              loadMode="LoadImmediate" />
```

-   Read-only (no save back)
-   Returns single record
-   Use for: metadata, reference data, parent info

### NestedObject (Bo\*) — Writable child BO, 1:1

```xml
<NestedObject name="boSalesData" objectClass="BoBpaSales"
              dataSourceProperty="pKey" nestingProperty="businessPartnerPKey"
              loadMode="LoadImmediate" />
```

-   Writable (participates in parent's save)
-   Single child BO
-   Use for: role objects, detail records, sub-entities

### ListObject (Lo*) — Writable collection, 1:*

```xml
<ListObject name="loAddresses" objectClass="LoBpaAddress"
            dataSourceProperty="pKey" listProperty="referencePKey"
            loadMode="LoadImmediate">
  <Events>
    <Event name="listItemChanged" eventHandler="onAddressChanged" />
  </Events>
</ListObject>
```

-   Writable (items can be added/removed/modified)
-   Multiple items
-   Can fire events on item changes
-   Use for: addresses, line items, tasks, contacts

### Wiring Pattern (same for all three)

-   `dataSourceProperty` → Which property VALUE from parent to pass
-   `listProperty` / `nestingProperty` / `lookupProperty` → Parameter NAME in child's DS

Example flow:

```
Parent BO has pKey = "001ABC"
<ListObject dataSourceProperty="pKey" listProperty="visitId" />
→ Child DS receives: jsonQuery = {visitId: "001ABC"}
→ QueryCondition: Task.VisitId = #visitId#
```

## Domain Types Reference

| Type          | Purpose                             | Example Value            |
| ------------- | ----------------------------------- | ------------------------ |
| `DomPKey`     | Primary key (18-char Salesforce ID) | `"001O300001TYlYFIA1"`   |
| `DomText`     | Text string                         | `"Northern Trail"`       |
| `DomLongText` | Long text/description               | Multi-paragraph text     |
| `DomString`   | Generic string (icons, codes)       | `"WarningTriangle_IC"`   |
| `DomBool`     | Boolean                             | `true` / `false`         |
| `DomInteger`  | Whole number                        | `42`                     |
| `DomDecimal`  | Decimal number                      | `123.45`                 |
| `DomMoney`    | Currency value                      | `1299.99`                |
| `DomDate`     | Date only                           | `"2026-02-18"`           |
| `DomTime`     | Time only                           | `"14:30"`                |
| `DomDateTime` | Full timestamp                      | `"2026-02-18T14:30:00Z"` |

Custom domain types (type-safe enumerations):

-   `DomVisitStatus`, `DomVisitPriority`, `DomTaskType`, `DomABC`
-   Used for picklist fields with controlled values

## Process Integration Patterns

| Pattern           | Flow                                          | Use Case              |
| ----------------- | --------------------------------------------- | --------------------- |
| **Load & View**   | LOAD → VIEW                                   | Display detail screen |
| **Create & Edit** | CREATE → VIEW → VALIDATE → SAVE               | New record wizard     |
| **Load & Edit**   | LOAD → VIEW → VALIDATE → SAVE                 | Edit existing record  |
| **Copy**          | LOAD template → CREATE new → copy data → SAVE | Duplicate record      |

## Persistence — Making Save Actually Work

The framework's SAVE process action delegates to the BO's `saveAsync` → `beforeSaveAsync` → `afterSaveAsync` chain. But `generateLoadMethod="true"` only auto-generates `loadAsync` — **it does NOT auto-generate save methods**. Without explicit save infrastructure, the SAVE action silently does nothing.

### Required for Persistence

1. **Declare methods** in `.businessobject.xml`:

```xml
<Method name="saveAsync" />
<Method name="beforeSaveAsync" />
<Method name="afterSaveAsync" />
```

2. **`beforeSaveAsync` must call `Facade.saveObjectAsync(me)`** — this is what actually persists to SQLite:

```javascript
function beforeSaveAsync(context) {
    var me = this;

    // Recombine split date/time into DateTime properties for persistence
    me.setPlannedStartDateTime(me.getPlannedStartDate() + ' ' + me.getPlannedStartTime() + ':00');
    me.setPlannedEndDateTime(me.getPlannedEndDate() + ' ' + me.getPlannedEndTime() + ':00');

    var promise = Facade.saveObjectAsync(me);
    return promise;
}
```

3. **`afterCreateAsync` must set essential fields** — without these, the record either won't save or won't be queryable after save:

```javascript
function afterCreateAsync(result, context) {
    var me = this;

    me.setPKey(PKey.next());
    me.setVisitorId(ApplicationContext.get('user').getPKey());
    me.setStatus('Planned');
    me.setPlannedStartDate(Utils.createAnsiDateToday());
    me.setPlannedEndDate(Utils.createAnsiDateToday());
    me.setPlannedStartTime('08:00');
    me.setPlannedEndTime('09:00');
    me.setObjectStatus(STATE.NEW | STATE.DIRTY);

    return when.resolve(result);
}
```

### DateTimeAttribute Split vs Combined Properties

When a DS uses `<DateTimeAttribute>` to split one SF column into separate date and time fields:

```xml
<DateTimeAttribute dateName="plannedStartDate" timeName="plannedStartTime"
                   table="Visit" column="PlannedVisitStartTime" />
```

The BO needs **both** the split properties (for UI binding) and the combined DateTime property (for persistence):

```xml
<!-- Combined — maps to actual SF column, used for save -->
<SimpleProperty name="plannedStartDateTime" type="DomDateTime"
                dataSourceProperty="plannedVisitStartTime" />
<!-- Split — populated by DateTimeAttribute, used for UI -->
<SimpleProperty name="plannedStartDate" type="DomDate"
                dataSourceProperty="plannedStartDate" />
<SimpleProperty name="plannedStartTime" type="DomTime"
                dataSourceProperty="plannedStartTime" />
```

The DS also needs a plain `Attribute` for the combined column alongside the `DateTimeAttribute`:

```xml
<!-- For save mapping -->
<Attribute name="plannedVisitStartTime" table="Visit" column="PlannedVisitStartTime" />
<!-- For UI date/time split -->
<DateTimeAttribute dateName="plannedStartDate" timeName="plannedStartTime"
                   table="Visit" column="PlannedVisitStartTime" />
```

`Facade.saveObjectAsync` reads the DS `Attribute` mappings to build the INSERT/UPDATE statement. `DateTimeAttribute` entries alone are not included in the save mapping. Without the plain `Attribute`, the column is never written.

In `beforeSaveAsync`, recombine the split fields into the combined property:

```javascript
me.setPlannedStartDateTime(me.getPlannedStartDate() + ' ' + me.getPlannedStartTime() + ':00');
```

## Best Practices

| Do                                                                      | Don't                                            |
| ----------------------------------------------------------------------- | ------------------------------------------------ |
| Use `@namespace CUSTOM` for all new code                                | Modify `@namespace CORE` files                   |
| Compute display properties in `afterLoadAsync`                          | Put display logic in UI layer                    |
| Use ACL pattern for protected field modification                        | Skip ACL for sensitive fields                    |
| Validate in `afterDoValidateAsync` using `messageCollector`             | Throw errors from business logic                 |
| Use `storable="false"` for read-only joined fields                      | Mark everything storable                         |
| Set defaults in `afterCreateAsync`                                      | Leave properties uninitialized                   |
| Use `generateLoadMethod="true"` for simple BOs                          | Write custom loadAsync when hooks suffice        |
| Declare + implement `beforeSaveAsync` with `Facade.saveObjectAsync(me)` | Assume SAVE action auto-persists without BL      |
| Add combined DateTime BO properties + DS Attributes for persistence     | Rely on DateTimeAttribute alone for save mapping |
| Set `objectStatus = STATE.NEW \| STATE.DIRTY` in afterCreateAsync       | Omit objectStatus on new records                 |

## Validation rules

The modeler's validator runs four passes against every BusinessObject file (XSD, single-file, dependency-builder, cross-contract). The rules below are the ones authors most commonly trip over — what to do, what not to do, and what the build will silently rewrite for you.

### Cross-cutting (every contract, not just BO)

-   Contract names must be unique across the entire workspace — two files with the same root `@name` fail validation, regardless of contract type.
-   A deployment may declare at most 5 "platform objects" (BOs flagged for quick-action surfaces). Exceeding the cap is rejected.
-   Files must be readable, well-formed XML with a recognized root element — truly broken files fail fatally before any other rule runs.

### Must

-   Root element is `<BusinessObject>`.
-   File name starts with `Bo` and ends with `.businessobject.xml`. Custom BOs additionally start with the customizing prefix (e.g. `My…`), and the root `@name` starts with that same prefix.
-   If the BO is a platform object, exactly one `<SimpleProperty>` must carry `id="true"`.
-   A SimpleProperty named `recordTypeId` must use `type="DomId"`.
-   Storable BOs should reference a DataSource — omitting the DS is warned (not rejected), but it means the BO won't persist.
-   Each non-standard `<Method @name>` must be backed by a matching BusinessLogic (`.bl.js` or legacy `.BusinessLogic.xml`) file. A missing backing file is warned.
-   If a `<SimpleProperty @dataSourceProperty>` is declared, it must resolve to an Attribute in the referenced DataSource. The mismatch is an error (downgraded to warning for Salesforce external DataSources).
-   `<Events/Event @name>` on a ListObject child must be either `listItemChanged` or `listItemChangedBatch`, and the `@eventHandler` must name a declared Method.

### Must not

-   A SimpleProperty `@type` cannot start with `__` (reserved for framework-internal domains).
-   A SimpleProperty `@blobTable` cannot reference a temp table (table name ending in `_T`).
-   Two `<Field>` elements inside the same BO cannot share an `id`.

### Coerced (silently rewritten)

-   Embedded `<BusinessLogic>` XML blocks inside a BO are extracted into sibling `.bl.js` files by the migrator before validation runs. Authoring logic directly inline in the BO is a no-op — the extraction is automatic and silent.
-   Any `xmlns="*.xsd"` attribute on the root element is stripped during pre-processing.

Internal schema reference: `rcg-mobile-dev-agent/wiki/contracts/business-object.md`.

## Cross-References

-   [[datasource]] — BO references a DS for data loading; editableEntity controls write target
-   [[list-objects]] — LOs can be children of a BO via `<ListObject>` declaration
-   [[business-logic]] — BL files implement all BO methods and lifecycle hooks
-   [[processes]] — Process loads/saves/creates BOs via action types
-   [[user-interface]] — UI binds to BO properties via ProcessContext
