Tailoring Overlay
Extend an existing core-library Datex Studio component (most commonly a grid, but the model applies analogously to forms, editors, and hubs) via the baseConfiguration overlay mechanism, or flatten a tailored overlay into a standalone custom_<base> variant. A tailored component references a base via baseConfiguration, inherits its full contract, and layers targeted overrides on top — new columns, new flows, customization hooks against existing flows, additional secondary datasources, suppressed items. The overlay tracks the base as it evolves; flattening collapses the overlay into a fully authoritative copy when the customer's needs have diverged enough that the inheritance no longer pays for itself.
References
- ../datex-studio-shared/branch-setup.md — Branch/connection selection (shared across skills)
- references/tailoring.md — Authoritative tailoring reference: provenance variants, anatomy of a tailored component (
baseConfiguration, fromBaseConfiguration: true shadow marker, onCustomization<Slot>FlowConfig hook pairs, removed: true suppression), secondary enrichment datasources, the 12-step flatten-to-custom recipe, pre-flight checklist
- ../datex-studio-conventions/file-format.md —
configurationTypeId table and TypeScript-expression encoding rules (tailoring is not its own type — the type identifier is whatever the tailored component is, most commonly grid)
- ../datex-studio-conventions/naming-conventions.md —
tailored_ / custom_ prefix rules; tailored_<base>_grid naming
- ../datex-studio-runtime/runtime-globals.md — platform-injected globals available in tailored flow code (
$grid, $row, $flows, $apis, $utils, ...) — identical to the base's tier
- ../grid-creator/references/grids.md — grid authoring reference (the most common tailored component type) — five-location rule, imperative cell API, embedded datasource envelope
- ../datasource-creator/references/flow-datasources.md — flow-type datasource shape for tailored secondary (
tailored_ds_<base>) enrichment datasources
- ../datasource-creator/references/odata-datasources.md — schema pre-flight applies when flattening to a custom that newly authorizes OData fields
- ../component-wiring-check/references/component-wiring.md — vars-must-be-declared rule applies to tailored flow code and to flattened custom flows
Dependencies
requirements-gathering skill — invoked to produce a requirements brief if one doesn't already exist in the conversation context
grid-creator skill — invoked when the tailored component is a grid; the grid authoring rules (file shape, five-location rule, imperative cell API, embedded datasource envelope) apply identically to tailored grids
datasource-creator skill — invoked when a tailored overlay's secondary enrichment datasource is large enough to deserve its own standalone file, or when consolidating a secondary into the primary during flatten (the five-location rule still applies)
schema-explorer skill — invoked when the flatten step newly authorizes OData fields against the Footprint schema (OData schema pre-flight in step 10 of the flatten recipe)
component-wiring-check skill — invoked to audit reference contracts and the vars-must-be-declared rule on the tailored overlay (and again on the flattened custom)
grid-validator skill — mandatory invocation after every tailored-grid edit (and after flattening to a custom grid); re-runs the grid pre-flight checklist, catches five-location drift, secondary-datasource wiring misses, and the partial-lookupcode/id syncs that survive flatten
component-validator skill — invoked for non-grid tailored components (forms, editors, hubs) where grid-validator doesn't apply
CLI Lifecycle
Tailoring is not its own configuration type — a tailored overlay is itself an instance of whatever the base is (most commonly a grid). The CLI surface is the same as the base's: dxs configuration get / dxs configuration upsert <type> against the tailored variant's referenceName. The type identifier passed to dxs configuration is whatever matches ConfigurationEndpoints.normalize_type output for the base — grid (mapping to configurationTypeId: 3) in the typical case; analogously form, editor, or hub for the less common cases.
Create a new tailored overlay (grid example):
# 1. Fetch the base body to identify referenceName / moduleId / columns / flows / datasources / inParams / outParams (read-only — used as source material for shadow copies)
dxs configuration get grid <base_referenceName> -b <branchId> -O base-envelope.json
jq .json base-envelope.json > base.json
# 2. Build body.json for the tailored overlay from scratch (see references/tailoring.md → Pre-Flight Checklist — Authoring a Tailored Overlay)
# - referenceName = tailored_<base_name>_grid
# - baseConfiguration = { configId: <base_referenceName>, moduleId: <base_package>, isOwned: null }
# - every inherited element carries fromBaseConfiguration: true (shadow copies; must match base exactly)
# - new elements carry fromBaseConfiguration: null
# - onCustomization<Slot>FlowConfig + ...ExecutionBehaviorType paired
# 3. Validate — gates the push. Exit 1 = errors found (read validation_errors, fix, re-run), not a broken CLI
dxs configuration validate grid -b <branchId> -D body.json
# 4. Create
dxs configuration upsert grid -b <branchId> -D body.json
Edit an existing tailored overlay:
# 1. Fetch — note the envelope wrapper
dxs configuration get grid <tailored_referenceName> -b <branchId> -O envelope.json
# 2. EXTRACT THE INNER BODY (round-trip footgun guard — see "Round-trip rule" below)
jq .json envelope.json > body.json
# 3. Edit body.json (respect shadow-copy rules — do not hand-edit fromBaseConfiguration: true elements)
# 4. Validate — gates the push. Exit 1 = errors found (read validation_errors, fix, re-run), not a broken CLI
dxs configuration validate grid -b <branchId> -D body.json
# 5. Push
dxs configuration upsert grid -b <branchId> -D body.json
Flatten a tailored overlay into a standalone custom (grid example):
# 1. Fetch the tailored overlay body
dxs configuration get grid <tailored_referenceName> -b <branchId> -O envelope.json
jq .json envelope.json > body.json
# 2. Walk the 12-step recipe in references/tailoring.md → Conversion Recipe: Flattening a Tailored Component
# - rename to custom_<base>; drop baseConfiguration; flip every fromBaseConfiguration: true -> null;
# cut every removed: true entry; collapse onCustomization* hooks per behavior;
# consolidate datasources; resolve overlapping columns; etc.
# 3. Validate — gates the push. Exit 1 = errors found (read validation_errors, fix, re-run), not a broken CLI
dxs configuration validate grid -b <branchId> -D body.json
# 4. Create the custom (the flattened body is a brand-new component)
dxs configuration upsert grid -b <branchId> -D body.json
# 5. Decide whether to keep or remove the original tailored overlay (often the custom replaces it)
Round-trip rule (critical)
When editing an existing config, never pipe the envelope.json directly into dxs configuration upsert — it silently destroys configuration content. The corrected sequence above (extract inner .json with jq before editing) is mandatory for any round-trip. See ../datex-studio-shared/configuration-roundtrip.md for the canonical round-trip and the underlying bug.
Upserting a package-owned reference name is refused. On dxs ≥ 0.4.19, if the bare referenceName in your body resolves to a config owned by a referenced package and this branch has no owned copy, dxs configuration upsert refuses rather than silently forking it into your application's namespace. A tailored_<base> / custom_<base> overlay is a new reference name owned by your branch, so the normal tailoring path never trips this. You hit it when you edit the base itself by its own name — which tailoring exists to avoid. If that is genuinely what you want, --allow-external forks deliberately; say so and get the user's agreement first. Pre-flight any config you did not create on this branch with dxs source explore config <ref> -b <branch> (reports isExternal, applicationReferenceName, and an owner block). See ../datex-studio-shared/configuration-roundtrip.md.
Tailored bodies carry every slot the base carries — columns, datasourceConfig, embedded datasources[], flows / rowFlows, topToolbar / toolbar, filters, inParams / outParams / vars / rowVars — plus the three tailoring-specific moving parts (baseConfiguration, the fromBaseConfiguration: true shadow markers, the onCustomization<Slot>FlowConfig hook pairs, and removed: true suppression flags). Round-trip discipline (fetch → jq-extract → edit → validate → push) is non-negotiable, and shadow drift between the overlay and its base surfaces at import as "Outdated contract" errors — the single most common tailoring failure mode.
Workflow
[Phase 1: Setup + Requirements]
Follow branch-setup.md for branch/connection selection
|
[requirements brief in context?]
+-----+-----+
| |
YES NO -> invoke `requirements-gathering`
| |
+-----+------+
|
[Phase 2: Decide overlay vs flatten]
Consult references/tailoring.md → Provenance Variants and When to Flatten:
- core base + customer-specific tweaks tracking base evolution
-> author a tailored overlay (Phase 3)
- existing tailored overlay whose base evolution is no longer
relevant, or whose maintenance burden outweighs the benefit
-> flatten to standalone custom (Phase 5)
- net-new customer-specific component with no base relationship
-> stop; this is a from-scratch custom; invoke `grid-creator`
(or form-/editor-/hub-creator) directly
|
[Phase 3: Author tailored overlay]
Build body.json:
- referenceName = tailored_<base_name>_grid (analog for form/editor/hub)
- baseConfiguration = { configId, moduleId, isOwned: null } pointing at
the core base; moduleId is the BASE's package, not the overlay's
- Every inherited element copied with fromBaseConfiguration: true
(top-level params, columns, toolbar entries, flows, rowFlows, primary
datasource and its nested shape, dynamicFilters, configParameters);
shadow copies must match the base exactly — drift surfaces as
"Outdated contract" at import
- New elements (columns, toolbar buttons, flows) authored with
fromBaseConfiguration: null
- The three moving parts:
* fromBaseConfiguration: true vs null — shadow vs new
* onCustomization<Slot>FlowConfig + ...ExecutionBehaviorType
(before / after / replace) — hook pair; placeholder behavior on
null configs keeps the pair contractually shaped
* removed: true — suppress an inherited entry without deleting
the shadow
- Secondary (enrichment) datasource: tailored_ds_<base> embedded
alongside the inherited primary in datasources[]; invoke from
an `after` customization on on_data_loaded; batch-fetch by Id;
populate via Imperative Cell API; tailored-specific columns
carry empty declarative bindings (the one legitimate case)
- Vars / rowVars: every $grid.vars.<id> / $row.vars.<id> the
tailored flows write is declared at the overlay's top-level
vars[] / rowVars[]
- Invoke `component-wiring-check` to audit the tailored overlay's
reference contracts
|
[Phase 4: Validate + push]
dxs configuration validate <type> -b <branchId> -D body.json
|
+----+----+
| |
CREATE MODIFY-EXISTING
| |
| use the corrected round-trip
| (get -O envelope -> jq .json -> body)
| |
+----+----+
|
v
dxs configuration upsert <type> -b <branchId> -D body.json
(upsert creates or updates by referenceName — one command for both)
|
[Phase 5: (Optional) Flatten to standalone custom]
When the decision in Phase 2 was "flatten" (or you've reached the
point where the overlay no longer pays for itself), walk the
12-step recipe in references/tailoring.md:
1. Rename to custom_<base>; rename file; update embedded ids
2. Drop baseConfiguration -> null
3. Unshadow everything: flip every fromBaseConfiguration: true -> null
4. Cut every removed: true entry from JSON
5. Collapse onCustomization* hooks into the base slot per behavior;
null out onCustomization* fields; delete merged tailored_* flows
6. Consolidate datasources (preferred: merge secondary into primary;
touch queryOptions.selects AND all five type-metadata locations
AND dynamicFilters / dynamicOrderBys on BOTH sites) OR keep as
custom_ds_<base> if it queries a different entity set
7. Resolve overlapping columns (drop base's, rename tailored to
canonical id, update $row.cells.<col> references)
8. Update imperative populations — empty declarative bindings
replaced with $row.entity.<field> where the field is now
authoritative on the custom
9. Repackage — update $shell.<Package>.* / $types.<Package>.* /
$flows.<Package>.* to the custom's package
10. OData schema pre-flight via `schema-explorer` for newly-
authoritative OData field additions
11. Re-declare vars / rowVars at the custom's top level
12. Verify descriptions <= 100 chars
|
[Validate with `grid-validator` (mandatory for tailored grids and
flattened custom grids) — generic `component-validator` for non-grid
tailored components]
|
[invoke `post-edit-verification` for description/JSON/schema hygiene]
Phase Details
Phase 1: Setup + Requirements
- Follow ../datex-studio-shared/branch-setup.md for branch and connection selection. Never assume a branch ID — ask the user to confirm.
- Check whether a requirements brief already exists in the conversation context (produced by
requirements-gathering or another calling skill).
- Brief exists — use it. The brief should establish the base component being tailored (its
referenceName, package, what it does), the customer-specific tweaks needed (new columns / fields, new toolbar actions, base flows to hook with before / after / replace behavior, base elements to suppress), any fields not in the base datasource's select list that drive the need for a secondary enrichment datasource, and whether the goal is author an overlay or flatten an existing overlay into a custom.
- No brief — invoke the
requirements-gathering skill first. Getting the tailored vs custom decision right up front avoids re-authoring the file from scratch when the customer's actual needs diverge enough from the base that flattening is the better path.
Phase 2: Decide overlay vs flatten
Consult references/tailoring.md → Provenance Variants and Conversion Recipe: Flattening a Tailored Component before authoring. The decision drives whether you're writing an overlay or collapsing one.
Author a tailored overlay when:
- A core base exists with the bulk of the behavior you need.
- Customer-specific tweaks are limited in scope: a few new columns, a new toolbar action, a
before / after hook on a base flow, a suppressed inherited entry, an enrichment datasource for fields not in the base's select list.
- The base is actively evolving and the customer benefits from inheriting ongoing improvements automatically.
Flatten a tailored overlay into a standalone custom when:
- The customer's needs have diverged enough that little of the base's behavior survives the overrides.
- The base is deprecated, replaced, or otherwise no longer evolving in a direction the customer cares about.
- The maintenance burden of the overlay (tracking base drift, keeping shadows in sync, debugging "Outdated contract" errors) outweighs the benefit of automatic base updates.
Stop and use a different skill when:
- The component being authored is genuinely net-new with no base relationship — invoke
grid-creator (or form-creator / editor-creator / hub-creator) directly. The custom_ prefix is reserved for overlays that have been flattened or customer-specific components that intentionally signal "no upstream"; it's not a default naming convention for every from-scratch component.
- The change being made to the base is generic enough that it should land on the base itself — propose the change against the core package rather than overlaying.
Phase 3: Author tailored overlay
Build body.json from the steps in references/tailoring.md → Pre-Flight Checklist — Authoring a Tailored Overlay. Key points:
Read the base body first. Identify its referenceName, moduleId (package), columns, flows, datasources, inParams, outParams. The base body is read-only — never edit it as part of tailoring. Fetching via dxs configuration get <type> <base_referenceName> and extracting the inner .json gives you the source material for shadow copies.
Filename and referenceName. Filename matches the pattern tailored_<base_name>_<type>-<type>.json (e.g. tailored_contact_addresses_grid-grid.json for a grid). referenceName is the snake_case form ending in the type suffix (tailored_contact_addresses_grid). The package may be different from the base's — cross-package overlay is supported. See references/tailoring.md → Provenance Variants.
baseConfiguration points at the base. Top-level field, present and non-null. configId = base's referenceName. moduleId = base's package (the package the base lives in, not the overlay's package). isOwned: null for the cross-referenced base case. Setting moduleId to the overlay's package is one of the most common authoring mistakes — the platform resolves the base by { moduleId, configId } and a wrong package silently fails to find the base. See references/tailoring.md → baseConfiguration — pointer to the base.
Shadow copies carry fromBaseConfiguration: true. Every inherited element — top-level inParams[] / outParams[], columns[], topToolbar[] (and nested buttonConfig), flows[] / rowFlows[], datasources[], datasources[0].queryOptionsObjectTypeDef[], datasources[0].outParams[].objectTypeDef[], datasources[0].inParams[], datasources[0].dynamicFilters[], datasources[0].configParameters[].parameter — carries fromBaseConfiguration: true. Shadow content must match the base exactly. Drift at import surfaces as "Outdated contract" errors against the entity shape or against the base's contract. Don't hand-edit shadow content; re-pull from the base if the base has changed since you copied. See references/tailoring.md → fromBaseConfiguration: true — the shadow marker.
New elements carry fromBaseConfiguration: null. Tailored columns, new toolbar buttons, new flows / rowFlows authored fresh on the overlay take null here. Flipping true → null wholesale is the flatten step, not a normal authoring action.
Customization hook pairs. For each base flow slot you want to hook (onInitFlowConfig, onDataLoadedFlowConfig, onSelectionChangedFlowConfig, onInitNewRowFlowConfig, onSaveNewRowFlowConfig, onSaveExistingRowFlowConfig, onRowDataLoadedFlowConfig, onExcelImportFlowConfig, onExcelExportFlowConfig — analogous on forms/editors/hubs), populate the matching onCustomization<Slot>FlowConfig with { flowId, flowParameters } pointing at a flow in your overlay's flows[] / rowFlows[] (conventionally named tailored_<slot>), and set onCustomization<Slot>FlowConfigExecutionBehaviorType to before / after / replace. The ...ExecutionBehaviorType field is present even when the config is null — it carries a placeholder (typically "after") so the pair is always contractually shaped. Buttons carry their own pair: buttonConfig.clickFlowConfig (base-defined) and buttonConfig.onCustomizationClickFlowConfig. See references/tailoring.md → onCustomization<Slot>FlowConfig + ...ExecutionBehaviorType — the hook pair.
removed: true suppresses without deleting. Applied to an inherited (fromBaseConfiguration: true) entry to suppress it at runtime while preserving the shadow. Appears on toolbar entries (and nested buttonConfig), and is shaped identically across collections where supported. Don't delete the suppressed entry from the JSON — the platform needs the shadow to reconcile against the base. See references/tailoring.md → removed: true — suppression flag.
Secondary (enrichment) datasource — the standard pattern. When you need fields not in the base datasource's select list (UDFs, external-system fields, FK display resolutions), embed a tailored_ds_<base> datasource in the overlay's datasources[] array alongside the inherited primary (fromBaseConfiguration: true). Then in an after customization on on_data_loaded, batch-fetch by Id in ${ids} against the secondary and write enrichment into each row via the Imperative Cell API. Tailored-specific columns carry empty value strings in their declarative displayControl / editControl configs — they're populated imperatively. This is one of the legitimate cases for empty declarative bindings; don't "fix" them to $row.entity.<field>. See references/tailoring.md → Secondary (Enrichment) Datasources.
Vars / rowVars declared. Any $grid.vars.<id> / $row.vars.<id> your tailored flows write must be declared at the overlay's top-level vars[] / rowVars[] arrays. The base may declare some vars; new ones the overlay writes are authored fresh with fromBaseConfiguration: null. See ../component-wiring-check/references/component-wiring.md.
Invoke component-wiring-check. Audit the overlay's reference contracts and the vars-must-be-declared rule before push. If the tailored component is a grid, also confirm the inner grid → embedded-datasource contract on any new secondary datasource.
Phase 4: Validate + push
# Validate the body locally against the branch (type identifier matches the tailored component's base type — `grid` in the typical case).
# Exit 1 = validation found errors (read validation_errors, fix body.json, re-run) — not a broken CLI. Do not push on exit 1.
dxs configuration validate grid -b <branchId> -D body.json
# For a new tailored overlay
dxs configuration upsert grid -b <branchId> -D body.json
# For modify-existing (round-trip — never skip the jq extract)
dxs configuration get grid <tailored_referenceName> -b <branchId> -O envelope.json
jq .json envelope.json > body.json
# ... edit body.json ...
dxs configuration upsert grid -b <branchId> -D body.json
Validation surfaces missing required fields, malformed parameter-descriptor shapes, undefined flow-id references, and base-resolution errors before push. It does not catch shadow drift between the overlay and the base (which surfaces at import as "Outdated contract"), unpaired onCustomization<Slot>FlowConfig / ...ExecutionBehaviorType configs, secondary-datasource five-location drift, partial lookupcode/id syncs on tailored columns, or removed: true entries accidentally deleted from the JSON. Walk the references/tailoring.md → Pre-Flight Checklist before push and always invoke grid-validator (for tailored grids) or component-validator (for non-grid tailored components).
Phase 5: (Optional) Flatten to standalone custom
Flattening collapses the overlay into an authoritative copy. It is mechanical once every tailoring surface is recognized, but it touches the whole file — plan the 12 steps explicitly so nothing gets skipped. Walk the recipe top-to-bottom in references/tailoring.md → Conversion Recipe: Flattening a Tailored Component. Summary:
- Rename.
referenceName → custom_<base>; rename the file; update embedded ids (tailored_ds_<base> → custom_ds_<base>, tailored_on_<slot> → custom_on_<slot> or merge-into-existing).
- Drop
baseConfiguration. Set to null.
- Unshadow everything. Flip every
fromBaseConfiguration: true → null across the file. All elements become authoritative.
- Cut suppressed items. Drop every entry with
removed: true from the JSON (no base to suppress against).
- Collapse the customization hooks. For each populated
onCustomization<Slot>FlowConfig pair, merge the tailored flow into the base slot per its behavior (before prepend, after append, replace overwrite). Null out every onCustomization* field. Delete the freestanding tailored_* flow from flows[] / rowFlows[] once its body is merged (unless still called by another flow).
- Consolidate datasources. Preferred: merge the secondary's fields into the primary. Touch all of these — missing any causes silent failures:
datasources[0].queryOptions.selects (the OData $select clause — the runtime data shape; without this entry, the field is undefined on $row.entity even if every type-metadata location is correct; nested → expands[].queryOptions.selects); all five type-metadata locations per grids.md → Datasource Wiring — Five Places Must Stay in Sync; datasources[0].dynamicFilters / dynamicOrderBys and datasourceConfig.dynamicFilters / dynamicOrderBys if the merged field is filterable/sortable. Then delete the secondary entirely and strip the post-load enrichment calls. Alternative: keep the secondary as custom_ds_<base> when it queries a different entity set or can't be folded into a single query.
- Resolve overlapping columns. If tailoring added a parallel column (
tailored_country overriding the inherited country), drop the base's column and rename the tailored one to the canonical id. Update every $row.cells.<col> reference in flow code.
- Update imperative populations. Cells now source-bound: replace empty declarative
value with $row.entity.<field>; strip matching row.cells.<col>.displayControl.text = ... lines from on_data_loaded. Cells still needing imperative population (FK display resolution, computed text) stay.
- Repackage. Custom components often live in the tailoring's package, not the base's. Update cross-component references (
$shell.<Package>.*, $types.<Package>.*, $flows.<Package>.*) to match.
- OData schema pre-flight. Newly-authoritative OData field additions trigger schema pre-flight via the
schema-explorer skill — see odata-datasources.md → Pre-Flight. UDF fields queryable via OData but absent from the exported schema are acceptable; document them in the change notes.
- Re-declare vars / rowVars. Any
$grid.vars.<id> / $row.vars.<id> touched by flattened flows must be declared at the custom's top-level vars[] / rowVars[]. On the overlay they may have been implicitly inherited; on the standalone they must be explicit.
- Verify descriptions ≤ 100 chars.
Once the recipe is complete, the custom component has no baseConfiguration, no fromBaseConfiguration: true markers, no onCustomization* values, and no removed flags — it's indistinguishable in shape from a from-scratch custom grid (or form / editor / hub). Then push as a brand-new component (dxs configuration upsert <type> -b <branchId> -D body.json); the flattened body is a net-new create, not an update of the overlay's id.
Pre-Flight Checklist
Before push, walk the full checklist in references/tailoring.md → Pre-Flight Checklist — Authoring a Tailored Overlay (and, for flatten, Conversion Recipe top-to-bottom). The fast version:
Authoring a tailored overlay:
baseConfiguration is set with configId = base's referenceName, moduleId = base's package (not the overlay's), isOwned: null.
- Every inherited element carries
fromBaseConfiguration: true — top-level params, columns, toolbar entries, flows, rowFlows, primary datasource, nested entity shape, dynamic filters/orderbys, configParameters.
- New elements carry
fromBaseConfiguration: null.
- Customization hooks are paired correctly. Every hook with a non-null
onCustomization<Slot>FlowConfig has a matching populated ...ExecutionBehaviorType; the referenced flow exists in flows[] / rowFlows[].
- Shadows match the base exactly. If the base has drifted since you copied, re-pull shadows — drift at import surfaces as "Outdated contract".
- Vars / rowVars declared. Every
$grid.vars.<id> / $row.vars.<id> your tailored flows write has a top-level declaration on the overlay.
- Enrichment columns carry empty declarative bindings when populated imperatively from a secondary datasource. Don't "fix" them to
$row.entity.<field>.
removed: true preserves the shadow — don't delete suppressed entries from the JSON.
Flattening to custom:
baseConfiguration → null; referenceName → custom_<base>; file renamed.
- Every
fromBaseConfiguration: true → null.
- Every
removed: true entry dropped from JSON.
- Every
onCustomization<Slot>FlowConfig pair collapsed into the base slot per its behavior; onCustomization* fields all null.
- Secondary datasources merged (or retained as
custom_ds_<base> with references updated).
- For OData-merged fields:
queryOptions.selects updated (and expands[].queryOptions.selects for nested), plus all five type-metadata locations. This is the single most common silent-failure after flattening.
- Overlapping columns resolved; all
$row.cells.<col> references updated.
- Imperative populations now source-bound where appropriate; empty declarative bindings replaced with
$row.entity.<field>.
- Vars / rowVars re-declared at the custom's top level.
- Cross-component references (
$shell.<Package>.*, $types.<Package>.*, $flows.<Package>.*) updated to the custom's package.
- OData schema pre-flight completed; UDF absences documented.
- Descriptions ≤ 100 chars.
Common Mistakes
| Mistake |
Fix |
baseConfiguration.moduleId set to the overlay's package instead of the base's |
The platform resolves the base by { moduleId, configId }. Wrong package silently fails to find the base, the overlay can't reconcile shadows, and import explodes. moduleId is always the base's package; cross-package overlay is supported via this exact field. |
Hand-editing the content of a fromBaseConfiguration: true shadow |
The base owns the definition. Shadow drift surfaces at import as "Outdated contract" errors. If the base has changed, re-pull the shadow from the base; don't hand-edit. |
onCustomization<Slot>FlowConfig populated but ...ExecutionBehaviorType left at its default placeholder, or the reverse |
The pair is always contractually present, but only non-null configs fire. Populate both together: set the config to { flowId, flowParameters } AND set the behavior to before / after / replace based on intent. |
Tailored flow referenced from onCustomization<Slot>FlowConfig doesn't exist in the overlay's flows[] / rowFlows[] |
Validation catches some cases; runtime catches the rest. Author the tailored flow with fromBaseConfiguration: null in the appropriate flows[] / rowFlows[] array before populating the hook. |
Deleting an inherited entry from JSON instead of setting removed: true |
The shadow must remain for the platform to reconcile against the base. Delete → "Outdated contract" because the base still declares it. Suppress with removed: true to keep the shadow and hide the entry at runtime. |
New tailored column "fixed" to $row.entity.<field> when populated imperatively from a secondary datasource |
The empty declarative value is intentional — the cell is populated by the after customization on on_data_loaded. The "fix" overwrites the imperative population on every render. See grids.md → Empty Declarative Bindings Are Legitimate. |
| Secondary enrichment datasource omitted; tailored column shows empty values |
The standard pattern is datasources[] carries both the inherited primary (with fromBaseConfiguration: true) and the new tailored_ds_<base> (with fromBaseConfiguration: null). Without the secondary, there's nothing to fetch the extra fields from. |
$grid.vars.<id> = ... or $row.vars.<id> = ... in tailored flow code with no declaration on the overlay |
Import error: Property 'vars' does not exist on type 'IGrid' / IRow. Declare every var / rowVar the overlay's tailored flows write at the top-level vars[] / rowVars[]. Inheritance from the base does not extend to vars the overlay introduces. |
Flattening: missed queryOptions.selects when consolidating a secondary into the primary (OData backings) |
Silent failure — type metadata says the field exists, runtime data doesn't include it. $row.entity.<field> is undefined. Add the field to datasources[0].queryOptions.selects (or the appropriate expands[].queryOptions.selects for nested) in addition to the five type-metadata locations. This is the single most common flatten failure. |
Flattening: fromBaseConfiguration: true left in place on some elements |
The custom is supposed to be authoritative end-to-end. Any remaining fromBaseConfiguration: true means the platform still tries to resolve against a base — but baseConfiguration is null, so resolution fails. Flip every occurrence to null in one sweep. |
Flattening: forgot to delete the tailored_* flow whose body was merged into the base slot |
The flow is now dead code; references to it from elsewhere in the file may also be stale. Delete the freestanding entry from flows[] / rowFlows[] once its body has been merged. (Keep only if still being called as a helper.) |
| Flattening: vars / rowVars inherited from base not re-declared on the custom |
The overlay may have implicitly inherited some vars from the base. On a standalone custom, every var/rowVar must be declared explicitly. Walk every $grid.vars.<id> / $row.vars.<id> reference in the flattened flow code and verify each has a top-level declaration. |
Treated custom_<base> as a default naming convention for from-scratch components |
The custom_ prefix signals "the result of flattening a tailored overlay" or "intentionally signals no upstream relationship". For genuinely net-new components with no base history, the conventional name doesn't carry the custom_ prefix. Don't reach for custom_ as the default. |
Piping dxs configuration get -O envelope.json directly into dxs configuration upsert -D envelope.json |
Silently destroys config content. Always jq .json envelope.json > body.json before editing. See "Round-trip rule" above. |
Reaching for --allow-external to get past an upsert refusal |
The refusal means the reference name is owned by a referenced package and this branch has no owned copy — the flag forks it, and package updates stop flowing in. Tailor under a new tailored_<base> name, or fix it in its own package and re-pin. Fork only with the user's explicit agreement. |
description exceeds 100 chars on the overlay or the flattened custom |
SQL column limit — push will fail validation. Tighten. |
Tailored grid edit pushed without invoking grid-validator |
The generic component-validator doesn't catch five-location drift on the secondary datasource, OData selects misses, dynamic-filter registration mirror drift, or partial lookupcode/id syncs on tailored columns. Always invoke grid-validator for tailored grids — it's mandatory. |
After your edit, invoke post-edit-verification for description/JSON/schema hygiene. If you tailored a grid, also invoke grid-validator (mandatory — grids carry envelope/text-display gotchas). For non-grid tailored components, invoke component-validator.
1---2name: tailoring-overlay3description: Use when extending an existing core-library Datex Studio component (most commonly a grid) via the baseConfiguration overlay mechanism, or flattening a tailored overlay into a standalone custom_ variant. Owns the three provenance variants (core / tailored_ / custom_), the three moving parts of a tailored overlay, the flatten-to-custom 12-step recipe, and enrichment-datasource patterns alongside the tailored grid. Triggers: "tailor xxx grid", "add a customer-specific column to xxx", "override the base's on_save flow", "flatten this tailored grid into a custom one", "enrich a core grid with extra fields", "Outdated contract errors between a tailored overlay and its base".4---56# Tailoring Overlay78Extend an existing core-library Datex Studio component (most commonly a grid, but the model applies analogously to forms, editors, and hubs) via the `baseConfiguration` overlay mechanism, or flatten a tailored overlay into a standalone `custom_<base>` variant. A tailored component references a base via `baseConfiguration`, inherits its full contract, and layers targeted overrides on top — new columns, new flows, customization hooks against existing flows, additional secondary datasources, suppressed items. The overlay tracks the base as it evolves; flattening collapses the overlay into a fully authoritative copy when the customer's needs have diverged enough that the inheritance no longer pays for itself.910## References1112- [../datex-studio-shared/branch-setup.md](../datex-studio-shared/branch-setup.md) — Branch/connection selection (shared across skills)13- [references/tailoring.md](references/tailoring.md) — Authoritative tailoring reference: provenance variants, anatomy of a tailored component (`baseConfiguration`, `fromBaseConfiguration: true` shadow marker, `onCustomization<Slot>FlowConfig` hook pairs, `removed: true` suppression), secondary enrichment datasources, the 12-step flatten-to-custom recipe, pre-flight checklist14- [../datex-studio-conventions/file-format.md](../datex-studio-conventions/file-format.md) — `configurationTypeId` table and TypeScript-expression encoding rules (tailoring is not its own type — the type identifier is whatever the tailored component is, most commonly `grid`)15- [../datex-studio-conventions/naming-conventions.md](../datex-studio-conventions/naming-conventions.md) — `tailored_` / `custom_` prefix rules; `tailored_<base>_grid` naming16- [../datex-studio-runtime/runtime-globals.md](../datex-studio-runtime/runtime-globals.md) — platform-injected globals available in tailored flow code (`$grid`, `$row`, `$flows`, `$apis`, `$utils`, ...) — identical to the base's tier17- [../grid-creator/references/grids.md](../grid-creator/references/grids.md) — grid authoring reference (the most common tailored component type) — five-location rule, imperative cell API, embedded datasource envelope18- [../datasource-creator/references/flow-datasources.md](../datasource-creator/references/flow-datasources.md) — flow-type datasource shape for tailored secondary (`tailored_ds_<base>`) enrichment datasources19- [../datasource-creator/references/odata-datasources.md](../datasource-creator/references/odata-datasources.md) — schema pre-flight applies when flattening to a custom that newly authorizes OData fields20- [../component-wiring-check/references/component-wiring.md](../component-wiring-check/references/component-wiring.md) — vars-must-be-declared rule applies to tailored flow code and to flattened custom flows2122## Dependencies2324- **`requirements-gathering`** skill — invoked to produce a requirements brief if one doesn't already exist in the conversation context25- **`grid-creator`** skill — invoked when the tailored component is a grid; the grid authoring rules (file shape, five-location rule, imperative cell API, embedded datasource envelope) apply identically to tailored grids26- **`datasource-creator`** skill — invoked when a tailored overlay's secondary enrichment datasource is large enough to deserve its own standalone file, or when consolidating a secondary into the primary during flatten (the five-location rule still applies)27- **`schema-explorer`** skill — invoked when the flatten step newly authorizes OData fields against the Footprint schema (OData schema pre-flight in step 10 of the flatten recipe)28- **`component-wiring-check`** skill — invoked to audit reference contracts and the vars-must-be-declared rule on the tailored overlay (and again on the flattened custom)29- **`grid-validator`** skill — **mandatory** invocation after every tailored-grid edit (and after flattening to a custom grid); re-runs the grid pre-flight checklist, catches five-location drift, secondary-datasource wiring misses, and the partial-lookupcode/id syncs that survive flatten30- **`component-validator`** skill — invoked for non-grid tailored components (forms, editors, hubs) where `grid-validator` doesn't apply3132## CLI Lifecycle3334Tailoring is not its own configuration type — a tailored overlay is itself an instance of whatever the base is (most commonly a grid). The CLI surface is the same as the base's: `dxs configuration get` / `dxs configuration upsert <type>` against the tailored variant's referenceName. The type identifier passed to `dxs configuration` is whatever matches `ConfigurationEndpoints.normalize_type` output for the base — `grid` (mapping to `configurationTypeId: 3`) in the typical case; analogously `form`, `editor`, or `hub` for the less common cases.3536**Create a new tailored overlay (grid example):**3738```bash39# 1. Fetch the base body to identify referenceName / moduleId / columns / flows / datasources / inParams / outParams (read-only — used as source material for shadow copies)40dxs configuration get grid <base_referenceName> -b <branchId> -O base-envelope.json41jq .json base-envelope.json > base.json42# 2. Build body.json for the tailored overlay from scratch (see references/tailoring.md → Pre-Flight Checklist — Authoring a Tailored Overlay)43# - referenceName = tailored_<base_name>_grid44# - baseConfiguration = { configId: <base_referenceName>, moduleId: <base_package>, isOwned: null }45# - every inherited element carries fromBaseConfiguration: true (shadow copies; must match base exactly)46# - new elements carry fromBaseConfiguration: null47# - onCustomization<Slot>FlowConfig + ...ExecutionBehaviorType paired48# 3. Validate — gates the push. Exit 1 = errors found (read validation_errors, fix, re-run), not a broken CLI49dxs configuration validate grid -b <branchId> -D body.json50# 4. Create51dxs configuration upsert grid -b <branchId> -D body.json52```5354**Edit an existing tailored overlay:**5556```bash57# 1. Fetch — note the envelope wrapper58dxs configuration get grid <tailored_referenceName> -b <branchId> -O envelope.json59# 2. EXTRACT THE INNER BODY (round-trip footgun guard — see "Round-trip rule" below)60jq .json envelope.json > body.json61# 3. Edit body.json (respect shadow-copy rules — do not hand-edit fromBaseConfiguration: true elements)62# 4. Validate — gates the push. Exit 1 = errors found (read validation_errors, fix, re-run), not a broken CLI63dxs configuration validate grid -b <branchId> -D body.json64# 5. Push65dxs configuration upsert grid -b <branchId> -D body.json66```6768**Flatten a tailored overlay into a standalone custom (grid example):**6970```bash71# 1. Fetch the tailored overlay body72dxs configuration get grid <tailored_referenceName> -b <branchId> -O envelope.json73jq .json envelope.json > body.json74# 2. Walk the 12-step recipe in references/tailoring.md → Conversion Recipe: Flattening a Tailored Component75# - rename to custom_<base>; drop baseConfiguration; flip every fromBaseConfiguration: true -> null;76# cut every removed: true entry; collapse onCustomization* hooks per behavior;77# consolidate datasources; resolve overlapping columns; etc.78# 3. Validate — gates the push. Exit 1 = errors found (read validation_errors, fix, re-run), not a broken CLI79dxs configuration validate grid -b <branchId> -D body.json80# 4. Create the custom (the flattened body is a brand-new component)81dxs configuration upsert grid -b <branchId> -D body.json82# 5. Decide whether to keep or remove the original tailored overlay (often the custom replaces it)83```8485### Round-trip rule (critical)8687When editing an existing config, **never pipe the envelope.json directly into `dxs configuration upsert`** — it silently destroys configuration content. The corrected sequence above (extract inner `.json` with `jq` before editing) is mandatory for any round-trip. See [../datex-studio-shared/configuration-roundtrip.md](../datex-studio-shared/configuration-roundtrip.md) for the canonical round-trip and the underlying bug.8889**Upserting a package-owned reference name is refused.** On dxs ≥ 0.4.19, if the bare `referenceName` in your body resolves to a config owned by a **referenced package** and this branch has no owned copy, `dxs configuration upsert` refuses rather than silently forking it into your application's namespace. A `tailored_<base>` / `custom_<base>` overlay is a *new* reference name owned by your branch, so the normal tailoring path never trips this. You hit it when you edit the **base itself** by its own name — which tailoring exists to avoid. If that is genuinely what you want, `--allow-external` forks deliberately; say so and get the user's agreement first. Pre-flight any config you did not create on this branch with `dxs source explore config <ref> -b <branch>` (reports `isExternal`, `applicationReferenceName`, and an `owner` block). See [../datex-studio-shared/configuration-roundtrip.md](../datex-studio-shared/configuration-roundtrip.md#upserting-a-package-owned-reference-name-is-refused---allow-external).9091Tailored bodies carry every slot the base carries — `columns`, `datasourceConfig`, embedded `datasources[]`, `flows` / `rowFlows`, `topToolbar` / `toolbar`, `filters`, `inParams` / `outParams` / `vars` / `rowVars` — plus the three tailoring-specific moving parts (`baseConfiguration`, the `fromBaseConfiguration: true` shadow markers, the `onCustomization<Slot>FlowConfig` hook pairs, and `removed: true` suppression flags). Round-trip discipline (fetch → jq-extract → edit → validate → push) is non-negotiable, and shadow drift between the overlay and its base surfaces at import as "Outdated contract" errors — the single most common tailoring failure mode.9293## Workflow9495```96[Phase 1: Setup + Requirements]97Follow branch-setup.md for branch/connection selection98 |99[requirements brief in context?]100 +-----+-----+101 | |102 YES NO -> invoke `requirements-gathering`103 | |104 +-----+------+105 |106[Phase 2: Decide overlay vs flatten]107Consult references/tailoring.md → Provenance Variants and When to Flatten:108 - core base + customer-specific tweaks tracking base evolution109 -> author a tailored overlay (Phase 3)110 - existing tailored overlay whose base evolution is no longer111 relevant, or whose maintenance burden outweighs the benefit112 -> flatten to standalone custom (Phase 5)113 - net-new customer-specific component with no base relationship114 -> stop; this is a from-scratch custom; invoke `grid-creator`115 (or form-/editor-/hub-creator) directly116 |117[Phase 3: Author tailored overlay]118Build body.json:119 - referenceName = tailored_<base_name>_grid (analog for form/editor/hub)120 - baseConfiguration = { configId, moduleId, isOwned: null } pointing at121 the core base; moduleId is the BASE's package, not the overlay's122 - Every inherited element copied with fromBaseConfiguration: true123 (top-level params, columns, toolbar entries, flows, rowFlows, primary124 datasource and its nested shape, dynamicFilters, configParameters);125 shadow copies must match the base exactly — drift surfaces as126 "Outdated contract" at import127 - New elements (columns, toolbar buttons, flows) authored with128 fromBaseConfiguration: null129 - The three moving parts:130 * fromBaseConfiguration: true vs null — shadow vs new131 * onCustomization<Slot>FlowConfig + ...ExecutionBehaviorType132 (before / after / replace) — hook pair; placeholder behavior on133 null configs keeps the pair contractually shaped134 * removed: true — suppress an inherited entry without deleting135 the shadow136 - Secondary (enrichment) datasource: tailored_ds_<base> embedded137 alongside the inherited primary in datasources[]; invoke from138 an `after` customization on on_data_loaded; batch-fetch by Id;139 populate via Imperative Cell API; tailored-specific columns140 carry empty declarative bindings (the one legitimate case)141 - Vars / rowVars: every $grid.vars.<id> / $row.vars.<id> the142 tailored flows write is declared at the overlay's top-level143 vars[] / rowVars[]144 - Invoke `component-wiring-check` to audit the tailored overlay's145 reference contracts146 |147[Phase 4: Validate + push]148dxs configuration validate <type> -b <branchId> -D body.json149 |150 +----+----+151 | |152 CREATE MODIFY-EXISTING153 | |154 | use the corrected round-trip155 | (get -O envelope -> jq .json -> body)156 | |157 +----+----+158 |159 v160dxs configuration upsert <type> -b <branchId> -D body.json161 (upsert creates or updates by referenceName — one command for both)162 |163[Phase 5: (Optional) Flatten to standalone custom]164When the decision in Phase 2 was "flatten" (or you've reached the165point where the overlay no longer pays for itself), walk the16612-step recipe in references/tailoring.md:167 1. Rename to custom_<base>; rename file; update embedded ids168 2. Drop baseConfiguration -> null169 3. Unshadow everything: flip every fromBaseConfiguration: true -> null170 4. Cut every removed: true entry from JSON171 5. Collapse onCustomization* hooks into the base slot per behavior;172 null out onCustomization* fields; delete merged tailored_* flows173 6. Consolidate datasources (preferred: merge secondary into primary;174 touch queryOptions.selects AND all five type-metadata locations175 AND dynamicFilters / dynamicOrderBys on BOTH sites) OR keep as176 custom_ds_<base> if it queries a different entity set177 7. Resolve overlapping columns (drop base's, rename tailored to178 canonical id, update $row.cells.<col> references)179 8. Update imperative populations — empty declarative bindings180 replaced with $row.entity.<field> where the field is now181 authoritative on the custom182 9. Repackage — update $shell.<Package>.* / $types.<Package>.* /183 $flows.<Package>.* to the custom's package184 10. OData schema pre-flight via `schema-explorer` for newly-185 authoritative OData field additions186 11. Re-declare vars / rowVars at the custom's top level187 12. Verify descriptions <= 100 chars188 |189[Validate with `grid-validator` (mandatory for tailored grids and190flattened custom grids) — generic `component-validator` for non-grid191tailored components]192 |193[invoke `post-edit-verification` for description/JSON/schema hygiene]194```195196## Phase Details197198### Phase 1: Setup + Requirements1992001. Follow [../datex-studio-shared/branch-setup.md](../datex-studio-shared/branch-setup.md) for branch and connection selection. **Never assume a branch ID** — ask the user to confirm.2012. Check whether a **requirements brief** already exists in the conversation context (produced by `requirements-gathering` or another calling skill).202 - **Brief exists** — use it. The brief should establish the **base component** being tailored (its `referenceName`, package, what it does), the **customer-specific tweaks** needed (new columns / fields, new toolbar actions, base flows to hook with `before` / `after` / `replace` behavior, base elements to suppress), any **fields not in the base datasource's select list** that drive the need for a secondary enrichment datasource, and whether the goal is **author an overlay** or **flatten an existing overlay into a custom**.203 - **No brief** — invoke the `requirements-gathering` skill first. Getting the tailored vs custom decision right up front avoids re-authoring the file from scratch when the customer's actual needs diverge enough from the base that flattening is the better path.204205### Phase 2: Decide overlay vs flatten206207Consult [references/tailoring.md → Provenance Variants](references/tailoring.md#provenance-variants) and [Conversion Recipe: Flattening a Tailored Component](references/tailoring.md#conversion-recipe-flattening-a-tailored-component) before authoring. The decision drives whether you're writing an overlay or collapsing one.208209**Author a tailored overlay** when:210211- A core base exists with the bulk of the behavior you need.212- Customer-specific tweaks are limited in scope: a few new columns, a new toolbar action, a `before` / `after` hook on a base flow, a suppressed inherited entry, an enrichment datasource for fields not in the base's select list.213- The base is actively evolving and the customer benefits from inheriting ongoing improvements automatically.214215**Flatten a tailored overlay into a standalone custom** when:216217- The customer's needs have diverged enough that little of the base's behavior survives the overrides.218- The base is deprecated, replaced, or otherwise no longer evolving in a direction the customer cares about.219- The maintenance burden of the overlay (tracking base drift, keeping shadows in sync, debugging "Outdated contract" errors) outweighs the benefit of automatic base updates.220221**Stop and use a different skill** when:222223- The component being authored is genuinely net-new with no base relationship — invoke `grid-creator` (or `form-creator` / `editor-creator` / `hub-creator`) directly. The `custom_` prefix is reserved for overlays that have been flattened *or* customer-specific components that intentionally signal "no upstream"; it's not a default naming convention for every from-scratch component.224- The change being made to the base is generic enough that it should land on the base itself — propose the change against the core package rather than overlaying.225226### Phase 3: Author tailored overlay227228Build `body.json` from the steps in [references/tailoring.md → Pre-Flight Checklist — Authoring a Tailored Overlay](references/tailoring.md#pre-flight-checklist--authoring-a-tailored-overlay). Key points:2292301. **Read the base body first.** Identify its `referenceName`, `moduleId` (package), columns, flows, datasources, inParams, outParams. The base body is read-only — never edit it as part of tailoring. Fetching via `dxs configuration get <type> <base_referenceName>` and extracting the inner `.json` gives you the source material for shadow copies.2312322. **Filename and `referenceName`.** Filename matches the pattern `tailored_<base_name>_<type>-<type>.json` (e.g. `tailored_contact_addresses_grid-grid.json` for a grid). `referenceName` is the snake_case form ending in the type suffix (`tailored_contact_addresses_grid`). The package may be different from the base's — cross-package overlay is supported. See [references/tailoring.md → Provenance Variants](references/tailoring.md#provenance-variants).2332343. **`baseConfiguration` points at the base.** Top-level field, present and non-null. `configId` = base's `referenceName`. `moduleId` = base's package (the package the base lives in, **not** the overlay's package). `isOwned: null` for the cross-referenced base case. Setting `moduleId` to the overlay's package is one of the most common authoring mistakes — the platform resolves the base by `{ moduleId, configId }` and a wrong package silently fails to find the base. See [references/tailoring.md → `baseConfiguration` — pointer to the base](references/tailoring.md#baseconfiguration--pointer-to-the-base).2352364. **Shadow copies carry `fromBaseConfiguration: true`.** Every inherited element — top-level `inParams[]` / `outParams[]`, `columns[]`, `topToolbar[]` (and nested `buttonConfig`), `flows[]` / `rowFlows[]`, `datasources[]`, `datasources[0].queryOptionsObjectTypeDef[]`, `datasources[0].outParams[].objectTypeDef[]`, `datasources[0].inParams[]`, `datasources[0].dynamicFilters[]`, `datasources[0].configParameters[].parameter` — carries `fromBaseConfiguration: true`. **Shadow content must match the base exactly.** Drift at import surfaces as "Outdated contract" errors against the entity shape or against the base's contract. Don't hand-edit shadow content; re-pull from the base if the base has changed since you copied. See [references/tailoring.md → `fromBaseConfiguration: true` — the shadow marker](references/tailoring.md#frombaseconfiguration-true--the-shadow-marker).2372385. **New elements carry `fromBaseConfiguration: null`.** Tailored columns, new toolbar buttons, new flows / rowFlows authored fresh on the overlay take `null` here. Flipping `true` → `null` wholesale is the flatten step, not a normal authoring action.2392406. **Customization hook pairs.** For each base flow slot you want to hook (`onInitFlowConfig`, `onDataLoadedFlowConfig`, `onSelectionChangedFlowConfig`, `onInitNewRowFlowConfig`, `onSaveNewRowFlowConfig`, `onSaveExistingRowFlowConfig`, `onRowDataLoadedFlowConfig`, `onExcelImportFlowConfig`, `onExcelExportFlowConfig` — analogous on forms/editors/hubs), populate the matching `onCustomization<Slot>FlowConfig` with `{ flowId, flowParameters }` pointing at a flow in your overlay's `flows[]` / `rowFlows[]` (conventionally named `tailored_<slot>`), and set `onCustomization<Slot>FlowConfigExecutionBehaviorType` to `before` / `after` / `replace`. The `...ExecutionBehaviorType` field is present even when the config is null — it carries a placeholder (typically `"after"`) so the pair is always contractually shaped. Buttons carry their own pair: `buttonConfig.clickFlowConfig` (base-defined) and `buttonConfig.onCustomizationClickFlowConfig`. See [references/tailoring.md → `onCustomization<Slot>FlowConfig` + `...ExecutionBehaviorType` — the hook pair](references/tailoring.md#oncustomizationslotflowconfig--executionbehaviortype--the-hook-pair).2412427. **`removed: true` suppresses without deleting.** Applied to an inherited (`fromBaseConfiguration: true`) entry to suppress it at runtime while preserving the shadow. Appears on toolbar entries (and nested `buttonConfig`), and is shaped identically across collections where supported. **Don't delete the suppressed entry from the JSON** — the platform needs the shadow to reconcile against the base. See [references/tailoring.md → `removed: true` — suppression flag](references/tailoring.md#removed-true--suppression-flag).2432448. **Secondary (enrichment) datasource — the standard pattern.** When you need fields not in the base datasource's `select` list (UDFs, external-system fields, FK display resolutions), embed a `tailored_ds_<base>` datasource in the overlay's `datasources[]` array alongside the inherited primary (`fromBaseConfiguration: true`). Then in an `after` customization on `on_data_loaded`, batch-fetch by `Id in ${ids}` against the secondary and write enrichment into each row via the [Imperative Cell API](../grid-creator/references/grids.md#imperative-cell-api). Tailored-specific columns carry **empty `value` strings** in their declarative `displayControl` / `editControl` configs — they're populated imperatively. This is one of the legitimate cases for empty declarative bindings; don't "fix" them to `$row.entity.<field>`. See [references/tailoring.md → Secondary (Enrichment) Datasources](references/tailoring.md#secondary-enrichment-datasources).2452469. **Vars / rowVars declared.** Any `$grid.vars.<id>` / `$row.vars.<id>` your tailored flows write must be declared at the overlay's top-level `vars[]` / `rowVars[]` arrays. The base may declare some vars; new ones the overlay writes are authored fresh with `fromBaseConfiguration: null`. See [../component-wiring-check/references/component-wiring.md](../component-wiring-check/references/component-wiring.md).24724810. **Invoke `component-wiring-check`.** Audit the overlay's reference contracts and the vars-must-be-declared rule before push. If the tailored component is a grid, also confirm the inner grid → embedded-datasource contract on any new secondary datasource.249250### Phase 4: Validate + push251252```bash253# Validate the body locally against the branch (type identifier matches the tailored component's base type — `grid` in the typical case).254# Exit 1 = validation found errors (read validation_errors, fix body.json, re-run) — not a broken CLI. Do not push on exit 1.255dxs configuration validate grid -b <branchId> -D body.json256257# For a new tailored overlay258dxs configuration upsert grid -b <branchId> -D body.json259260# For modify-existing (round-trip — never skip the jq extract)261dxs configuration get grid <tailored_referenceName> -b <branchId> -O envelope.json262jq .json envelope.json > body.json263# ... edit body.json ...264dxs configuration upsert grid -b <branchId> -D body.json265```266267Validation surfaces missing required fields, malformed parameter-descriptor shapes, undefined flow-id references, and base-resolution errors before push. It does **not** catch shadow drift between the overlay and the base (which surfaces at import as "Outdated contract"), unpaired `onCustomization<Slot>FlowConfig` / `...ExecutionBehaviorType` configs, secondary-datasource five-location drift, partial lookupcode/id syncs on tailored columns, or `removed: true` entries accidentally deleted from the JSON. Walk the [references/tailoring.md → Pre-Flight Checklist](references/tailoring.md#pre-flight-checklist--authoring-a-tailored-overlay) before push and always invoke `grid-validator` (for tailored grids) or `component-validator` (for non-grid tailored components).268269### Phase 5: (Optional) Flatten to standalone custom270271Flattening collapses the overlay into an authoritative copy. It is mechanical once every tailoring surface is recognized, but it touches the whole file — plan the 12 steps explicitly so nothing gets skipped. Walk the recipe top-to-bottom in [references/tailoring.md → Conversion Recipe: Flattening a Tailored Component](references/tailoring.md#conversion-recipe-flattening-a-tailored-component). Summary:2722731. **Rename.** `referenceName` → `custom_<base>`; rename the file; update embedded ids (`tailored_ds_<base>` → `custom_ds_<base>`, `tailored_on_<slot>` → `custom_on_<slot>` or merge-into-existing).2742. **Drop `baseConfiguration`.** Set to `null`.2753. **Unshadow everything.** Flip every `fromBaseConfiguration: true` → `null` across the file. All elements become authoritative.2764. **Cut suppressed items.** Drop every entry with `removed: true` from the JSON (no base to suppress against).2775. **Collapse the customization hooks.** For each populated `onCustomization<Slot>FlowConfig` pair, merge the tailored flow into the base slot per its behavior (`before` prepend, `after` append, `replace` overwrite). Null out every `onCustomization*` field. Delete the freestanding `tailored_*` flow from `flows[]` / `rowFlows[]` once its body is merged (unless still called by another flow).2786. **Consolidate datasources.** Preferred: merge the secondary's fields into the primary. Touch **all** of these — missing any causes silent failures: `datasources[0].queryOptions.selects` (the OData `$select` clause — the runtime data shape; without this entry, the field is `undefined` on `$row.entity` even if every type-metadata location is correct; nested → `expands[].queryOptions.selects`); all five type-metadata locations per [grids.md → Datasource Wiring — Five Places Must Stay in Sync](../grid-creator/references/grids.md#datasource-wiring--five-places-must-stay-in-sync); `datasources[0].dynamicFilters` / `dynamicOrderBys` and `datasourceConfig.dynamicFilters` / `dynamicOrderBys` if the merged field is filterable/sortable. Then delete the secondary entirely and strip the post-load enrichment calls. Alternative: keep the secondary as `custom_ds_<base>` when it queries a different entity set or can't be folded into a single query.2797. **Resolve overlapping columns.** If tailoring added a parallel column (`tailored_country` overriding the inherited `country`), drop the base's column and rename the tailored one to the canonical id. Update every `$row.cells.<col>` reference in flow code.2808. **Update imperative populations.** Cells now source-bound: replace empty declarative `value` with `$row.entity.<field>`; strip matching `row.cells.<col>.displayControl.text = ...` lines from `on_data_loaded`. Cells still needing imperative population (FK display resolution, computed text) stay.2819. **Repackage.** Custom components often live in the tailoring's package, not the base's. Update cross-component references (`$shell.<Package>.*`, `$types.<Package>.*`, `$flows.<Package>.*`) to match.28210. **OData schema pre-flight.** Newly-authoritative OData field additions trigger schema pre-flight via the `schema-explorer` skill — see [`odata-datasources.md` → Pre-Flight](../datasource-creator/references/odata-datasources.md#pre-flight-validate-against-schema). UDF fields queryable via OData but absent from the exported schema are acceptable; document them in the change notes.28311. **Re-declare vars / rowVars.** Any `$grid.vars.<id>` / `$row.vars.<id>` touched by flattened flows must be declared at the custom's top-level `vars[]` / `rowVars[]`. On the overlay they may have been implicitly inherited; on the standalone they must be explicit.28412. **Verify descriptions ≤ 100 chars.**285286Once the recipe is complete, the custom component has no `baseConfiguration`, no `fromBaseConfiguration: true` markers, no `onCustomization*` values, and no `removed` flags — it's indistinguishable in shape from a from-scratch custom grid (or form / editor / hub). Then push as a brand-new component (`dxs configuration upsert <type> -b <branchId> -D body.json`); the flattened body is a net-new create, not an update of the overlay's id.287288## Pre-Flight Checklist289290Before push, walk the full checklist in [references/tailoring.md → Pre-Flight Checklist — Authoring a Tailored Overlay](references/tailoring.md#pre-flight-checklist--authoring-a-tailored-overlay) (and, for flatten, [Conversion Recipe](references/tailoring.md#conversion-recipe-flattening-a-tailored-component) top-to-bottom). The fast version:291292**Authoring a tailored overlay:**2932941. **`baseConfiguration` is set** with `configId` = base's `referenceName`, `moduleId` = **base's** package (not the overlay's), `isOwned: null`.2952. **Every inherited element carries `fromBaseConfiguration: true`** — top-level params, columns, toolbar entries, flows, rowFlows, primary datasource, nested entity shape, dynamic filters/orderbys, configParameters.2963. **New elements carry `fromBaseConfiguration: null`**.2974. **Customization hooks are paired correctly.** Every hook with a non-null `onCustomization<Slot>FlowConfig` has a matching populated `...ExecutionBehaviorType`; the referenced flow exists in `flows[]` / `rowFlows[]`.2985. **Shadows match the base exactly.** If the base has drifted since you copied, re-pull shadows — drift at import surfaces as "Outdated contract".2996. **Vars / rowVars declared.** Every `$grid.vars.<id>` / `$row.vars.<id>` your tailored flows write has a top-level declaration on the overlay.3007. **Enrichment columns carry empty declarative bindings** when populated imperatively from a secondary datasource. Don't "fix" them to `$row.entity.<field>`.3018. **`removed: true` preserves the shadow** — don't delete suppressed entries from the JSON.302303**Flattening to custom:**3043051. `baseConfiguration` → `null`; `referenceName` → `custom_<base>`; file renamed.3062. Every `fromBaseConfiguration: true` → `null`.3073. Every `removed: true` entry dropped from JSON.3084. Every `onCustomization<Slot>FlowConfig` pair collapsed into the base slot per its behavior; `onCustomization*` fields all null.3095. Secondary datasources merged (or retained as `custom_ds_<base>` with references updated).3106. For OData-merged fields: `queryOptions.selects` updated (and `expands[].queryOptions.selects` for nested), plus all five type-metadata locations. This is the single most common silent-failure after flattening.3117. Overlapping columns resolved; all `$row.cells.<col>` references updated.3128. Imperative populations now source-bound where appropriate; empty declarative bindings replaced with `$row.entity.<field>`.3139. Vars / rowVars re-declared at the custom's top level.31410. Cross-component references (`$shell.<Package>.*`, `$types.<Package>.*`, `$flows.<Package>.*`) updated to the custom's package.31511. OData schema pre-flight completed; UDF absences documented.31612. Descriptions ≤ 100 chars.317318## Common Mistakes319320| Mistake | Fix |321|---|---|322| `baseConfiguration.moduleId` set to the overlay's package instead of the base's | The platform resolves the base by `{ moduleId, configId }`. Wrong package silently fails to find the base, the overlay can't reconcile shadows, and import explodes. `moduleId` is always the **base's** package; cross-package overlay is supported via this exact field. |323| Hand-editing the content of a `fromBaseConfiguration: true` shadow | The base owns the definition. Shadow drift surfaces at import as "Outdated contract" errors. If the base has changed, re-pull the shadow from the base; don't hand-edit. |324| `onCustomization<Slot>FlowConfig` populated but `...ExecutionBehaviorType` left at its default placeholder, or the reverse | The pair is always contractually present, but only non-null configs fire. Populate both together: set the config to `{ flowId, flowParameters }` AND set the behavior to `before` / `after` / `replace` based on intent. |325| Tailored flow referenced from `onCustomization<Slot>FlowConfig` doesn't exist in the overlay's `flows[]` / `rowFlows[]` | Validation catches some cases; runtime catches the rest. Author the tailored flow with `fromBaseConfiguration: null` in the appropriate `flows[]` / `rowFlows[]` array before populating the hook. |326| Deleting an inherited entry from JSON instead of setting `removed: true` | The shadow must remain for the platform to reconcile against the base. Delete → "Outdated contract" because the base still declares it. Suppress with `removed: true` to keep the shadow and hide the entry at runtime. |327| New tailored column "fixed" to `$row.entity.<field>` when populated imperatively from a secondary datasource | The empty declarative `value` is intentional — the cell is populated by the `after` customization on `on_data_loaded`. The "fix" overwrites the imperative population on every render. See [grids.md → Empty Declarative Bindings Are Legitimate](../grid-creator/references/grids.md#empty-declarative-bindings-are-legitimate). |328| Secondary enrichment datasource omitted; tailored column shows empty values | The standard pattern is `datasources[]` carries both the inherited primary (with `fromBaseConfiguration: true`) and the new `tailored_ds_<base>` (with `fromBaseConfiguration: null`). Without the secondary, there's nothing to fetch the extra fields from. |329| `$grid.vars.<id> = ...` or `$row.vars.<id> = ...` in tailored flow code with no declaration on the overlay | Import error: `Property 'vars' does not exist on type 'IGrid'` / `IRow`. Declare every var / rowVar the overlay's tailored flows write at the top-level `vars[]` / `rowVars[]`. Inheritance from the base does not extend to vars the overlay introduces. |330| Flattening: missed `queryOptions.selects` when consolidating a secondary into the primary (OData backings) | Silent failure — type metadata says the field exists, runtime data doesn't include it. `$row.entity.<field>` is `undefined`. Add the field to `datasources[0].queryOptions.selects` (or the appropriate `expands[].queryOptions.selects` for nested) **in addition to** the five type-metadata locations. This is the single most common flatten failure. |331| Flattening: `fromBaseConfiguration: true` left in place on some elements | The custom is supposed to be authoritative end-to-end. Any remaining `fromBaseConfiguration: true` means the platform still tries to resolve against a base — but `baseConfiguration` is `null`, so resolution fails. Flip every occurrence to `null` in one sweep. |332| Flattening: forgot to delete the `tailored_*` flow whose body was merged into the base slot | The flow is now dead code; references to it from elsewhere in the file may also be stale. Delete the freestanding entry from `flows[]` / `rowFlows[]` once its body has been merged. (Keep only if still being called as a helper.) |333| Flattening: vars / rowVars inherited from base not re-declared on the custom | The overlay may have implicitly inherited some vars from the base. On a standalone custom, every var/rowVar must be declared explicitly. Walk every `$grid.vars.<id>` / `$row.vars.<id>` reference in the flattened flow code and verify each has a top-level declaration. |334| Treated `custom_<base>` as a default naming convention for from-scratch components | The `custom_` prefix signals "the result of flattening a tailored overlay" *or* "intentionally signals no upstream relationship". For genuinely net-new components with no base history, the conventional name doesn't carry the `custom_` prefix. Don't reach for `custom_` as the default. |335| Piping `dxs configuration get -O envelope.json` directly into `dxs configuration upsert -D envelope.json` | Silently destroys config content. Always `jq .json envelope.json > body.json` before editing. See "Round-trip rule" above. |336| Reaching for `--allow-external` to get past an upsert refusal | The refusal means the reference name is owned by a referenced package and this branch has no owned copy — the flag *forks* it, and package updates stop flowing in. Tailor under a new `tailored_<base>` name, or fix it in its own package and re-pin. Fork only with the user's explicit agreement. |337| `description` exceeds 100 chars on the overlay or the flattened custom | SQL column limit — push will fail validation. Tighten. |338| Tailored grid edit pushed without invoking `grid-validator` | The generic `component-validator` doesn't catch five-location drift on the secondary datasource, OData `selects` misses, dynamic-filter registration mirror drift, or partial lookupcode/id syncs on tailored columns. Always invoke `grid-validator` for tailored grids — it's mandatory. |339340**After your edit, invoke `post-edit-verification` for description/JSON/schema hygiene. If you tailored a grid, also invoke `grid-validator` (mandatory — grids carry envelope/text-display gotchas). For non-grid tailored components, invoke `component-validator`.**