---
name: create-lookup-object
description: >
    Use when creating or modifying .lookupobject.xml files for CG Mobile modeler contracts.
    Trigger phrases: "create an LU", "lookup for X", "lookup object for X", "reference list for X",
    ".lookupobject.xml", "LU for X", "reference lookup for X", "validation lookup".
    Invokes verify-sqlite-schema first for every table/column the companion DsLu DS references.
    NOT for display lists (use create-list-object) or editable records (use create-business-object).
---

# Create LookupObject — CG Mobile Modeler

A LookupObject (LU) is a **read-only reference registry** — a cached, single-record-by-key contract for slowly-changing catalog data. Examples: `LuProduct` (resolve a product by pKey), `LuProductGroup` (resolve a product group), `LuCall` (resolve a visit/call record for a count or status check).

LUs share the shape of a BusinessObject (SimpleProperties, Methods, DataSource binding) but are structurally simpler: no `<NestedObjects>`, no `<ListObjects>`, no `<ObjectLookups>`, and only read-path lifecycle hooks. They are never saved or created by consumer code.

## When to Use This Skill

| Invoked by      | Phrase                                                                                 |
| --------------- | -------------------------------------------------------------------------------------- |
| User directly   | "create an LU", "lookup for X", "reference lookup for X", ".lookupobject.xml"          |
| Workflow skills | `create-business-object` (ObjectLookup wiring to an LU), `add-cockpit-card` (count LU) |

NOT for:

-   Lists of records for a screen — use `create-list-object`
-   Editable single records — use `create-business-object`
-   Build errors in an existing LU — use `build-and-simulate`

---

## Prerequisites

**Invoke `verify-sqlite-schema` for every table/column the companion `DsLu<Name>` DS references BEFORE writing the LU or DS file. Stop if any column is missing and cannot be remediated.**

```bash
# Quick manual check:
sqlite3 appl/data/app.db3 "PRAGMA table_info(Product2);"
sqlite3 appl/data/app.db3 ".tables"
```

Count-pattern DSs (with `DerivedAttribute` and no direct column reads) are low-risk but the entity table must still exist.

---

## Mandatory Checklist

-   [ ] Corresponding `DsLu<Name>` DS exists in `src/<Module>/DS/` — create via `create-datasource` first if missing
-   [ ] LU name starts with `Lu<Name>`; folder structure: `BO/Lu<Name>/Lu<Name>.lookupobject.xml`
-   [ ] Exactly one `<SimpleProperty>` has `id="true"` (the `pKey` field)
-   [ ] `generateLoadMethod="true"` on root element (omit only for fully custom loaders — rare)
-   [ ] `schemaVersion="1.1"` on root element (LUs stay on 1.1; DataSources use 2.0)
-   [ ] LU is read-only — `<Methods>` block contains ONLY `afterLoadAsync`, `beforeLoadAsync`, and/or `loadAsync`; no `saveAsync`, `createAsync`, `doValidateAsync`

---

## Failure Modes

### Failure mode 1: LU returns no data

**Symptom:** `Facade.getLookupAsync("LuProduct", pKey)` resolves with an empty or default object. No build error.
**Diagnosis:** The `<DataSource name="DsLu<Name>"/>` reference doesn't match the DS file's own `name` attribute, or the DS file doesn't exist in the build path.
**Fix:** Verify the DS file name matches exactly — `DsLuProduct.datasource.xml` or `DsLuProduct_sf.datasource.xml`. The value of `name="..."` inside the DS XML must match `<DataSource name="DsLuProduct"/>` in the LU.

```xml
<!-- LU XML -->
<LookupObject name="LuProduct" ...>
  <DataSource name="DsLuProduct" />   ← must match DS file's name attribute
```

---

### Failure mode 2: Consumer's `getLookupAsync` returns `undefined`

**Symptom:** `var lu = await Facade.getLookupAsync("LuProduct"); lu.getShortText()` throws `Cannot read properties of undefined`.
**Diagnosis:** The LU `name` attribute doesn't exactly match the string passed to `getLookupAsync`. The lookup is case-sensitive.
**Fix:** Confirm the root element `name="Lu<Name>"` matches the call site. Common mismatch: `"LuProduct"` vs `"luProduct"` or a typo in either location.

---

### Failure mode 3: "saveAsync is not a function" at runtime

**Symptom:** Consumer code calls `lu.saveAsync()` or `Facade.saveObjectAsync(lu)` and gets a runtime error.
**Diagnosis:** The consumer is treating an LU like an editable BO. LUs are read-only and have no save path.
**Fix:** Use a BO (create via `create-business-object`) for any record that needs editing and saving. If you need a count or validation check from an LU, call `Facade.getLookupAsync(...)` and read properties — do not attempt to save.

---

### Failure mode 4: Property reads return `undefined`

**Symptom:** `lu.getShortText()` returns `undefined`. No build error.
**Diagnosis:** The `<SimpleProperty dataSourceProperty="shortText"/>` value doesn't match any `<Attribute name="shortText"/>` in the DS.
**Fix:** Open the `DsLu<Name>` file, list all `<Attribute name="...">` and `<DerivedAttribute name="...">` values, and verify each LU `dataSourceProperty` matches exactly (case-sensitive).

```xml
<!-- WRONG — mismatch -->
<SimpleProperty name="shortText" type="DomText" dataSourceProperty="ShortText"/>

<!-- CORRECT -->
<SimpleProperty name="shortText" type="DomText" dataSourceProperty="shortText"/>
```

---

## Minimal Template (inline preview)

Full template at `templates/basic-lookup.lookupobject.xml.template`. First 15 lines:

```xml
<LookupObject name="Lu<NAME>" generateLoadMethod="true" schemaVersion="1.1">
  <DataSource name="DsLu<NAME>" />
  <SimpleProperties>
    <SimpleProperty id="true" name="pKey" type="DomPKey" dataSourceProperty="pKey" />
    <SimpleProperty name="shortText" type="DomText" dataSourceProperty="shortText" />
    <SimpleProperty name="text1" type="DomText" dataSourceProperty="text1" />
    <!-- Add more descriptor properties here -->
  </SimpleProperties>
  <Methods>
    <Method name="afterLoadAsync" />
    <Method name="beforeLoadAsync" />
  </Methods>
</LookupObject>
```

---

## Common Variants

### Variant 1 — Reference lookup (resolve a record by pKey)

Use when: BOs or BL code need to look up a product, group, or call record by its primary key.

Key settings:

-   `generateLoadMethod="true"`
-   DS: declarative (`external="false"`), `QueryCondition` filters by `#pKey#`
-   DS has a `<Parameter name="pKey" type="TEXT"/>`

Real-file anchors:

-   `src/Product/BO/LuProduct/LuProduct.lookupobject.xml` — 17 properties, `DomPrdCategory`, `DomPrdType`, computed `simplePricingBasePrice`
-   `src/Product/BO/LuProductGroup/LuProductGroup.lookupobject.xml` — 5 properties, joined `RecordType` filter in DS

DS anchor:

-   `src/Product/DS/DsLuProductGroup_sf.datasource.xml` — declarative with inner join on `RecordType.DeveloperName = 'Product_Group'`

---

### Variant 2 — Count lookup (aggregate value from a related table)

Use when: a cockpit card or BL method needs a count of related records (attachments on a folder, tactics in a sales folder).

Key settings:

-   DS: declarative (`external="false"`), uses `<DerivedAttribute name="..." value="Count(*)"/>` instead of column-mapped Attributes
-   No direct column read — only the aggregate
-   `generateLoadMethod="true"`

Real-file anchors:

-   `src/Sales Folder/DS/DsLuAttachmentcount_sf.datasource.xml` — `Count(*)` + inner joins + `#pKey#` parameter
-   `src/Sales Folder/DS/DsLuSalesFolderCount_sf.datasource.xml` — same pattern, different entities

---

### Variant 3 — Scripted / custom-load LU (loadAsync implemented in BL)

Use when: the LU data requires JS logic (piecing together multi-source data or performing calculations).

Key settings:

-   `generateLoadMethod="false"` on the LU root
-   `<Method name="loadAsync"/>` declared in `<Methods>` and fully implemented in `.bl.js`
-   DS may still exist (for the BL to query via `Facade.getListAsync`) or may be a minimal scripted DS

Real-file anchor:

-   `src/Product/BO/LuProductGroup/LuProductGroup.lookupobject.xml` — declares `loadAsync` alongside the two standard hooks
-   `src/Product/BO/LuPrdSales/LuPrdSales.lookupobject.xml` — declares `loadAsync`; custom pKey field (`productPKey`)

---

## Decision Table

| User wants...                                 | Make a...       | Tool                     |
| --------------------------------------------- | --------------- | ------------------------ |
| "Give me the Product record for pKey X"       | LU (reference)  | `create-lookup-object`   |
| "How many attachments does this folder have?" | LU (count)      | `create-lookup-object`   |
| "Is this category still valid for this org?"  | LU (validation) | `create-lookup-object`   |
| "Give me the list of open Visits for today"   | LO              | `create-list-object`     |
| "Edit this Visit's notes and save"            | BO              | `create-business-object` |
| "Show product details in a data form"         | BO              | `create-business-object` |

---

## File and Folder Layout

```
src/<Module>/
├── DS/
│   └── DsLu<Name>_sf.datasource.xml       ← companion DS (create first via create-datasource)
└── BO/
    └── Lu<Name>/
        ├── Lu<Name>.lookupobject.xml       ← this file
        └── Mv2/
            ├── Lu<Name>.AfterLoadAsync.bl.js   ← if afterLoadAsync is declared
            └── Lu<Name>.LoadAsync.bl.js        ← only for Variant 3 (custom loader)
```

All `.bl.js` files under `Mv2/` must use `@namespace CUSTOM` in their JSDoc block. See `_shared/namespace.md`.

---

## Real LU Structures

### LuProduct (reference lookup, 17 properties)

From `src/Product/BO/LuProduct/LuProduct.lookupobject.xml`:

```xml
<LookupObject name="LuProduct" generateLoadMethod="true" schemaVersion="1.1">
  <DataSource name="DsLuProduct" />
  <SimpleProperties>
    <SimpleProperty id="true" name="pKey" type="DomPKey" dataSourceProperty="pKey" />
    <SimpleProperty name="shortText" type="DomText" dataSourceProperty="shortText" />
    <SimpleProperty name="groupId" type="DomId" dataSourceProperty="groupId" />
    <SimpleProperty name="category" type="DomPrdCategory" dataSourceProperty="category" />
    <SimpleProperty name="prdType" type="DomPrdType" dataSourceProperty="productType" />
    <SimpleProperty name="piecesPerSmallestUnit" type="DomDecimal" storable="false" dataSourceProperty="piecesPerSmallestUnit" />
    <SimpleProperty name="simplePricingBasePrice" type="DomMoney" storable="false" />
    <!-- 10 more properties -->
  </SimpleProperties>
  <Methods>
    <Method name="afterLoadAsync" />
    <Method name="beforeLoadAsync" />
  </Methods>
</LookupObject>
```

Key observations:

-   Root: `<LookupObject name="..." generateLoadMethod="true" schemaVersion="1.1">` — NOT `<LU>` or `<lookupObject>`
-   `schemaVersion="1.1"` (LUs stay on 1.1; DataSources use 2.0)
-   `storable="false"` on computed properties (no `dataSourceProperty` needed on the last one)
-   No `<ObjectLookups>`, `<NestedObjects>`, `<ListObjects>` — LUs never have child objects
-   Only `afterLoadAsync` and `beforeLoadAsync` in `<Methods>` for a pure reference LU

### LuProductBasePrice (minimal, 4 properties)

From `src/Product/BO/LuProductBasePrice/LuProductBasePrice.lookupobject.xml`:

```xml
<LookupObject name="LuProductBasePrice" generateLoadMethod="false" schemaVersion="1.1">
  <DataSource name="DsLuProductBasePrice" />
  <SimpleProperties>
    <SimpleProperty name="pKey" type="DomPKey" dataSourceProperty="pKey" />
    <SimpleProperty name="basePrice" type="DomMoney" dataSourceProperty="basePrice" />
  </SimpleProperties>
  <Methods>
    <Method name="afterLoadAsync" />
    <Method name="beforeLoadAsync" />
  </Methods>
</LookupObject>
```

Note: `generateLoadMethod="false"` — this LU has custom load logic in BL. The `pKey` property lacks `id="true"` in this file (an acceptable quirk in some LUs; add it in new files for correctness).

---

## Escape Hatch

When the above isn't enough, consult in order:

1. `references/lu-patterns.md` — reference / count / validation patterns, when LU vs BO vs LO, `generateLoadMethod`, real-file breakdowns
2. `references/scripted-lu-ds.md` — `DsLu<Name>` DS structure, declarative vs scripted DS for LUs, count pattern with `DerivedAttribute`, real DS citations
3. `ai-wiki/wiki/lookup-objects.md` (if present) — validation rules, XSD quirks, blob handling, cross-references
4. Sibling skill: `create-datasource` — for building the companion `DsLu<Name>` first

Real-file anchors:

-   Reference LU: `src/Product/BO/LuProduct/LuProduct.lookupobject.xml`
-   Small LU: `src/Product/BO/LuProductGroup/LuProductGroup.lookupobject.xml`
-   Minimal LU: `src/Product/BO/LuProductBasePrice/LuProductBasePrice.lookupobject.xml`
-   Custom pKey LU: `src/Product/BO/LuPrdSales/LuPrdSales.lookupobject.xml`

---

## Verify

After writing the LU, its companion DS, and any BL files, run a build:

```bash
# cd to your workspace root
sf mdl build 2>&1 | tail -20
```

Then verify the LU resolves correctly in the simulator. From a consuming BO's `afterLoadAsync`:

```bash
sf mdl simulate
# In devtools console, trigger the consuming BO load
# Watch for: "undefined" property reads, "getLookupAsync returned undefined", SQLITE_ERROR
```

A clean build and non-undefined property values on the resolved LU instance confirms correct wiring.
