# Datasource Creator

> Use when creating Datex Studio datasources with dxs datasource/configuration commands: generating OData or flow datasource configs — both the cloud `-datasource.json` (typeId 6, called by functions/selectors) and the server-tier FootprintDatasource variant (`-footprintDatasource.json`, typeId 19, called by actions) — validating configs against a branch, and upserting standalone or owned datasources. Triggers: "create a datasource", "OData/flow datasource", "FPDS", "footprintDatasource", "a datasource an action can call", "standalone vs owned datasource".

- Skill: `datex/datasource-creator` (Agent Skill, multi-file: 6 files)
- Install (CLI): `npx skillmds@latest add datex/datasource-creator`
- Raw SKILL.md: https://api.skillmd.com/api/skills/datex/datasource-creator/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: DevOps & Infra
- Author: datex (https://skillmd.com/u/datex)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/datex/datasource-creator

---


# Datasource Creator

Create OData or flow-based datasource configurations for Datex Studio.

## References

- [../datex-studio-shared/branch-setup.md](../datex-studio-shared/branch-setup.md) -- Branch & connection selection (shared across skills)
- [references/parameter-strategies.md](references/parameter-strategies.md) -- Parameter strategies, linked datasources, quoting rules, cascading params
- [references/datasources.md](references/datasources.md) -- Datasource taxonomy: component variant (`-datasource.json` vs `-footprintDatasource.json`) × query type (OData vs flow), calling convention, tier restrictions, embedded datasources
- [references/odata-datasources.md](references/odata-datasources.md) -- OData query-type platform reference: queryOptions tree, filter expressions, expands, result type shape, pre-flight schema validation, canonical skeleton
- [references/flow-datasources.md](references/flow-datasources.md) -- Flow query-type platform reference: paginated vs single-result shapes, getFlow/getListFlow/getByKeysFlow slots, callsite syntax, entity-definition contract, canonical skeletons

## Dependencies

**REQUIRED BACKGROUND:** Read the `schema-explorer` skill for OData entity discovery
and the `odata-execution` skill for query building and verification.

**Schema exploration is mandatory for ODATA datasources** —
The type definition must be built from validated schema, not from examples or templates alone. Flow datasources might aggregate data from OData queries under the hood but if it works with already existing datasources, we assume they are already verified.

> **Footprint connections — consult `footprint-entity-expert` first.** If the datasource will query Footprint WMS entities (Tasks, Shipments, ArchivedShippingLicensePlateContents, LicensePlate, Lot, Material, etc.), check `footprint-entity-expert` before schema exploration. It documents which entity backs a given business concept, the correct navigation chain to Owner/Project, weight-calc branching, and pagination obligations — context that the OData metadata doesn't surface. Skip for non-Footprint connections (custom apps, non-WMS data).

### Prerequisites check

Before starting schema exploration or datasource generation, check whether a **requirements brief** already exists in the conversation context (produced by `requirements-gathering` or `report-creator`).

- **Requirements brief exists** → use it. The brief provides the field list, semantic roles, and business rules that drive which entities to explore and which fields to include. Verify datasource output against the brief.
- **No requirements brief exists** → invoke the `requirements-gathering` skill first. This happens when datasource-creator is invoked standalone (not from report-creator). The brief ensures you don't miss fields, map entities incorrectly, or skip calculated fields.

Do NOT skip this check. Building a datasource without understanding what fields are needed and what they mean leads to incomplete configs and rework.

## Input/Output Contract

| Direction | Item | Details |
|-----------|------|---------|
| Input | Connection ID | Required -- identifies the Footprint API connection |
| Input | Branch ID | Required -- target branch for validation and upsert |
| Input | Mode | `owned` (embed in the hosting grid / editor / form / report) or `standalone` (upsert to branch). **Defaults to `owned`** — see below |
| Input | Datasource type | `OData` (query-based) or `Flow` (custom JS code) |
| Output | JSON config file | Generated by `dxs datasource generate` or `generate-flow` |
| Output (owned) | File path + reference name | For a report, used with `--owned FILE:ALIAS` on `dxs report datasource add`; for a grid / editor / form, spliced into the host's `datasources[]` |
| Output (standalone) | Upserted datasource | Uploaded to branch, verified with `datasource-fields`, test data discovered |

**Mode defaults to `owned`.** If the caller didn't specify one, author the datasource owned and say so in the return summary. Switch to `standalone` only when an override applies — a second consumer genuinely needs the same query, the consumer type can't hold an owned datasource (selectors, lists, calendars, cards, widgets), a flow datasource must call it via `$datasources.<Package>.<name>`, or it ships in a library package. The rule and the full override list live in [references/datasources.md → Owned by Default](references/datasources.md#owned-by-default).

## Steps by Mode

| Step | Owned | Standalone |
|------|-------|-----------|
| `dxs datasource generate` / `generate-flow` | Yes | Yes |
| `dxs datasource context` (type defs) | Yes | Yes |
| `dxs datasource validate` (against branch) | Yes | Yes |
| `dxs configuration upsert datasource` (to branch) | No | Yes |
| `datasource-fields` (post-upsert verification) | No (deferred to after report upload) | Yes |
| Test data / `in_params` discovery | No (report-creator handles) | Yes |

For a grid, editor, or form the owned column continues into the splice procedure in [references/datasources.md → Creating an Owned Datasource](references/datasources.md#creating-an-owned-datasource); the host is validated in place of the standalone upsert.

## Workflow

```
[requirements brief in context?]
        |
  +-----+-----+
  |            |
 YES          NO
  |            |
 use it    invoke `requirements-gathering` skill
  |            |
  +-----+------+
        |
[determine type: OData or Flow]
        |
  +-----+-----+
  |            |
 OData       Flow
  |            |
 schema ->   schema -> (REQUIRED: validate entities/properties
 query ->     the flow code will query against the connection)
 generate    create standalone OData datasources (generate + dxs configuration upsert datasource each)
  |          write type-def YAML (from validated schema, NOT from examples)
  |          write flow TS code (referencing $datasources.<RepoName>.<ref_name>)
  |          generate-flow
  |            |
  +-----+------+
        |
  context (dxs datasource context - get type defs)
  validate (dxs datasource validate - both modes)
        |
  +-----+-----+
  |            |
Standalone   Owned
  |            |
 dxs configuration upsert datasource
              return JSON file path
 fields       + ref name
 test data
 return ref
```

### When to use OData vs Flow

| Use OData when | Use Flow when |
|----------------|---------------|
| All needed fields are scalar or reachable through single navigation properties (no collections in the path) | The result needs data from collection navigation properties flattened into scalar fields (e.g., a single-entity shipment where `OrderLookups` or `WarehousesContactsLookup` must appear as flat fields) |
| The result IS a collection that maps directly to a table/tablix (e.g., ShipmentLines) | Multiple OData queries need to be combined or joined into a single result |
| Simple parameter-based filtering is sufficient | Calculated fields require data from multiple entities or custom aggregation |
| Data lives in the Footprint OData schema | Rows come from a cloud `*-storage.json` component, read function-tier via `$db` (grid/selector over storage) — see [../db-query/references/flow-db-datasources.md](../db-query/references/flow-db-datasources.md) |

**How to tell:** Run `dxs report datasource-fields <ref> --branch <id>` (or `--report <ref>` for owned). If the output has a `collections:` section with fields you need in **standalone textboxes** (not tables), use a flow datasource. Collections in OData datasources cannot be bound as flat DataSet fields — they silently resolve to blank.

**The production pattern:** All existing Datex Studio reports with complex navigation (packing slips, BOLs, master BOLs) use flow datasources for their header/detail data. The flow code fetches the OData entities and flattens collections into scalar fields. OData datasources are used for simple list queries (line items, lookup tables).

> **Flow datasources and schema exploration:** Flow datasources are NOT a shortcut around schema exploration. A flow datasource's JavaScript code typically queries one or more OData entities and reshapes the data. Before writing the type definition or flow code, you MUST use `schema-explorer` to validate that the target connection has the expected entities and properties. Existing report examples and templates show what *has worked* on some connection — they are starting hypotheses, not validated designs for the current connection.

## Return to Caller

After completing the workflow (either standalone or owned), return this structured summary to the calling skill or user:

1. **Reference name** — the `-r` value (e.g., `ds_shipment_bol`)
2. **File path** — the `-o` output path (e.g., `reports/bol/ds_shipment_bol.json`)
3. **Mode** — `owned` (local file, to splice into the host's `datasources[]` or register with `--owned`) or `standalone` (upserted). State which, and if `standalone`, which override justified it
4. **Result type** — `single` or `collection` (from the generated config's `resultIsCollection`)
5. **in_params** — list of input parameter names and types (from the config's `inParams`), or empty if none
6. **Field summary** — read the generated JSON config and extract the field tree from `queryOptionsObjectTypeDef`. List fields as `name: type` with dot-notation for nested objects. Mark collections. This gives the caller a machine-derived field list without re-reading the field-mapping artifact.

Example return:
```
Datasource: ds_shipment_bol
File: reports/bol/ds_shipment_bol.json
Mode: owned
Result type: single
in_params: shipmentId (number)
Fields:
  Id: number (key)
  BillOfLading: string
  LookupCode: string
  Carrier.Name: string
  Carrier.ScacCode: string
  Status.Name: string
  ShipmentLines [collection]:
    LineNumber: number
    OrderLine.Material.LookupCode: string
    OrderLine.Material.Description: string
```

**Collection fields in the return require caller action.** When the field summary contains `[collection]` markers, the caller must choose:

1. **Flow datasource (preferred):** Rewrite the datasource as a flow that flattens collections into scalar fields. The caller gets a flat field list with no collections.
2. **Child datasets (alternative):** Create a separate DataSet in the report with `CommandText: "$.ds_name.result.CollectionPath.*"` and use `=First(Fields!Field.Value, "child_dataset")` in standalone textboxes. This works but adds complexity.
3. **Separate OData datasource:** Query the collection entity directly (e.g., `ShipmentOrderLookups?$filter=ShipmentId eq {id}`) as its own datasource. Only viable when the entity supports direct filtering.

**Never** pass collection-path fields (e.g., `OrderLookups.Order.OwnerReference`) to `dxs report dataset add --field` as flat fields on a single-result DataSet. They will silently render blank.

## OData Datasource Generation

> **Platform reference:** [references/odata-datasources.md](references/odata-datasources.md) covers the underlying Datex Studio file shape — `queryOptions` tree, filter expression syntax, expands, result type contract, single-object vs collection traps, and the canonical skeleton. Consult it when reasoning about why a generated config has the structure it does, when authoring or hand-editing JSON outside the generator, or when debugging import errors.

Generate an OData datasource config with `dxs datasource generate`:

```bash
dxs datasource generate \
  -c <connection_id> \
  -q '<odata_query>' \
  -r <reference_name> \
  -t "<reference_name>" \
  -d "<description>" \
  --api-setting-name <app_level_name> \
  -o ds_name.json \
  --branch <branch_id>
```

### Key Flags

| Flag | Purpose |
|------|---------|
| `-c` | Connection ID |
| `-q` / `-Q` | OData query string / query file (mutually exclusive) |
| `-r` | Reference name (valid JS identifier, `ds_` prefix convention) |
| `-t` | Display title (must match `-r` -- see Naming Convention) |
| `-d` | Description (always provide) |
| `--api-setting-name` | App-level setting `name` from `dxs source branch settings` (NOT the connection name like `DSV`). Optional -- omit and the CLI auto-resolves by matching your `-c` connection's name to the setting whose `apiConnectionName` equals it (works on host and ComponentModule branches). Pass explicitly only to disambiguate when multiple API-connection settings exist. |
| `--param-keys` | For single-entity queries with `Entity(0)` pattern |
| `--detect-params` | Detect filter parameters using `${$datasource.inParams.paramName}` syntax (stamped `required: false`, guarded) |
| `--dynamic-filter PROP:TYPE` | Optional UI filtering (generates conditional filter with `$utils.isDefined()` guard) |
| `--dynamic-orderby PROP` | Optional UI sorting |
| `--param-filter PROPERTY:OPERATOR:PARAM_NAME:TYPE` | Conditional filters with `$utils.isDefined()` guards. Operators: `eq`, `ne`, `gt`, `ge`, `lt`, `le`, `in`, `contains`, `startswith`, `endswith` |
| `--linked name:type:target` | Linked datasource. `oneToOne`/`oneToMany` = 3-part; `oneToOneWithMerge` = 4-part with `$entity.Field` |
| `--linked-param LINKED_NAME:PARAM_ID:EXPRESSION` | Map parent fields to linked datasource input parameters |
| `--custom-column` | Add computed columns |
| `--private` | Set access modifier to private (default: public) |
| `-o` | Output file path |
| `--branch` | Target branch ID |

### Parameter Strategy

| Need | Flag | Query syntax |
|------|------|-------------|
| Scope to one entity (detail/document) | `--param-keys` | `Entity(0)?$select=...` |
| Report-supplied filter params | `--detect-params` | Use `${$datasource.inParams.paramName}` in `$filter`. Stamped `required: false` and `$utils.isDefined()`-guarded as one unit -- see the note below |
| Optional UI list filtering | `--dynamic-filter PROP:TYPE` + `--dynamic-orderby PROP` | `Entity?$select=...` (no placeholders) |
| Conditional filters with guards | `--param-filter PROPERTY:OPERATOR:PARAM_NAME:TYPE` | Auto-generates `$utils.isDefined()` guard on its own appended predicate, leaving the base `-q` filter unconditional |

## Flow Datasource Generation

> **Platform reference:** [references/flow-datasources.md](references/flow-datasources.md) covers the underlying Datex Studio file shape — paginated vs single-result execution shapes, the `getFlow`/`getListFlow`/`getByKeysFlow` slots, callsite syntax via `referenceName`, the entity-definition output contract, and the canonical skeletons for both shapes (including the enum-dropdown pattern). Consult it when reasoning about which shape a use case needs, when authoring JSON outside the generator, or when fields are silently undefined at runtime.

### Flow Runtime Model (CRITICAL)

A flow datasource draws its data from one of **two** legitimate sources, never from a raw OData query string: (a) **standalone OData datasources** already on the branch, via `$datasources.<RepoName>.<ref_name>` (covered below); or (b) **cloud storage**, via the function-tier `$db.<Package>.<storage_referenceName>` predicate API — the pattern that backs a grid or selector over a `*-storage.json` component. The `$db` path has its own authoring rules (paging, dynamic filter/sort, `getQuery()` factory); see [../db-query/references/flow-db-datasources.md](../db-query/references/flow-db-datasources.md). The rest of this section covers source (a).

Flow datasource code does **NOT** execute raw OData queries. Instead, flows reference **standalone OData datasources** that already exist on the branch, using the `$datasources` object. Standalone datasources are scoped under their **repository module name** (the repo's `uniqueIdentifier` name, e.g., `PurchaseOrders`, `AsnOrders`), so the path is `$datasources.<RepoName>.<ref_name>`:

```typescript
// CORRECT: Reference standalone OData datasources with module scope, unwrap .result
const shipmentResp = await $datasources.PurchaseOrders.ds_shipment.get({ shipmentId: $flow.inParams.shipmentId });
const shipment = shipmentResp.result;
// For collection datasources, .result is an array:
const contactsResp = await $datasources.PurchaseOrders.ds_wh_contacts.get({ warehouseId: shipment.ActualWarehouse.Id });
const firstContact = contactsResp.result?.[0]?.Contact;
// Set output via $flow.outParams.result
$flow.outParams.result = { Name: shipment.Name, Phone: firstContact?.PrimaryTelephone };
```

```typescript
// WRONG: Missing module scope — datasources are not at the root level
const result = await $datasources.ds_shipment.get({ shipmentId: 123 });

// WRONG: Raw OData queries in flow code — this is NOT how flows work
const result = await $datasource.getList({ query: 'Shipments(123)?$expand=Carrier' });
```

**Flow code runtime variables:**

| Variable | Purpose |
|----------|---------|
| `$flow.inParams` | Access the flow datasource's input parameters |
| `$flow.outParams.result` | Set the flow's output (assign, don't return) |
| `$datasources.<RepoName>.<ref_name>` | Access standalone datasources on the branch (module-scoped by repository name) |

**Unwrapping responses:** All `$datasources` calls return `{ result?: ... }`. For `--param-keys` datasources, `result` is a single object. For collection datasources, `result` is an array. Always access `.result` before navigating into fields.

**The workflow for building a flow datasource:**

1. **Create standalone OData datasources** for each query the flow needs — use `dxs datasource generate` + `dxs configuration upsert datasource` for each
2. **Write the flow code** referencing those datasources via `$datasources.<RepoName>.<ref_name>.get()` or `$datasources.<RepoName>.<ref_name>.getList()` (where `<RepoName>` is the repository's `name` from `dxs source repo list`)
3. **Generate the flow config** with `dxs datasource generate-flow`, which embeds the flow code and type definition
4. The flow datasource itself is **owned** by default (embedded in its host), but its OData dependencies must be **standalone** on the branch — `$datasources.<RepoName>.<ref_name>` resolves only for standalone configs, so an embedded dependency is unreachable from flow code. This is one of the standing overrides in [references/datasources.md → Owned by Default](references/datasources.md#owned-by-default).

**`$datasources` API:**

| Method | Use when | Returns |
|--------|----------|---------|
| `$datasources.RepoName.ds_name.get({ paramName: value })` | The OData datasource uses `--param-keys` (single entity) | Single object |
| `$datasources.RepoName.ds_name.getList({ paramName: value })` | The OData datasource returns a collection | Array of objects |

Pass input parameters as an object — the keys must match the OData datasource's `inParams` exactly.

### Pagination for high-volume collection datasources

OData responses are capped at **5,000 records per request**. A flow that aggregates `Tasks`, `ArchivedShippingLicensePlateContents`, `Shipments`, or any other entity over a date range or multi-warehouse scope must paginate explicitly — there's no automatic continuation, and the truncation is silent.

To paginate, the standalone OData datasource the flow calls must declare a `skip` inParam, and its query must use `$top=5000&$skip=${$datasource.inParams.skip}`. Then the flow loops with `getList({ ..., skip })`, breaking when a short page comes back.

See [../datex-studio-shared/flow-code-patterns.md#odata-pagination--the-5000-record-cap](../datex-studio-shared/flow-code-patterns.md#odata-pagination--the-5000-record-cap) for the full pattern — datasource query shape, the `--detect-params` requirement, and the canonical fetch loop. If you're building a flow datasource whose underlying queries could exceed 5k rows, build the underlying OData datasources with pagination wired from the start; retrofitting later requires regenerating both the datasource and the flow code.

**`--param-keys` creates named params, NOT a `keys` array.** When an OData datasource uses `--param-keys` (e.g., `Shipments(0)`), the generator creates `inParams` named after the entity key (e.g., `shipmentId`). Always check the generated config's `inParams` to get the exact param name. **Never** use `{ keys: [value] }` — that pattern does not exist.

```typescript
// CORRECT: Use the actual inParam name from the generated config
const resp = await $datasources.ds_shipment.get({ shipmentId: $flow.inParams.shipmentId });

// WRONG: There is no "keys" parameter — this causes TS compilation errors
const resp = await $datasources.ds_shipment.get({ keys: [$flow.inParams.shipmentId] });
```

### Generation Command

Generate a flow datasource config with `dxs datasource generate-flow`:

```bash
dxs datasource generate-flow \
  -r ds_lookup -t "ds_lookup" -d "Custom lookup datasource" \
  --type-def types.yaml \
  --get-flow get.ts \
  --in-param id:number \
  -o ds_lookup.json --branch <branch_id>
```

**Required:** At least one flow method (`--get-flow`, `--get-list-flow`, or `--get-by-keys-flow`) and a type definition file (`--type-def`).

### Key Flags

| Flag | Purpose |
|------|---------|
| `--type-def FILE` | YAML/JSON file defining output type shape (required) |
| `--get-flow FILE` | JavaScript code for single-entity retrieval |
| `--get-list-flow FILE` | JavaScript code for collection retrieval |
| `--get-by-keys-flow FILE` | JavaScript code for key-based retrieval (requires `--key`) |
| `--on-init-flow FILE` | JavaScript code for initialization |
| `--in-param NAME:TYPE` | Input parameter (append `?` for optional, e.g., `search:string?`). Repeatable |
| `--key NAME:TYPE` | Key field definition. Repeatable |
| `--collection` | Force `resultIsCollection=true` |
| `--single` | Force `resultIsCollection=false` |

### Type Definition YAML Format

```yaml
- id: Id
  type: number
- id: Name
  type: string
- id: Items
  type: object
  isCollection: true
  objectTypeDef:
    - id: LineNumber
      type: number
```

Valid types: `string`, `number`, `boolean`, `date`, `object`, `union`, `blob`.

Enhancement flags (`--dynamic-filter`, `--linked`, `--custom-column`, etc.) work the same as OData datasources.

**File size limit:** All code files and the type definition file are limited to **512 KB**.

## Server-Tier Variant (FPDS — `-footprintDatasource.json`, typeId 19)

Everything above produces the **cloud** `-datasource.json` (`configurationTypeId: 6`), callable by **functions** and **selectors**. When the datasource must be called by an **action** (Footprint server tier), author the **FootprintDatasource (FPDS)** variant instead. Both OData and flow query types support it; the body shape is identical to the cloud variant — only the variant fields change.

The generator has no FPDS flag — generate the config exactly as above, then edit the produced JSON:

- **OData FPDS:** set `configurationTypeId: 19`. `apiSettingName` already names the branch's Footprint API connection (set by `generate`) — leave it. *(One-field change.)*
- **Flow FPDS:** set `configurationTypeId: 19` **and** set `apiSettingName` to the branch's Footprint API connection setting name (`generate-flow` leaves it `null`; get the name from `dxs source branch settings <branch_id>` — conventionally `FootprintApi`, but branches may differ, e.g. `fpapiconn`).

Everything else — `queryOptions` / flow slots, `outParams`, `keyDef`, and the explicit `null` slots — is identical to the cloud variant. See [references/datasources.md → Structural Deltas Between Variants](references/datasources.md#structural-deltas-between-variants).

Validate and push with the **`footprintdatasource`** CLI type (the `dxs datasource …` / `dxs configuration upsert datasource` path is hardwired to the cloud `datasource`/6 endpoint and cannot emit typeId 19):

```bash
dxs configuration validate footprintdatasource -b <branch_id> -D ds_name.json   # exit 1 = errors found
dxs configuration upsert  footprintdatasource -b <branch_id> -D ds_name.json
```

`upsert` resolves by `referenceName` (creates or updates). Delete with `dxs configuration delete footprintdatasource <id> -b <branch_id> -y`.

**Selectors must never be backed by an FPDS** — a selector backing must be the cloud `-datasource.json` variant (see [references/datasources.md](references/datasources.md)). FPDS targets are for action-tier callers.

## Context Command

See [../datex-studio-shared/context-navigation.md](../datex-studio-shared/context-navigation.md) for the full guide on retrieving and reading context responses, including backend vs frontend symbol filtering. To quickly confirm a `$types.<Package>.*` custom type or enum member exists without parsing the full context blob, use the `nomenclature` registry described there.

```bash
dxs -O json datasource context <file.json> --branch <branch_id>
```

Returns designer type definitions for writing expressions or TypeScript code. Works for both OData and flow datasources. Run after `generate` / `generate-flow` to understand the available fields before writing custom columns or flow code.

For datasources, the primary scope symbols are `$entity`, `$ccentity`, and `$datasource` (in `flowContext` or `linkedDatasourcesContext`). The `appContext` contains additional services — read `defaultContext.imports` to determine which ones are available (see the shared reference).

## Validation

```bash
dxs datasource validate <file.json> --branch <branch_id>
```

Validates the datasource config against the branch. **Run for BOTH standalone and owned modes** before proceeding to upsert or report upload. This command validates the datasource in isolation — it does **not** catch consumer-shape mismatches, so a datasource whose shape no consumer can use still passes here.

The branch runs **two** separate server-side gates. Both fire at publish and via `dxs configuration validate`; this command only partly stands in for them:

- **Usage gate** — grid/selector require `getList` + `getByKeys`; editor/form require `get` on a single result; each `linkedDatasources` entry must match its link `type` (see [`references/flow-datasources.md` → Linked datasource link types](references/flow-datasources.md#linked-datasource-link-types)). It only fires when the *consumer* is validated (`dxs configuration validate <consumer type>`), so it says nothing about a datasource you validate on its own.
- **Isolation gate** — checks the datasource's own shape, so a mis-shaped datasource with **no** consumer at all no longer publishes clean the way it used to. It reaches FootprintDatasource configs too. The flow-slot rules are the part this repo documents, and this command's local lint reproduces them offline — see [`references/flow-datasources.md` → Three Execution Shapes](references/flow-datasources.md#three-execution-shapes). The gate also checks OData query options and the outputs contract; those rules are not documented here yet.

**Two things this command cannot check on a filtered query**, both worth a minute before upsert:
whether the filter column is indexed, and whether the predicate that bounds the query can vanish at
runtime — every `generate` flag that parameterizes a filter wraps it in a `$utils.isDefined()` guard,
so an absent parameter fails *open* against the whole table. See
[references/odata-datasources.md → Also check: is the filter column indexed?](references/odata-datasources.md#also-check-is-the-filter-column-indexed)
and [→ The index check is undone by a guarded scoping filter](references/odata-datasources.md#the-index-check-is-undone-by-a-guarded-scoping-filter).

### Reading the report

The command runs a **local flow-shape lint** and merges its findings with the server's, so one run reports both. Every item carries four fields:

| Field | Values | Meaning |
|---|---|---|
| `origin` | `local` \| `server` | Which checker produced it. `local` is the CLI's own lint (reproducible offline); `server` came from the branch and can shift as the branch does. |
| `severity` | `error` \| `warning` | Errors block; warnings are advisory. |
| `source` | — | Which rule or subsystem flagged it. |
| `message` | — | The finding. |

**Exit codes:**

- **Any error, local or server → exit 1**, with `validation_errors[]` carrying errors *and* warnings merged into one list. That non-zero exit means *validation found errors* — read the payload, fix the config, re-validate. It does **not** mean the CLI broke, and it is not a reason to halt or retry the command unchanged.
- **Warnings alone → exit 0**, with `validation_result: {status: "valid", warnings: [...]}`. Still read them; they just don't block.

`dxs configuration validate datasource` and `dxs configuration validate footprintdatasource` run the same local lint and report the same merged shape — the two commands are deliberately kept in sync. Other config types report server findings only. Full exit-code matrix across the CLI (including `dxs function validate`, which still exits 0 on errors): [../datex-studio-shared/configuration-roundtrip.md](../datex-studio-shared/configuration-roundtrip.md#validate-exit-codes--a-non-zero-exit-is-a-finding-not-a-malfunction).

If the server call itself fails while local findings exist, the report keeps the local findings and adds a `validate API call failed (…)` item with `origin: server` — the local findings are still real and still worth fixing.

## Standalone Completion

After validation passes, complete the standalone workflow:

### 1. Upsert

```bash
dxs configuration upsert datasource -D <file.json> --branch <branch_id>
```

### 2. Verify Fields

```bash
dxs report datasource-fields <reference_name> --branch <branch_id>
```

**Check these in the output:**

1. **`in_params` names** -- must match exactly in `--datasource-param` (don't assume `id`)
2. **`result_type`** -- `single` vs `list` affects report layout
3. **`collections`** -- nav-property collections available for table sections
4. **Field paths** -- exact dot-notation paths for expressions

### 3. Discover Test Data

Query the entity **without** template literal params, using the base filter + `$top=5` + `$orderby` to find recent records:

```bash
dxs odata execute -c <id> \
  -q 'Entity?$top=5&$filter=<base_filters>&$select=Id,<param_fields>&$orderby=<date_field> desc'
```

Verify count with `$count=true&$top=1`:

```bash
dxs odata execute -c <id> \
  -q 'Entity?$count=true&$top=1&$filter=<full_filter_with_real_values>&$select=Id'
```

### 4. Deleting a Datasource

Delete by reference name or by config ID:

```bash
dxs datasource delete ds_my_report --branch <branch_id>
dxs datasource delete --id 42 --branch <branch_id>
```

Use `--id` when reference-name lookup returns 404 (can happen on branches with component modules). Get the ID from `dxs datasource list`.

## Naming Convention (CRITICAL)

The datasource **reference name** (`-r`), **display title** (`-t`), the RDLX-JSON **DataSet name**, and the `--owned` **alias** must ALL be identical:

```
-r ds_my_report -t "ds_my_report"              # generate: -t = -r
DataSet.Name = "ds_my_report"                  # RDLX-JSON
--owned ds_my_report.json:ds_my_report         # report datasource add: file:alias
```

**Rules:**
- Use `ds_` prefix convention
- Must be valid JS identifiers (start with letter/`_`/`$`, no spaces/hyphens, no leading digits)
- Examples: `ds_shipment_bol`, `ds_orders`, `ds_inventory_summary`

## Key Rules

- **Use `schema batch`** to combine multiple schema discovery calls into 2-3 requests instead of 9+ sequential calls
- **Check composite keys** -- some entities have multi-field keys that affect `--param-keys` behavior
- **Use `$top=1` during query testing** -- large queries timeout without limits
- **Single quotes for `$` values** -- shell expands `$` in double quotes; use single quotes for `-q`, `--linked`, `--custom-column`, `--datasource-param`, `--linked-param`
- **Always include `$select` in `$expand`** -- bare `$expand` without `$select` pulls all fields

## Common Mistakes

| Mistake | Fix |
|---------|-----|
| Using manager connection name as `--api-setting-name` | Use app-level `name` from `branch settings`, or omit the flag entirely and let the CLI auto-resolve from the branch's AppConfig |
| Looking for `apiConnectionId` in `branch settings` output | Don't -- the AppConfig setting record identifies the connection by *name*, in `apiConnectionName`. Match your `-c` connection to the setting whose `apiConnectionName` equals its name, then use that setting's `name` for `--api-setting-name` (or just omit the flag and auto-resolve). Record shape and the enum-serialization caveat: [../datex-studio-shared/branch-setup.md](../datex-studio-shared/branch-setup.md#select-connection) |
| Retrying blindly when `generate` fails to resolve the API setting | A `DXS-DS-021` error means your `-c` connection isn't wired to a Footprint API setting on the branch. Pass `--api-setting-name` explicitly (from `dxs source branch settings`), or wire the connection into the branch's AppConfig in Studio -- don't re-run the same command |
| Copying a datasource config JSON from another branch/app and upserting it as-is | `apiSettingName` is **app-scoped** -- the copied value likely names a setting the target app never defined. Upsert accepts it silently; Studio then flags "Missing API Connection setting `<name>`" (and on CACs, `dxs ng push` fails the `DXS-NG-047` preflight). Check `dxs source branch settings <target-branch>` first, or regenerate against the target branch with `dxs datasource generate` (it auto-resolves; `DXS-DS-021` if no connection is wired). If the app has no API connection at all, wire one in Studio -- the CLI never creates connections |
| `{Param}` instead of `${$datasource.inParams.Param}` in filter | `--detect-params` requires template literal syntax -- simple `{curly braces}` are silently ignored |
| Using only `--dynamic-filter` for report-supplied params | Dynamic filters are optional UI filters -- use `--detect-params` with template literals for params the report passes in |
| Assuming `--detect-params` makes a parameter required | It stamps `required: false` and guards the whole `$filter`; no generate flag emits a required unguarded filter. If the predicate bounds the query, patch `required: true` into the JSON or enforce the bound in the consumer -- see [references/odata-datasources.md](references/odata-datasources.md#the-index-check-is-undone-by-a-guarded-scoping-filter) |
| Not verifying `in_params` after upsert | Always run `datasource-fields` and confirm `in_params` is populated, not empty |
| Assuming inParam name is `id` | Check `datasource-fields` output -- might be `shipmentId`, `orderId`, etc. |
| Datasource `-t` title differs from `-r` reference name | Title and reference must be identical (e.g., `-r ds_foo -t "ds_foo"`) |
| Linked target doesn't exist | Create targets first, verify with `datasource-fields` |
| `mergeByValue` on `oneToOne` | Only `oneToOneWithMerge` gets 4th component |
| Hardcoded dates in filter | Use `${new Date(...).toISOString()}` for dynamic |
| Raw OData queries in flow code | Flow code must reference standalone datasources via `$datasources.RepoName.ds_name.get()` / `.getList()` — never embed OData query strings |
| `$datasources.ds_name` without module scope | Standalone datasources are scoped under the repository module — use `$datasources.RepoName.ds_name` (e.g., `$datasources.PurchaseOrders.ds_shipment`). Without the module prefix, the flow validator reports "Property does not exist on type 'IDatasourceService'" |
| Flow datasource without standalone OData dependencies on the branch | Create and upsert the OData datasources first, then write the flow that references them |

---

**After your edit, invoke `post-edit-verification` to surface description/JSON/schema violations. For a final review, invoke `component-validator`.**

