About grid-creator
grid-creator is a skill for creating or modifying Datex Studio grids on a branch. It guides datasource selection, grid configuration, validation, and upload, with particular attention to keeping entity fields and dynamic-filter settings synchronized.
Key features
- 01Creates and modifies Datex Studio grids
- 02Guides OData or flow datasource selection
- 03Maintains grid entity-shape locations
- 04Configures filters, sorting, and interactions
- 05Validates and uploads configuration through dxs
Use cases
- 01Build a tabular multi-record view
- 02Add filtering, sorting, or row actions to a grid
- 03Update an existing grid configuration on a branch
Install
Copy the command and run it in a terminal.
npx skills add https:/ / github.com/ datex/ skills --skill grid-creatorAfter installing, type/grid-creatorin Claude Code to confirm it loaded.
Bundled filesevals/evals.jsonreferences/grids.md
Details
From the project READMEGrid Creator
Author or modify a Datex Studio grid (configurationTypeId=3) on a branch — the platform's primary data-density component. Grids render tabular rows from a backing datasource with optional inline editing, selection, toolbars, dynamic filters and sorting, and per-row/per-cell interactions. They are typically mounted inside hub tabs, and they carry more cross-location invariants than any other component — the five-location entity-shape rule, the two-site dynamic filter/sort registration mirror, and the OData runtime selects location all live here.
References
- ../datex-studio-shared/branch-setup.md — Branch/connection selection (shared across skills)
- references/grids.md — Authoritative grid authoring reference: file shape, runtime globals, five-location rule, dynamic filters, secondary datasources, imperative cell API, pre-flight checklist
- ../datex-studio-conventions/file-format.md —
configurationTypeIdtable and TypeScript-expression encoding rules - ../datex-studio-conventions/naming-conventions.md —
_gridsuffix, filename stem matching, display-name rule - ../datex-studio-runtime/runtime-globals.md — platform-injected globals available in grid code (
$grid,$row,$flows,$apis,$utils, ...) - ../datex-studio-runtime/calling-conventions.md — UI-tier calling rules (call functions, never actions; CRUD via
$apis.<Package>.FootprintApi.extendedActions.<action_name>) - ../datasource-creator/references/datasources.md — datasource taxonomy (variants × query types) for what backs the rows
- ../datasource-creator/references/flow-datasources.md — flow-type datasource shape (paginated
getListFlow/getByKeysFlow) for grid-embedded datasources - ../component-wiring-check/references/component-wiring.md — host reference contracts, vars-must-be-declared rule, moduleId rule
- ../tailoring-overlay/references/tailoring.md — overlay model for tailored grid variants and secondary-datasource enrichment
- ../collection-filter-authoring/SKILL.md — Any/All filters on scalar leaves through object collections; includes verified CLI generation and remaining live integration checks
Dependencies
requirements-gatheringskill — invoked to produce a requirements brief if one doesn't already exist in the conversation contextschema-explorerskill — invoked before authoring any PascalCase-plural-named grid (or any grid whose rows look like an OData entity) to confirm whether an entity exists in the Footprint schema; a 5-second lookup is cheaper than rebuilding the grid against the wrong sourcedatasource-creatorskill — invoked when the grid needs a standalone datasource (shared across multiple consumers); grid-only datasources are authored as owned entries inside the grid'sdatasources[]array, not as separate files. To author that owned entry, follow ../datasource-creator/references/datasources.md → Creating an Owned Datasource — scaffold withdxs datasource generate-flow, then splice. Keep the pagination count ingetListFlow; the top-level component/reference outputs follow the finalized datasource contract.form-creator/editor-creator/selector-creatorskills — invoked when the requirement is actually transient input collection (form), a single-entity view/edit screen (editor), or a dropdown/autocomplete (selector), not a tabular listhub-creatorskill — invoked when the grid's host hub tab / button needs itsconfigParameterscontract set up to mount the gridcomponent-wiring-checkskill — invoked to auditconfigParameters↔ targetinParamscontracts on both the grid's host (outer) and the grid's embedded datasource (inner) before pushtailoring-overlayskill — invoked when authoring or maintaining a tailored grid variant (baseConfiguration+onCustomization*FlowConfig) or when secondary enrichment datasources come from the overlaygrid-validatorskill — mandatory invocation after every grid edit; re-runs the pre-flight checklist, catches five-location drift, partial lookupcode/id syncs, ODataselectsmisses, and dynamic-filter registration mirror drift. Treat its blockers as must-fix.
CLI Lifecycle
Grid authoring goes through dxs configuration — the generic CRUD primitive over every platform configuration type. There is no dxs grid subcommand and no field-level patching; you build (or fetch + extract) the whole JSON body, edit it, and push the whole thing back. The type identifier in the CLI is grid (lowercase, matches ConfigurationEndpoints.normalize_type output), mapping to configurationTypeId: 3.
Create a new grid:
# 1. Build body.json from scratch (see references/grids.md → Minimal Valid Skeleton)
# 2. 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
# 3. Create
dxs configuration upsert grid -b <branchId> -D body.json
Edit an existing grid:
# 1. Fetch — note the envelope wrapper
dxs configuration get grid <configId> -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
# 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
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.
Grids carry columns, datasourceConfig, an embedded datasources[] array (each entry a full flow- or OData-type datasource with its own getListFlow / getByKeysFlow and entity-shape locations), flows / rowFlows (with a code string on every executeCodeConfig step), topToolbar / toolbar (polymorphic control descriptors), filters, and grid-level inParams / outParams / vars / rowVars schemas — round-trip discipline (fetch → jq-extract → edit → validate → push) is non-negotiable, and partial syncs that touch only some of the entity-shape locations are the single most common source of upload-time contract errors.
Workflow
[Phase 1: Setup + Requirements]
Follow branch-setup.md for branch/connection selection
|
[requirements brief in context?]
+-----+-----+
| |
YES NO -> invoke `requirements-gathering`
| |
+-----+------+
|
[Phase 2: Pre-author rows-source decision]
Consult references/grids.md → "Pre-Author Decision — What Backs the Rows?":
- PascalCase-plural name -> invoke `schema-explorer` (mandatory)
before defaulting to flow-type; if entity exists, build OData-backed
- snake_case name -> check the branch for a storage config
(`dxs source explore configs --type storage`) or treat as flow-type
- ambiguous -> invoke `schema-explorer` first
Owned vs standalone:
- grid-only datasource -> embedded entry in `datasources[]`
- shared across consumers -> standalone (invoke `datasource-creator`)
If requirement is single-entity view/edit -> invoke `editor-creator` and stop.
If requirement is transient input collection -> invoke `form-creator` and stop.
If requirement is a dropdown/autocomplete -> invoke `selector-creator` and stop.
|
[Phase 3: Author grid body]
Build body.json:
- File shape (configurationTypeId=3, *-grid.json suffix,
referenceName ends _grid, snake_case matches filename stem)
- Five-location entity-shape rule (flow-type) or five+selects (OData);
every entity field present at every site
- Two-site dynamic filter/sort registration mirror (datasources[0] and
datasourceConfig); per-column dynamicFilter/dynamicOrderBy/
dynamicFilterType/dynamicFilterControl; no $filter/$orderby in any
inParams or configParameters
- Dynamic filter/sort is all-or-nothing — flow-type backings either
fully wire (registrations + per-column + applyDynamicFilter/
applyDynamicOrderBy in getListFlow) or fully disable
- Toolbar items polymorphic — click handlers inside the type-specific
config block (buttonConfig.clickFlowConfig.flowId), siblings null;
destructive buttons get buttonDefaultStyleClass: "destructive";
selection-gated buttons seed readOnly: true statically AND gate
live in on_select_row
- Secondary enrichment datasources owned in same datasources[] array;
invoke from on_data_loaded; batch with Id in ${ids}; write via
imperative cell API ($row.cells.<col>.displayControl.text / value)
- Imperative cell API for post-load enrichment and change-guarded
saves; empty declarative bindings are legitimate when value is
populated imperatively — don't "fix" to $row.entity.<field>
- Text display bindings coerce non-string entity fields at the bind
site, not in the entity schema (entity type drives filter operators)
- Every $grid.vars.<id> / $row.vars.<id> written in flow code is
declared in top-level vars[] / rowVars[]
- Embedded datasource carries the full component-identity envelope
(referenceName matching datasourceConfig.configId, title, description,
hasKey, hasResult, accessModifier, ...)
- Invoke `schema-explorer` for OData entity / property / navigation
property validation before authoring OData-backed grids
- Invoke `component-wiring-check` to audit BOTH the outer host
contract AND the inner grid → embedded-datasource contract
(full_text_search and any inParam need explicit configParameters
entries)
|
[Phase 4: Validate + push]
dxs configuration validate grid -b <branchId> -D body.json
|
+----+----+
| |
CREATE MODIFY-EXISTING
| |
| use the corrected round-trip
| (get -O envelope -> jq .json -> body)
| |
+----+----+
|
v
dxs configuration upsert grid -b <branchId> -D body.json
(upsert creates or updates by referenceName — one command for both)
|
[Phase 5: Validate with `grid-validator` (MANDATORY)]
Delegate to `grid-validator` with the grid's file path / configId.
It re-runs the full Pre-Flight Checklist, catches five-location drift,
partial lookupcode/id syncs, OData `selects` misses, and registration
mirror drift. Treat its blockers as must-fix.
|
[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-gatheringor another calling skill).- Brief exists — use it. The brief should establish the row entity (or aggregated row shape), which columns the grid renders, which fields are filterable/sortable, the host that mounts the grid, and any row-action / toolbar-action flows the user needs.
- No brief — invoke the
requirements-gatheringskill first. Getting the row shape, filterability, and host mounting right up front avoids re-authoring the entity-shape locations and dynamic-filter registrations from scratch.
Phase 2: Pre-author rows-source decision
Before authoring any grid, decide what feeds the rows. This decision determines the embedded datasource's type (oDataQuery vs flows) and changes the five-location rule. Get this wrong and you write the whole datasource against the wrong source. Consult references/grids.md (datasource shape and the five-location rule) and the rows-source decision in Phase 2 below before authoring.
Flow-datasource shape check: a grid's rows datasource must be the keyed-collection shape —
resultIsCollection: true with both getListFlow and getByKeysFlow populated and a
non-empty keyDef. A flow datasource with only getListFlow cannot back a grid: single-row
refresh after actions calls getByKeys, which won't exist. Read suitability off the
implemented methods before wiring — see
../datasource-creator/references/flow-datasources.md
→ "Reading suitability off an existing flow datasource". This isn't just a runtime risk —
the branch's server-side usage gate enforces it at contract-validation time and blocks
publish if the grid's datasource is missing getList or getByKeys.
The grid's name is the strongest hint:
- PascalCase plural (
TaskStatuses,Orders,Shipments,Warehouses) — probably an OData entity in the Footprint schema. Invokeschema-explorerwithdescribe entity <Name>(orsearch <Name>) before authoring. If it resolves, build an OData-backed grid against that entity. Hard rule: never default to a flow-type datasource for a PascalCase-plural name without first runningschema-explorer. - snake_case (
task_statuses,invoicing_rules,widget_options) — probably a feature-owned storage component or a computed/aggregated source. Check the branch for a matching storage config (dxs source explore configs --branch <id> --type storage --search <name>), or treat as flow-type backed by feature code. - Ambiguous / unsure — invoke
schema-explorerfirst. A 5-second lookup is cheaper than rebuilding the grid after the user points out the entity exists.
If schema-explorer returns no match, fall back to a flow-type datasource — but document the choice in the grid's description or alongside the embedded datasource (so the next author doesn't repeat the lookup).
Owned vs. standalone. A datasource that exists only to feed this grid's rows is authored as an owned entry inside the grid's datasources[] array, not as a standalone <name>-datasource config of its own. Standalone datasources are for shared consumers (multiple grids / forms / editors / actions). A datasource whose sole purpose is this grid's row shape belongs inside the grid — it keeps the five-location rule local to one file and prevents accidental reuse that would later resist row-shape edits. Secondary enrichment datasources (see Phase 3) are also owned. If the requirement calls for a shared datasource, invoke datasource-creator instead. This is the platform-wide default, not a grid-specific preference — the rule and the full list of conditions that justify standalone are in ../datasource-creator/references/datasources.md → Owned by Default.
Wrong-component-type checks. If the requirement is actually single-entity view/edit, invoke editor-creator instead and stop. If it's transient input collection (a dialog that returns outParams), invoke form-creator and stop. If it's a dropdown / autocomplete field, invoke selector-creator and stop. Grids are for tabular multi-record lists; the other three are not interchangeable substitutes.
Phase 3: Author grid body
Build body.json from the skeleton in references/grids.md → Minimal Valid Skeleton. Key points:
-
File basics.
configurationTypeId: 3.referenceNameends in_grid(e.g.task_statuses_grid-grid.json→task_statuses_grid). File suffix is-grid.json— plus the universal checks (../datex-studio-conventions/universal-checklist.md). See references/grids.md → File Shape at a Glance and ../datex-studio-conventions/naming-conventions.md. -
Five-location base entity-shape rule. A grid-embedded flow datasource carries the base row entity in five independent locations:
datasources[0].queryOptionsObjectTypeDef,datasources[0].outParams[result].objectTypeDef,datasources[0].getListFlow.outParams[result].objectTypeDef,datasources[0].getByKeysFlow.outParams[result].objectTypeDef, anddatasourceConfig.configOutParameters[result].objectTypeDef. Plus thecodestrings ingetListFlow/getByKeysFlowthat populate the new field. For linked/custom fields, follow the finalized output contract rather than inserting derived fields into the base entity. After any base entity-shape edit, grep an unchanged neighbor field's id across the file — the new field must have a matching occurrence at each site. OData-backed grids change the rule: locations 3 and 4 arenulland a sixth, runtime-only location becomes load-bearing —datasources[0].queryOptions.selects(and the appropriateexpands[].queryOptions.selectsfor nested fields). Missingselectsleaves the entity undefined at runtime even if every type-metadata location is correct. See references/grids.md → Datasource Wiring — Five Places Must Stay in Sync. -
Dynamic filters and sorting — four sites + column wiring. Adding a filterable/sortable field involves: (a) the field exists in all five entity-shape locations above; (b) the field is registered identically in both
datasources[0].dynamicFilters/dynamicOrderBys(embedded datasource side) anddatasourceConfig.dynamicFilters/dynamicOrderBys(consumer side) — drift surfaces at import asOutdated contract. Type mismatch for dynamic filtering/sorting; (c) per-columndynamicFilter(dotted path),dynamicOrderBy,dynamicFilterType(inParam-shaped leaf type),dynamicFilterControl(textBox/numberBox/dateBox); (d) never declare$filter/$orderbyin anyinParamslist and don't add them todatasourceConfig.configParameters— the platform auto-injects typed versions from the registrations; manual declarations collide. See references/grids.md → Dynamic Filters and Sorting. -
Dynamic filter/sort application is all-or-nothing. For flow-type backings, the platform does not auto-apply
$filter/$orderbyto the row set. If you register filter/sort UI but don't apply the inputs ingetListFlowcode, the UI controls visibly do nothing — the bug state. The two acceptable end states are: (A) fully wired — registrations + per-column wiring + filter/sort execution insidegetListFlowagainst($flow.inParams as any).$filterand($flow.inParams as any).$orderby(the canonicalapplyDynamicFilter/applyDynamicOrderByhelpers cover flat scalar paths; collection filters require the evaluator described in collection-filter-authoring); or (B) fully disabled — nodynamicFilters/dynamicOrderBysregistrations on either side, no per-column wiring. For OData-type backings, the platform translates registrations into the OData URL declaratively — state A is the default. See references/grids.md → Application Is All-Or-Nothing and Default In-Memory $filter / $orderby Application for the canonical helpers. -
Array-field caveat. A primitive array cannot be the direct target of a scalar filter/sort. For a scalar leaf reached through an object collection, use collection-filter-authoring instead; sorting remains scalar-only. For filtering a joined array display, introduce a scalar sidecar field alongside (e.g.
accounts_display=accounts.join(', ')); the column keeps its array-indexed display ($row.entity.accounts[0]);dynamicFiltertargets the sidecar. Sidecars must appear in all five entity-shape locations and be populated in flow code. See references/grids.md → Array-Field Caveat — Scalar Sidecar Pattern. -
Toolbar items are polymorphic. Each
topToolbar/toolbarentry'stypeselects which sibling<type>Configblock the platform reads —button→buttonConfig,selectBox→selectBoxConfig, etc. All other sibling config blocks stay explicitlynull. Separator entries have all configs null. Click handlers live inside the type-specific config block (buttonConfig.clickFlowConfig.flowId), not at the entry's top level. Three conventions every toolbar button should follow: (a) destructive actions carrybuttonDefaultStyleClass: "destructive"; (b) selection-gated buttons initreadOnly: trueinbuttonConfig(so they mount disabled before the firston_select_rowevent) AND haveon_select_rowtoggle them live; (c) buttons with no useful tooltip settooltip: "''"(the TS empty-string literal) to suppress the platform's display-label fallback. Dynamic tooltips route through a declared$grid.vars.<name>— direct flow-code assignment to.tooltipis a no-op. See references/grids.md → Toolbar Items. -
Secondary (enrichment) datasources. A grid's
datasources[]array can carry additional datasources beyond the primary. Typical use: enrich each row with fields not in the primary entity (UDFs, external-system fields, foreign-key display resolutions). Invoke fromon_data_loadedafter$grid.rowshas loaded, batch the query withId in ${ids}, and write results via the Imperative Cell API. Secondary datasources do not carrydynamicFilters/dynamicOrderBys— those drive the primary's filter UI only. They still follow the five-location rule for their own entity shape. Tailored grids use this pattern heavily — see ../tailoring-overlay/references/tailoring.md. -
Imperative cell API. Row and grid flows can mutate individual cells:
$row.cells.<col>.displayControl.text = "..."(read-mode text),$row.cells.<col>.displayControl.value = ...(read-mode for checkBox / selectBox / etc.),$row.cells.<col>.editControl.value = ...(edit-mode), and the platform-managed$row.cells.<col>.editControl.isChangedboolean. Two idioms: post-load enrichment inon_data_loaded(columns whose value is populated imperatively keep emptydisplayControl.<cfg>.valuestrings — don't "fix" to$row.entity.<field>unless the entity actually has the field); change-guarded writes inon_save_existing_row(if ($row.cells.<col>.editControl.isChanged) { payload.<X> = $row.cells.<col>.editControl.value; }for minimal PATCH payloads). See references/grids.md → Imperative Cell API and Empty Declarative Bindings Are Legitimate. -
Text display bindings coerce non-string entity fields at the bind site.
displayControl.textConfig.valueis typedstring; binding"$row.entity.<Id>"when<Id>isEdm.Int32/ boolean / date fails import withType '<number|boolean|...>' is not assignable to type 'string'. Coerce at the bind site ("$row.entity.Id?.toString()","`${$row.entity.Id}`","$row.entity.Active ? 'Yes' : 'No'","$utils.date.format($row.entity.CreatedSysDateTime, 'MM/DD/YYYY')"), not in the entity schema — the entity's declaredtypedrives dynamic filter/sort operator selection. See references/grids.md → Text Display Bindings Need String Values. -
Embedded datasource component-identity envelope. An embedded
datasources[]entry isn't just a query spec — it's a full datasource component with the same identity fields a standalone-datasource.jsonwould carry.datasources[0].referenceNamemust matchdatasourceConfig.configIdexactly (the lookup key); the entry also needstitle, non-emptydescription≤ 100 chars,hasKey: truefor paginated/collection datasources,hasResult: true,id: nullon net-new,linkedDatasources/customColumns(null when unused), declaredinParams/outParams/vars/events, andaccessModifier: "private". Missing the envelope reports asInvalid contract. Referenced own configuration <name> does not exist or has been renamedand cascades intoCannot find name 'get' / 'getList' / 'inParams' / 'refresh'TS errors (all fallout from the one unresolved reference). See references/grids.md → Embedded Datasource Component-Identity Envelope. -
fullTextSearchand the innerconfigParameterscontract.fullTextSearchis non-nullable boolean.truemounts the built-in search box; its value arrives at the datasource as$flow.inParams.full_text_searchonly if you wire it via adatasourceConfig.configParametersentry whosevalueis"$grid.fullTextSearch". There is no auto-wiring. Same rule for any other inParam declared on the embedded datasource — a declared inParam with noconfigParametersentry stays unbound at runtime, any filter that depends on it never fires, and the import flags a contract mismatch between the grid and its embedded datasource. See references/grids.md → datasourceConfig.configParameters — Feeding Inputs to the Datasource. -
$grid.vars/$row.varsdeclared. Every$grid.vars.<id>written in flow code must be declared in the top-levelvars[]array; every$row.vars.<id>inrowVars[]. Same property-descriptor shape asinParams/outParams. Missing declarations fail on import withProperty 'vars' does not exist on type 'IGrid'(orIRow). -
Sibling
*Configkeys are explicitly null. On everydisplayControl,editControl, toolbar entry, and filter entry, only the active sub-config is populated; the rest stay explicitlynull. Match this convention for structural diffing — existing grid components keep all sibling slots present and null. -
Calling-tier compliance. Grid code calls functions via
$flows.<Package>.<fn>; the function wraps the CRUD action call as$apis.<Package>.FootprintApi.extendedActions.<action_name>({...}). The built-incrud_create_entity/crud_update_entity/crud_delete_entityfrom Utilities are the conventional persistence path. No direct action calls from grid code. See ../datex-studio-runtime/calling-conventions.md. -
Mounting from a hub. Hubs embed grids inside
tabs[].contentConfig(whencontentType: "grid"). The mount carries its ownconfigId+moduleId—moduleIdis the grid's package, not the hosting hub's feature folder.configParameterson the mount feed the grid's owninParams(every inParam the grid declares gets an entry on the host);configEventssubscribe to grid-emitted events. Invokecomponent-wiring-checkto audit reference contracts on both sides — the outer host → grid contract AND the inner grid → embedded-datasource contract. See ../component-wiring-check/references/component-wiring.md and references/grids.md → Mounting a Grid from a Hub.
Phase 4: Validate + push
# Validate the body locally against the branch. 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 grid
dxs configuration upsert grid -b <branchId> -D body.json
# For modify-existing (round-trip — never skip the jq extract)
dxs configuration get grid <configId> -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 reference errors before push. It does not catch five-location drift, OData selects misses, dynamic-filter registration mirror drift between datasources[0] and datasourceConfig, partial lookupcode/id syncs, half-wired dynamic filter/sort (state C), unwrapped TypeScript-expression slots, or text display bindings that need coercion — those are structural or behavioral and only surface at upload-time validation or runtime. Walk the pre-flight checklist below before push, and always invoke grid-validator (Phase 5).
Phase 5: Validate with grid-validator (MANDATORY)
Any grid edit — net-new, entity-shape change, filter/sort registration change, toolbar change, secondary-datasource change, or even a label tweak — must be followed by an invocation of the grid-validator subagent on the affected grid. Grids carry more silent-failure traps than any other component (the five-location rule, the two-site registration mirror, the OData selects sixth location, the partial-lookupcode/id syncs, the half-wired filter/sort bug state), and the generic component-validator doesn't know about them.
Delegate to the grid-validator subagent with the grid's file path or configId. It re-runs the full Pre-Flight Checklist, flags drift you may have missed (especially five-location drift), and returns a punch list — not a rewrite. Treat its blockers as must-fix before declaring the grid done. Skipping this step is how partial-sync errors reach upload-time validation.
If the running app is available, also open the grid through its normal mount path (the hub tab that hosts it) and confirm: row population from the backing datasource, column rendering (no empty columns where imperatively-enriched values were expected), filter / sort controls applying to the row set (state A, not state C), toolbar selection-gated buttons mounting disabled and enabling on first selection, and $grid.events.outParamsChange.emit() propagating to the host.
If the running app isn't available, re-fetch the config (using the corrected jq .json extract pattern) and diff against body.json to confirm the push landed.
Pre-Flight Checklist
Before push, walk this pre-flight checklist (and invoke grid-validator for the deep grid-specific gate). The fast version:
- File name, suffix,
referenceNameagree.descriptionnon-empty and ≤ 100 chars.accessModifierset. - Grid-specific datasources are embedded, not standalone. A standalone
<grid-specific-name>-datasourceconfig on the branch is a smell — promote to an owned entry inside the grid, or confirm another component actually consumes it. - Embedded datasource carries the full component-identity envelope —
referenceNamematchesdatasourceConfig.configId,title,description,hasKey: true,hasResult: true,id: nullon net-new,linkedDatasources/customColumns(null when unused), declaredinParams/outParams/vars/events,accessModifier: "private". - Text display bindings coerce non-string entity fields at the bind site. Coerce
Edm.Int32with?.toString(),booleanwith a ternary,Datevia$utils.date.format(...)— never change the entity schema's declaredtypeto satisfy a display binding. - Base entity-shape locations in sync. For fields consumed from returned rows, grep a neighbor field's id; count of occurrences must match for the new field. For OData-backed grids, confirm
queryOptions.selectsincludes the field (locations 3 and 4 arenull); nested fields go inexpands[].queryOptions.selects. Explicit filter-only OData navigation may remain unexpanded and absent from outputs; register its metadata-resolved nested filter tree. - Dynamic filter/sort wiring: field in both registration sites (
datasources[0].dynamicFiltersanddatasourceConfig.dynamicFilters); per-column plain-pathdynamicFilter/dynamicFilterType/dynamicFilterControlfor enabled filters, anddynamicOrderByonly for supported scalar sorting; no$filter/$orderbyin anyinParamsorconfigParameters. - Dynamic filter/sort application is all-or-nothing. For flow-type backings, either fully wired (registrations + per-column + applied in
getListFlow) or fully disabled (registrations and per-column wiring removed). State C — UI controls registered but flow code doesn't apply them — is the bug state. - Primitive arrays use a scalar sidecar for joined-display filter/sort. Object-collection scalar leaves use collection-filter-authoring, including quantifier-aware execution. Sorting remains scalar-only.
- Toolbar click handlers are inside the type-specific config block, not at the entry's top level. Sibling config blocks are explicitly
null. Separator entries have all configsnull. - Destructive buttons carry
buttonDefaultStyleClass: "destructive"; buttons with no tooltip usetooltip: "''"(empty TS literal) so the platform doesn't fall back to the label. Dynamic tooltips bind to a declared$grid.vars.<name>— direct flow-code assignment to.tooltipis a no-op. on_select_rowgates toolbarreadOnlyfor selection-dependent buttons (live toggling), and those buttons also seedreadOnly: truestatically inbuttonConfigso they mount disabled before the first selection event fires.- Cells populated imperatively have empty
displayControl.<cfg>.value— don't "fix" to$row.entity.<field>unless the entity actually has the field. - Every
$grid.vars.<id>and$row.vars.<id>written in flow code is declared in the grid's top-levelvars[]/rowVars[]arrays. - Outer host contract. Every inParam the grid declares has an entry on the host's
configParameters; no extra entries for params the grid doesn't declare.moduleIdon the host's reference is the grid's package, not the host's. - Inner datasource contract. The grid's own
datasourceConfig.configParameterscovers every inParam declared on the embedded datasource — includingfull_text_searchwhenfullTextSearch: true(no auto-wiring; needs an explicit entry withvalue: "$grid.fullTextSearch"). $grid.events.outParamsChange.emit()fires after any write to$grid.outParams.*the host subscribes to.- Calling-tier compliance — grid code calls functions via
$flows.<Package>.<fn>; functions wrap CRUD actions via$apis.<Package>.FootprintApi.extendedActions.<action_name>({...}); no direct action calls from the grid. grid-validatorinvoked. Mandatory — see closer below.
Common Mistakes
| Mistake | Fix |
|---|---|
Adding a _lookupcode column but forgetting the _id companion in the five entity-shape locations | A storage column usually comes as a pair: owner_id (number) + owner_lookupcode (string). The numeric id still needs to appear in all five locations even when only the lookupcode has a visible column — otherwise configOutParameters[result].objectTypeDef and outParams[result].objectTypeDef diverge on the id's presence. Carry every new storage field into all five, not just the ones with visible columns. |
| Patching only 3 of 5 entity-shape locations | Easy to miss getListFlow.outParams[result].objectTypeDef and getByKeysFlow.outParams[result].objectTypeDef because they sit nested inside flow bodies that look like unrelated flow definitions. They aren't — they declare what the flow returns, and the datasource's own outParams pulls from them. |
OData-backed grid: field added to queryOptionsObjectTypeDef and configOutParameters but missing from queryOptions.selects | The type metadata declares the field; selects is what actually retrieves it. Missing the sixth location leaves the entity undefined at runtime — the grid imports cleanly, renders an empty column, and may throw on first row render if a binding expression dereferences the undefined value. Add to expands[].queryOptions.selects for nested fields. |
Dynamic filter UI registered but getListFlow doesn't apply $filter / $orderby (state C) | Either fully wire (apply applyDynamicFilter / applyDynamicOrderBy against ($flow.inParams as any).$filter / $orderby) or fully disable (remove registrations and per-column wiring). Half-wired UI controls that do nothing are the bug state. |
Drift between datasources[0].dynamicFilters and datasourceConfig.dynamicFilters | Import error: Datasource <id>: Outdated contract. Type mismatch for dynamic filtering/sorting. Both registration sites must be identical — update them in the same edit. |
$filter / $orderby declared in getListFlow.inParams or in datasourceConfig.configParameters | The platform auto-injects typed versions from the registrations. Manual declarations collide. Remove them; cast with ($flow.inParams as any).$filter at the application site. |
| Scalar filter/sort targets a primitive array directly | Use a scalar sidecar for joined-display matching; for object-collection scalar leaves use collection-filter-authoring. Quantified filtering does not enable collection sorting. |
Toolbar entry's clickFlowConfig placed at the entry's top level | Click handlers live inside the type-specific config block (buttonConfig.clickFlowConfig.flowId). Top-level placement is silently ignored. |
displayControl.textConfig.value: "$row.entity.Id" when Id is Edm.Int32 | Import error: Type 'number' is not assignable to type 'string'. Coerce at the bind site ("$row.entity.Id?.toString()") — don't change the entity's declared type to string, which corrupts dynamic filter/sort operator selection. |
Imperatively-populated column "fixed" to $row.entity.<field> | The fix overwrites the imperative population on every render. Empty displayControl.<cfg>.value strings are legitimate for cells whose value comes from on_data_loaded enrichment. |
| Selection-gated toolbar button mounts enabled, can be clicked before any row is selected | The on_row_selected flow only fires after a selection change. Seed readOnly: true in buttonConfig statically AND gate it live in on_select_row — belt + suspenders. |
Embedded datasources[0].referenceName doesn't match datasourceConfig.configId | Import error: Invalid contract. Referenced own configuration <name> does not exist or has been renamed, followed by a cascade of Cannot find name 'get' / 'getList' / 'inParams' / 'refresh' TS errors. Fix the identity envelope and the entire cascade disappears. |
fullTextSearch: true and datasource declares full_text_search inParam but no configParameters entry on the grid | Search box mounts but the value never reaches the datasource. Add { parameter: { id: "full_text_search", ... }, value: "$grid.fullTextSearch" } to datasourceConfig.configParameters. |
$grid.vars.<id> = ... or $row.vars.<id> = ... in flow code with vars: null / rowVars: null | Import error: Property 'vars' does not exist on type 'IGrid' / IRow. Declare every var / rowVar in the top-level array with the same property-descriptor shape as inParams/outParams. |
Flow code assigns to $grid.topToolbar.<id>.buttonConfig.tooltip (or column / filter .tooltip) and tooltip doesn't change | .tooltip is declarative-only. Declare $grid.vars.<name> (string), bind the slot to "$grid.vars.<name>", and assign the var in flow code. |
Hub mount's moduleId set to the hub's feature folder instead of the grid's package | Cross-component reference rule — moduleId is always the target's module. The grid lives where it's registered. See ../component-wiring-check/references/component-wiring.md. |
Sibling *Config slot dropped (e.g. only textConfig populated on a column's displayControl, other sub-configs missing) | Existing grids keep all sibling slots present and null for structural diffing. Match the convention. |
rowSizingType / columnSizingType set to a non-member literal ("fill", "Fill") | Error: Error converting value '...' to type 'System.Nullable\1[...EGridColumnSizingType]'. Members are camelCase: "cellsWidth", "headersWidth", "fixedWidth", "fitedWidth"(note spelling), and"default", "relaxed", "compact"forrowSizingType`. |
fullTextSearch set to null or { enabled: true } | The server expects a bare System.Boolean. Both shapes fail import. Use true or false. |
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. |
description exceeds 100 chars | SQL column limit — push will fail validation. Tighten. |
referenceName doesn't end in _grid or doesn't match filename stem | Import / lookup breaks. Snake_case, _grid suffix, filename stem matches. |
Grid edit pushed without invoking grid-validator | The generic component-validator doesn't catch five-location drift, OData selects misses, dynamic-filter registration mirror drift, or partial lookupcode/id syncs. Always invoke grid-validator — it's mandatory. |
After your edit, invoke grid-validator (mandatory — grids carry envelope/text-display gotchas the generic validator misses). For description/JSON/schema hygiene, also invoke post-edit-verification.