---
title: DataSource (DS) — Layer 1
aliases: [DS, datasource, data source, DsBo, DsLo, DsLu]
sources:
    [
        sources/sessions/2026-02-18-datasource-analysis.md,
        sources/sessions/2026-04-19-ds-macros-and-parameter-flow.md,
        sources/sessions/2026-04-21-mfgvisit-build-fixes.md,
    ]
last_updated: 2026-04-21
status: draft
---

# DataSource (DS) — Layer 1

DataSources map Salesforce object fields to application-level attributes and define the queries used to fetch data from the local SQLite database.

## Overview

DataSources are the data access layer that:

-   Map Salesforce object fields to business object attributes
-   Define query conditions and parameters for data retrieval
-   Specify which Salesforce backend system to use
-   Configure read/write permissions and behavior

## Two DS Patterns

### Pattern A: Declarative (QueryCondition + Parameters)

Used for simple lookups and standard filtering. DsBo files are almost always declarative.

```xml
<DataSource name="DsLoMyTask" backendSystem="sf" businessObjectClass="LoMyTask"
            external="false" editableEntity="Task" schemaVersion="2.0">
  <Attributes>...</Attributes>
  <Entities>...</Entities>
  <QueryCondition><![CDATA[
    Task.OwnerId = '#UserPKey#'
    AND Task.IsDeleted = '0'
    #cond#
  ]]></QueryCondition>
  <OrderCriteria>
    <OrderCriterion entity="Task" attribute="ActivityDate" direction="DESC" />
  </OrderCriteria>
  <Parameters>
    <Parameter name="cond" treatAs="sqlSnippet" />
    <Parameter name="cardDate" type="INTEGER" />
  </Parameters>
</DataSource>
```

### Pattern B: Scripted (Database > Load)

Used for complex queries with conditional logic, UNIONs, or dynamic SQL.

```xml
<Datasource name="DsLoMyVisit" backendSystem="sf" objectClass="LoMyVisit"
            businessObjectClass="LoMyVisit" readOnly="true" external="true"
            editableEntity="Visit" schemaVersion="2.0">
  <Attributes>...</Attributes>
  <Entities>...</Entities>
  <Database platform="SQLite">
    <Load><![CDATA[
      var usrMainPKey = ApplicationContext.get('user').getPKey();
      var sqlStmt = "SELECT Visit.Id AS pKey, ... ";
      sqlStmt += "FROM Visit ";
      sqlStmt += "WHERE Visit.Responsible__c = #usrMainPKey# ";
      sqlStmt += "AND Visit.Status = 'Planned' ";
      var sqlResult = Utils.replaceMacrosParam(sqlStmt, {usrMainPKey});
      return {sql: sqlResult.sql, params: sqlResult.params};
    ]]></Load>
  </Database>
</Datasource>
```

#### Build-critical: `<Load>` must RETURN the SQL, not execute it

The `<Load>` script is a **query builder**, not an executor. It must return one of:

-   A plain SQL string (with `#paramName#` macros), OR
-   A `{sql, params}` object (what `Utils.replaceMacrosParam(sqlStmt, sqlParams)` gives you), OR
-   The bare `Utils.replaceMacrosParam(sqlStmt, sqlParams)` call

The framework takes the returned SQL and executes it against the local SQLite database. There is **no `Database` global** in the DS runtime context. Calling `Database.loadRecordsAsync(...)` compiles cleanly (build passes) but crashes at runtime with:

```
00000351 action loadLoXxx failed
BoCustomer.loadLoXxxAsync Error: Database is not defined
```

Right (what every shipped scripted DS does):

```javascript
return Utils.replaceMacrosParam(sqlStmt, sqlParams);
```

Wrong (LLM hallucination that type-checks but crashes):

```javascript
return Database.loadRecordsAsync(sqlStmt, sqlParams); // Database is not defined
return runQuery(sqlStmt, sqlParams); // no such helper
return db.exec(sqlStmt); // no such global
```

See `DsLoContactPartner_sf.datasource.xml`, `DsLoVisit_sf.datasource.xml`, and every other `*.datasource.xml` in the repo — they all end with `return Utils.replaceMacrosParam(sqlStmt, sqlParams);` or the object-form equivalent.

### When to Use Which

| Criterion                 | Declarative   | Scripted |
| ------------------------- | ------------- | -------- |
| DsBo (single entity)      | Almost always | Rare     |
| Simple filter by key/date | Yes           | Overkill |
| Conditional WHERE clauses | No            | Yes      |
| UNION queries             | No            | Yes      |
| Dynamic joins             | No            | Yes      |
| DsLu (lookups)            | Sometimes     | Often    |

## Validating DS Against the Local SQLite Schema

**Before writing any DS**, verify that the tables and columns you reference actually exist in the simulator's local SQLite database at:

```
appl/data/app.db3
```

This database is the **source of truth** for what's available locally. Only tables and columns that exist here can be queried in DS files. If a column exists in Salesforce but not in the local DB, it means the sync configuration hasn't been set up to replicate that field — and the DS will fail at runtime (not at build time).

### How to Check

```bash
# List all tables
sqlite3 appl/data/app.db3 ".tables"

# Show columns for a specific table
sqlite3 appl/data/app.db3 "PRAGMA table_info(Visit);"
sqlite3 appl/data/app.db3 "PRAGMA table_info(Account);"

# Check if a specific column exists
sqlite3 appl/data/app.db3 "PRAGMA table_info(Account);" | grep -i billing
```

### Common Pitfalls

| What you expect           | What actually exists              | Why                                                                        |
| ------------------------- | --------------------------------- | -------------------------------------------------------------------------- |
| `Account.BillingCity`     | `Account.ShippingCity`            | Only shipping address columns are synced                                   |
| `Task.IsClosed`           | (doesn't exist)                   | Use `DerivedAttribute` with `CASE WHEN Task.Status = 'Completed'`          |
| `ContentNote` table       | Exists but may be empty           | Use empty DS (BL-populated) for simulator compatibility                    |
| `SF_File` / `SF_FileLink` | Exist — use these for attachments | Alternative to ContentVersion/ContentDocumentLink                          |
| `Visit.Status`            | (doesn't exist)                   | The Visit table uses status stored differently — check actual column names |

### Rule: If a Spec Requires an Unavailable Column

If the design spec references a Salesforce field that doesn't exist in the local SQLite schema:

1. The field is **not synced** to the local database
2. An outside action is needed: configure the sync profile to replicate that field
3. Until the sync is configured, the DS cannot use that column
4. Use `DerivedAttribute` with a CASE expression as a workaround for computed/derived values
5. Use empty DS (`external="true"` with `return undefined;`) if the entire table is missing

### Always Verify Before Writing DS Files

This step prevents the most common class of runtime errors — `SQLITE_ERROR: no such column` or `no such table`. These errors only appear at runtime in the simulator, never at build time.

## Naming Conventions

| Prefix  | Usage                           | Example                                       |
| ------- | ------------------------------- | --------------------------------------------- |
| `DsBo*` | Single entity (Business Object) | `DsBoAccount`, `DsBoVisit`                    |
| `DsLo*` | Collection (ListObject)         | `DsLoAccountReceivables`, `DsLoMyTask`        |
| `DsLu*` | Lookup / reference data         | `DsLuCallCountByDate`, `DsLuVisitCountByDate` |
| `*_sf`  | Salesforce backend suffix       | `DsBoMyVisit_sf`, `DsLoMyTask_sf`             |

## Attribute Patterns

**Standard Mapping:**

```xml
<Attribute name="pKey" table="Account" column="Id" />
```

**DateTime Split** (one SF field → two DS attributes):

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

**Derived Attribute** (hardcoded default):

```xml
<DerivedAttribute name="duration" value="'90'" />
```

**Derived Attribute with SQL expression:**

```xml
<DerivedAttribute name="classificationImage"
                  value="CASE WHEN Task.Type = ' ' THEN '' ELSE 'TaskClassification_' || Task.Type END" />
<DerivedAttribute name="priorityImage"
                  value="'TaskPriority_' || CASE WHEN Task.Priority = 'High' THEN 'A' WHEN Task.Priority = 'Normal' THEN 'B' ELSE 'C' END" />
<DerivedAttribute name="responsibleName"
                  value="Responsible.LastName || ', ' || Responsible.FirstName" />
```

**Derived Attribute referencing system macro:**

```xml
<DerivedAttribute name="salesOrg" value="'#SalesOrg#'" />
<DerivedAttribute name="initiatorName"
                  value="CASE WHEN (Task.CreatedById = Task.OwnerId AND Task.OwnerId = '#UserPKey#') THEN 'Personal' ELSE Initiator.LastName || ', ' || Initiator.FirstName END" />
```

## System-Provided Macros (Never Declared in Parameters)

These are injected by the framework at runtime — available in any QueryCondition or Load section without declaration:

| Macro                       | Purpose                         | Notes                                                   |
| --------------------------- | ------------------------------- | ------------------------------------------------------- |
| `#UserPKey#`                | Current user's primary key      | Most common; 82 uses in codebase                        |
| `#UserSfId#`                | Current user's Salesforce ID    | Used for permission queries                             |
| `#TodayAsDate#`             | Current date as date type       | 307 uses; for date comparisons                          |
| `#Today#`                   | Current date                    | Alternative to TodayAsDate                              |
| `#Language#` / `#LANGUAGE#` | User's language code            | For localized field access: `Description_#Language#__c` |
| `#SalesOrg#` / `#SALESORG#` | Current sales organization      | For org-scoped filtering                                |
| `#MinDate#`                 | System minimum date constant    | Used as "no date" sentinel                              |
| `#MaxDate#`                 | System maximum date constant    | Used for "infinite future"                              |
| `#SECONDS_PER_DAY#`         | Constant 86400                  | For date arithmetic                                     |
| `#BUSINESSMODIFIED#`        | Business modification timestamp | Rare                                                    |

**Key rule:** System macros are NEVER declared in `<Parameter>` tags. They are always available.

## User-Declared Parameters

Parameters passed from callers (Process, BL methods). MUST be declared in `<Parameters>`:

```xml
<Parameters>
  <Parameter name="pKey" type="TEXT" />
  <Parameter name="customerPKey" type="TEXT" />
  <Parameter name="cardDate" type="INTEGER" />
  <Parameter name="cond" treatAs="sqlSnippet" />
</Parameters>
```

### Parameter Types

| Type       | Description         | Example Usage                                   |
| ---------- | ------------------- | ----------------------------------------------- |
| `TEXT`     | String values       | IDs, names, status values                       |
| `INTEGER`  | Numeric/date values | Dates converted via `Utils.convertForDBParam()` |
| `DATE`     | Date values         | Explicit date parameters                        |
| `DATETIME` | DateTime values     | Timestamps                                      |
| `BOOLEAN`  | True/false          | Feature flags                                   |

### Special: `treatAs="sqlSnippet"`

Injects the parameter value as raw SQL rather than escaping as a literal:

```xml
<Parameter name="cond" treatAs="sqlSnippet" />
<Parameter name="criterionAttribute" treatAs="sqlSnippet" />
<Parameter name="additionalCondition" treatAs="sqlSnippet" />
```

Used for dynamic conditions injected from BL methods:

```javascript
jsonQuery.cond = " AND Task.Status IN ('In Progress', 'Not Started') AND Task.ActivityDate <= #cardDate# ";
```

## Helper Functions (Framework Query Functions)

Available in QueryCondition and Load sections:

| Function                                                        | Purpose                                      | Example                                                                                 |
| --------------------------------------------------------------- | -------------------------------------------- | --------------------------------------------------------------------------------------- |
| `#compareAsDate(field, fieldType, operator, value, valueType)#` | Date comparison with type conversion         | `#compareAsDate('Visit.PlannedVisitStartTime', 'DateTime','<=',#TodayAsDate#, 'Date')#` |
| `#dateAsUnixepochLocaltime(field)#`                             | Convert datetime to Unix epoch in local time | `#dateAsUnixepochLocaltime('Visit.PlannedVisitStartTime')#`                             |
| `#dateAsStringLocaltime(value)#`                                | Date to localized string                     | `#dateAsStringLocaltime('#MinDate#')#`                                                  |
| `#toggleMapping(table, field)#`                                 | Field mapping toggle                         | `#toggleMapping('Promotion_Template__c', 'Mobility_Color__c')#`                         |

## Entity Joins

```xml
<Entity name="User" alias="Responsible">
  <Join Type="inner">
    <SimpleJoin>
      <Condition leftSideValue="Task.OwnerId"
                 comparator="eq"
                 rightSideType="Attribute"
                 rightSideValue="Responsible.Id" />
    </SimpleJoin>
  </Join>
</Entity>
```

**Aliased entities** allow joining the same table multiple times:

```xml
<Entity name="User" alias="Responsible">...</Entity>
<Entity name="User" alias="Initiator">...</Entity>
<Entity name="Account" alias="What">...</Entity>
```

**Join Types:** `inner`, `left` (LEFT OUTER JOIN)

## ConditionalParameters (Optional Search/Filter)

For LOs that support user-driven search. Each condition is applied ONLY when the parameter is provided:

```xml
<ConditionalParameters>
  <ConditionalParameter name="text">
    <SimpleConditions>
      <Condition leftSideValue="Task.Subject"
                 comparator="#textComp#"
                 rightSideType="Attribute"
                 rightSideValue="'#text#'" />
    </SimpleConditions>
  </ConditionalParameter>
  <ConditionalParameter name="dueDate">
    <SimpleConditions>
      <Condition leftSideValue="Task.ActivityDate"
                 leftSideType="Date"
                 comparator="#dueDateComp#"
                 comparatorType="Date"
                 rightSideValue="#dueDate#"
                 rightSideType="Date" />
    </SimpleConditions>
  </ConditionalParameter>
</ConditionalParameters>
```

Pattern: Each conditional parameter has a companion `*Comp` parameter for the comparator operator.

## QuickSearchParameters

Enables text search across multiple fields:

```xml
<QuickSearchParameters>
  <QuickSearchParameter name="Task.Subject" />
  <QuickSearchParameter name="Task.ActivityDate" />
  <QuickSearchParameter name="responsibleName" />
</QuickSearchParameters>
```

## Parameter Flow: Process → BL → DS

### For Declarative DS (via Process LOAD action):

```
Process: <Action actionType="LOAD" type="LoMyTask">
           <Parameters>
             <Input name="cardDate" value="ProcessContext::CardDate" />
           </Parameters>
         </Action>
  → Framework creates: jsonQuery = {params: [{field: "cardDate", value: "..."}]}
  → LO.beforeLoadAsync(context) receives context.jsonQuery
  → Framework substitutes #cardDate# in QueryCondition
  → SQL executes against SQLite
```

### For Card Methods (via Process LOGIC action):

```
Process: <Action actionType="LOGIC" call="ProcessContext::CardTasks_TasksList.getTasksForCard">
           <Parameters>
             <Input name="numberOfListItems" value="ProcessContext::CardController.numberOfListItems" />
             <Input name="cardDate" value="ProcessContext::CardDate" />
           </Parameters>
         </Action>
  → BL method receives as function arguments
  → BL builds jsonQuery:
      jsonQuery.params = [{field: "cardDate", value: convertedValue}, ...]
      jsonQuery.cond = " AND Task.Status IN (...) AND Task.ActivityDate <= #cardDate#"
  → Facade.getListAsync("LoMyTask", jsonQuery)
  → DS receives jsonQuery, substitutes parameters in QueryCondition
```

### For Child ListObjects (automatic via BO declaration):

```
BO declares: <ListObject name="loTactics" objectClass="LoPrmTactics"
               dataSourceProperty="pKey" listProperty="tacticParentPKey"
               loadMode="LoadImmediate" />
  → When BO loads, framework automatically:
      1. Gets parent BO's pKey value
      2. Creates jsonQuery with {tacticParentPKey: pKeyValue}
      3. Calls LoPrmTactics.loadAsync()
      4. DS QueryCondition: PrmTactic.TacticParentPKey = #tacticParentPKey#
```

## Scripted DS: Key APIs

```javascript
// Access user parameters from jsonQuery
var customerPKey = Utils.convertForDBParam(jsonQuery.customerPKey, 'DomPKey');
var dateParam = Utils.convertForDBParam(jsonQuery.cardDate, 'DomDate');

// Access system context
var usrMainPKey = ApplicationContext.get('user').getPKey();

// Build parameter object for macro replacement
var sqlParams = { customerPKey, dateParam, usrMainPKey };

// Build SQL dynamically
var sqlStmt = 'SELECT ... FROM ... WHERE ... = #customerPKey# ';
if (someCondition) {
    sqlStmt += 'AND ... = #dateParam# ';
}

// ALWAYS use replaceMacrosParam for safe injection
return Utils.replaceMacrosParam(sqlStmt, sqlParams);
// OR return {sql: sqlResult.sql, params: sqlResult.params};
```

### Scripted DS return contract (build-green, runtime-fail trap)

> **The scripted `<Load>` script must `return Utils.replaceMacrosParam(sqlStmt, sqlParams)` — it returns the SQL + params for the framework to execute. It must NOT call the SQL itself.**
>
> `Database`, `Database.loadRecordsAsync`, and any other SQLite-executor globals do **not** exist in the DS script runtime. `sf mdl build` accepts the file because the script body isn't executed at build time — the error only shows up when a process action triggers the load, surfacing as:
>
> ```
> 00000351 action loadLo<Name> failed ...
> Bo<Parent>.loadLo<Name>Async Error: Database is not defined
> ```
>
> Seen 2026-04-28 during the Visits-tab feature. Fix: replace the last line of the `<Load>` block with `return Utils.replaceMacrosParam(sqlStmt, sqlParams);`.

## Patterns

| Pattern                         | Description                      | Example                                                  |
| ------------------------------- | -------------------------------- | -------------------------------------------------------- |
| **Single Entity Lookup**        | Load one record by pKey          | `WHERE Entity.Id = #pKey#`                               |
| **Parent-Child Filter**         | Load children for a parent       | `WHERE Task.WhatId = #parentPKey#`                       |
| **Date Range Filter**           | Records within date range        | `#compareAsDate(field, 'Date','>=',#fromDate#, 'Date')#` |
| **User-Scoped Filter**          | Filter by current user           | `WHERE Visit.VisitorId = '#UserPKey#'`                   |
| **Dynamic Condition Injection** | Card-style loading with `#cond#` | `treatAs="sqlSnippet"`                                   |
| **Org-Scoped Filter**           | Filter by sales organization     | `WHERE Entity.Sales_Org__c = '#SalesOrg#'`               |
| **Entity Join with Alias**      | Same table joined multiple times | `<Entity name="User" alias="Responsible">`               |
| **DateTime Split**              | One field to date + time         | `<DateTimeAttribute>`                                    |
| **Computed Icon**               | CASE expression for icon names   | `<DerivedAttribute value="CASE WHEN...">`                |
| **Language-Specific Field**     | Localized column access          | `Description_#Language#__c`                              |

## Build-Critical Syntax Rules

### The `external` Attribute — Declarative vs Scripted DS

The `external` attribute controls how the framework loads data. Getting this wrong causes a runtime error (`Cannot read properties of undefined (reading 'Load')`), not a build error — making it hard to diagnose.

| `external` | Meaning           | Load mechanism                                                               | Use when                                                                 |
| ---------- | ----------------- | ---------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
| `false`    | Framework-managed | Framework auto-generates SQL from `QueryCondition`, `Entities`, `Attributes` | Declarative DS with `<QueryCondition>`                                   |
| `true`     | Self-managed      | Framework calls the `<Database><Load>` script block                          | Scripted DS with `<Database><Load>` block, or empty DS for context menus |

```xml
<!-- CORRECT: declarative DS with QueryCondition → external="false" -->
<DataSource name="DsLoMyList" external="false" ...>
  <Attributes>...</Attributes>
  <Entities>...</Entities>
  <QueryCondition><![CDATA[ ... ]]></QueryCondition>
</DataSource>

<!-- CORRECT: scripted DS with Database/Load → external="true" -->
<DataSource name="DsLoMyScripted" external="true" ...>
  <Database><Load><![CDATA[ ... ]]></Load></Database>
</DataSource>

<!-- WRONG: declarative with external="true" → runtime crash -->
<DataSource name="DsLoMyList" external="true" ...>
  <QueryCondition><![CDATA[ ... ]]></QueryCondition>
  <!-- Framework looks for Database/Load block, finds nothing → error -->
</DataSource>
```

**Rule:** If your DS has `<QueryCondition>`, it MUST have `external="false"`. If your DS has `<Database><Load>`, it MUST have `external="true"`.

### OrderCriterion

The `entity` attribute is **required** and the sort attribute is `direction` (not `sortOrder`):

```xml
<!-- CORRECT -->
<OrderCriterion entity="Task" attribute="ActivityDate" direction="DESC" />

<!-- WRONG — will fail build -->
<OrderCriterion attribute="ActivityDate" sortOrder="DESC" />
```

| Attribute   | Required | Values                              |
| ----------- | -------- | ----------------------------------- |
| `entity`    | Yes      | Must match an Entity name in the DS |
| `attribute` | Yes      | Column/attribute to sort by         |
| `direction` | Yes      | `ASC` or `DESC`                     |

### Empty DS for Context Menus

Context menu DataSources must use scripted `Database` pattern with explicit `return undefined;` in all four CRUD methods:

```xml
<DataSource name="DsLoMyContextMenu" backendSystem="sf" businessObjectClass="LoMyContextMenu"
            external="true" editableEntity="" schemaVersion="2.0">
  <Database>
    <Load><![CDATA[
      // Intended to be empty - This list object gets its items by business logic!
      return undefined;
    ]]></Load>
    <Update><![CDATA[
      // Intended to be empty - This list object gets its items by business logic!
      return undefined;
    ]]></Update>
    <Insert><![CDATA[
      // Intended to be empty - This list object gets its items by business logic!
      return undefined;
    ]]></Insert>
    <Delete><![CDATA[
      // Intended to be empty - This list object gets its items by business logic!
      return undefined;
    ]]></Delete>
  </Database>
</DataSource>
```

Key: `external="true"` and empty `editableEntity=""`.

## Best Practices

| Do                                                   | Don't                                     |
| ---------------------------------------------------- | ----------------------------------------- |
| Load only needed fields in Attributes                | Load all fields with `*`                  |
| Filter at query level in QueryCondition              | Load everything and filter in JavaScript  |
| Use indexed fields in WHERE (Id, lookup fields)      | Filter on formula fields                  |
| Set `readOnly="true"` for display-only data          | Leave writable when no writes needed      |
| Use `Utils.convertForDBParam()` for parameter values | Pass raw strings to SQL                   |
| Use `Utils.replaceMacrosParam()` in scripted DS      | Concatenate values directly into SQL      |
| Check `Utils.isDefined()` before using parameters    | Assume parameters are always present      |
| Use `treatAs="sqlSnippet"` for dynamic conditions    | Hardcode all possible filter combinations |

## Validation rules

DataSource is the single heaviest single-file validator in the modeler — 800+ lines of rules. Many of them cover narrow edge cases (SF_File editableEntity mapping, DC-backend `backendSystem='dc'` shape, multichannel resolution). The rules below call out the ones authors encounter every day.

### Cross-cutting (every contract)

-   Contract names (the root `@name`) must be unique workspace-wide — rename a DS that collides.
-   Files must be readable and recognisably shaped before any other rule runs.

### Must

-   Root element is `<DataSource>` (strict). Lowercase `<Datasource>` is accepted but bypasses full validation — prefer the strict root.
-   File name starts with `Ds` and ends with `.datasource.xml`. Custom DSs use the customizing prefix in the file name, root `@name`, and `objectClass`.
-   `businessObjectClass` (or `objectClass`) must be present — unless `backendSystem='dc'`, which has its own rules.
-   If the DS uses `<QueryCondition>`, `external` must be `false`. If the DS uses `<Database><Load>`, `external` must be `true`. This is enforced at runtime, not build — setting it wrong produces a "Cannot read properties of undefined (reading 'Load')" crash.
-   For `SF_File` editable entities, `linkedEntityAttributeName`, `pKey`, and a `pathOnClient` mapping are all mandatory; `fileType` is derived from the mapping.
-   For external DSs, the Load method must not return an array. At `schemaVersion="2.0"` it must return an object; at versions below 2.0 it must return a string.
-   For modeled DSs, each `<Parameter @type>` must be one of `NULL`, `INTEGER`, `REAL`, `TEXT`, or `LIST`; every macro referenced in SQL must be declared.
-   For multichannel DSs, only one of `classic` / `multichannel` may omit `default`, and there may not be two duplicate configurations.
-   For `backendSystem='dc'` (Data Cloud): 16 extra rules apply — the contract must be `UNMODIFIED_CORE`, contain a single `<Entity>`, use no joins, carry no `<Database>`, and the file name must end with `_sf.datasource.xml`. Authoring DC datasources is strict.
-   Any `<Attribute @table>` ending in `_T` must reference an existing temp-table column — dangling temp-table references are errors.

### Must not

-   `SF_File` editableEntity must not reference `MobileId__c`, must not define a stray `VersionData` mapping.
-   External DSs must not declare multiple `<Database>` tags.
-   DS code blocks must not reference `Framework.settings.multichannel` — that runtime API isn't safe from this layer.

### Coerced (silently rewritten)

-   `xmlns="*.xsd"` on the root is stripped during pre-processing — a no-op in authoring.
-   A missing `@external` attribute is warned; best practice is to set it explicitly.

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

## Cross-References

-   [[architecture-overview]] — Where DS fits in the layer stack
-   [[business-objects]] — BO references DS via `<DataSource name="...">`
-   [[list-objects]] — LO references DS; child LO params via `listProperty`
-   [[business-logic]] — BL methods build jsonQuery and call Facade.getListAsync
-   [[processes]] — Process passes parameters via LOAD/LOGIC actions
-   [[cockpit-cards]] — Cards use the `#cond#` + Facade pattern extensively
