Editor Creator
Author or modify a Datex Studio editor (configurationTypeId=4) on a branch — a single-entity view/edit screen that hydrates one record, binds fields 1:1 to its properties, toggles between read-only and edit mode, and persists changes through a wrapping function that calls a CRUD action.
References
- ../datex-studio-shared/branch-setup.md — Branch/connection selection (shared across skills)
- references/editors.md — Authoritative editor authoring reference: file shape, runtime globals, invocation contract, common patterns, pre-flight checklist
- ../datex-studio-conventions/file-format.md —
configurationTypeId table and TypeScript-expression encoding rules
- ../datex-studio-conventions/naming-conventions.md —
_editor suffix, filename stem matching, display-name rule
- ../datex-studio-runtime/runtime-globals.md — platform-injected globals available in editor code (
$editor, $flows, $shell, $utils, ...)
- ../datex-studio-runtime/calling-conventions.md — UI-tier calling rules (call functions, never actions; CRUD via
$apis.<Package>.FootprintApi.extendedActions.<action_name>)
- ../form-creator/references/forms.md — sibling component for transient input collection (the editor-vs-form decision)
- ../grid-creator/references/grids.md — typical host for editors via row-click / row-action flows
- ../datasource-creator/references/flow-datasources.md — single-result shape required for editor-backing datasources
- ../component-wiring-check/references/component-wiring.md — host reference contracts, vars-must-be-declared rule, moduleId rule
Dependencies
requirements-gathering skill — invoked to produce a requirements brief if one doesn't already exist in the conversation context
form-creator / grid-creator skills — invoked when the requirement is actually transient input collection (form) or a multi-record list (grid), not a single-entity detail screen
datasource-creator skill — invoked when the single-result flow datasource backing the editor needs to be authored as a standalone config (rare — the embedded private datasource is the usual shape). To author the embedded one, follow ../datasource-creator/references/datasources.md → Creating an Owned Datasource — generate-flow --single --get-flow already emits the shape an editor requires, so only the envelope delta applies.
component-wiring-check skill — invoked to audit configParameters ↔ target inParams contracts on the editor's host (hub tab / grid row action / form) before push
type-definition-creator skill — invoked when the editor's bound entity interface or a related type definition needs authoring or extension (e.g. adding a field to the schema that the editor must mirror with a new binding)
tailoring-overlay skill — invoked when customer-specific extensions to the editor (added fields, modified bindings) need to live in a tailored overlay rather than the base config
CLI Lifecycle
Editor authoring goes through dxs configuration — the generic CRUD primitive over every platform configuration type. There is no dxs editor 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 editor (lowercase, matches ConfigurationEndpoints.normalize_type output), mapping to configurationTypeId: 4.
Create a new editor:
# 1. Build body.json from scratch (see references/editors.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 editor -b <branchId> -D body.json
# 3. Create
dxs configuration upsert editor -b <branchId> -D body.json
Edit an existing editor:
# 1. Fetch — note the envelope wrapper
dxs configuration get editor <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 editor -b <branchId> -D body.json
# 5. Push
dxs configuration upsert editor -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.
Editors are dense — the body carries toolbar, fieldsets, flows, datasourceConfig, and datasources substantially populated, with a code field on every embedded flow. Surgical edits in this much JSON are error-prone; round-trip discipline (fetch → jq-extract → edit → validate → push) is non-negotiable.
Workflow
[Phase 1: Setup + Requirements]
Follow branch-setup.md for branch/connection selection
|
[requirements brief in context?]
+-----+-----+
| |
YES NO -> invoke `requirements-gathering`
| |
+-----+------+
|
[Phase 2: Editor vs Form vs Grid decision]
Consult references/editors.md → "Purpose & When to Use":
- single hydrated entity, view/edit toggle, persist via CRUD -> editor
- transient input collection, returns outParams, no entity -> form
- multi-record tabular view -> grid
If form -> invoke `form-creator` instead and stop here.
If grid -> invoke `grid-creator` instead and stop here.
Create-only dialogs are usually forms, not editors.
|
[Phase 3: Author editor body]
Build body.json:
- File shape (configurationTypeId=4, *-editor.json suffix,
referenceName ends _editor)
- Embedded single-result private datasource in datasources[]
(configurationTypeId=6, type=flows, accessModifier=private,
getFlow populated; getListFlow/getByKeysFlow null;
resultIsCollection=false; outParams[0].isCollection=false)
- Flow shape: getFlow populated; getListFlow and getByKeysFlow null
(editors call .get() — code left in getListFlow silently breaks hydration)
- Entity shape mirrored: datasources[0].outParams[0].objectTypeDef
== datasourceConfig.configOutParameters.result.objectTypeDef
- onInitFlowConfig (pre-hydration; no $editor.entity reads)
vs onDataLoadedFlowConfig (post-hydration; entity-derived defaults)
- View/edit toggle via $editor.vars.edit_mode (declared in vars[])
- Save branches on $editor.entity.isNew (crud_create vs crud_update)
- onFormValidateFlowConfig gates $editor.toolbar.<save>.control.readOnly
- EditorFields parity: every entity-interface field has a binding
(stub with removed:true when UX isn't ready)
- Invoke `datasource-creator` if a standalone datasource is missing
- Invoke `component-wiring-check` to audit host's configParameters
|
[Phase 4: Validate + push]
dxs configuration validate editor -b <branchId> -D body.json
|
+----+----+
| |
CREATE MODIFY-EXISTING
| |
| use the corrected round-trip
| (get -O envelope -> jq .json -> body)
| |
+----+----+
|
v
dxs configuration upsert editor -b <branchId> -D body.json
(upsert creates or updates by referenceName — one command for both)
|
[Phase 5: Verify in Studio (optional)]
Open the editor as a dialog (from its host hub/grid); confirm
hydration, toggle into edit mode, save commits, cancel restores
|
[invoke `post-edit-verification`; then `component-validator`]
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 entity being edited (and which key the host passes in), which fields are visible/editable, view-only-vs-edit-mode behavior, save semantics (which CRUD action persists changes), and whether the same editor handles create mode.
- No brief — invoke the
requirements-gathering skill first. Getting the entity shape and save semantics right up front avoids the dense round-trip that follows.
Phase 2: Editor vs Form vs Grid decision
Consult references/editors.md → Purpose & When to Use before authoring. The choice is not stylistic — editors, forms, and grids serve different roles and aren't interchangeable.
Pick an editor when:
- The user views or modifies a single entity identified by a key.
- Field-level inputs map 1:1 to properties of that entity.
- The UX wants a distinct view-mode (read-only) and edit-mode (inputs unlocked, save active) toggle.
- The flow persists changes through a CRUD action (
crud_create_entity / crud_update_entity).
Pick a form instead when:
- The work is transient input collection that doesn't correspond to a stored record.
- The dialog returns
outParams to the caller; nothing persists implicitly.
- The dialog is creation-only and no entity yet exists to hydrate.
Pick a grid instead when:
- The user views or operates on multiple records in a tabular layout.
If the answer is form, stop and invoke form-creator. If the answer is grid, stop and invoke grid-creator. Editors can handle create mode via $editor.entity.isNew, but that path is justified only when the same component handles both create and edit for the same entity type — pure create dialogs are usually a better fit for a form.
Phase 3: Author editor body
Build body.json from the skeleton in references/editors.md → Minimal Valid Skeleton. Key points:
File basics. Per the Pre-Flight Checklist below + ../datex-studio-conventions/universal-checklist.md; see references/editors.md → File Location & Naming for the -editor.json file shape.
Embedded single-result datasource. Each editor embeds exactly one private, single-result flow datasource in datasources[]. Required shape: configurationTypeId: 6, type: "flows", accessModifier: "private", getFlow populated, getListFlow: null, getByKeysFlow: null, resultIsCollection: false, outParams[0].isCollection: false. A collection-returning datasource breaks the editor reference — datasourceConfig.get({...}) can't hydrate $editor.entity from a list. See references/editors.md → Embedded Private Datasource and ../datasource-creator/references/flow-datasources.md → Single-Result Shape. Owned is the default — author the datasource embedded unless an override applies, and promote to standalone only when a second consumer genuinely needs the same query (see ../datasource-creator/references/datasources.md → Owned by Default for the full override list). If one does, invoke datasource-creator to author a standalone version. The branch's server-side usage gate independently enforces this at contract-validation time — it blocks publish if the editor's datasource doesn't implement get on a single, non-collection result.
Entity shape mirror. datasources[0].outParams[0].objectTypeDef (the datasource side) must mirror datasourceConfig.configOutParameters.result.objectTypeDef (the editor's consumer side) field-for-field. Any entity-shape change touches both places in the same edit. The datasourceKeyDef on datasourceConfig matches the embedded datasource's keyDef exactly and matches the inParams shape the host passes.
onInitFlowConfig vs onDataLoadedFlowConfig. The single most common editor bug is flipping these. onInit fires before the datasource resolves — $editor.entity is not yet populated; reading it yields undefined. Use onInit for var setup, defaulting edit_mode on isNew, or logic independent of entity data. onDataLoaded fires after the entity hydrates — use it for entity-derived defaults, stashing pre-edit snapshots, and populating UI-only state from entity fields. See references/editors.md → onInitFlowConfig vs onDataLoadedFlowConfig.
View/edit mode toggle. Declare edit_mode in top-level vars[] (every $editor.vars.<id> written in flow code must be declared — see ../component-wiring-check/references/component-wiring.md). Edit button visible when !edit_mode; Save + Cancel visible when edit_mode. Field readOnly bound to !edit_mode (or computed per-field when some stay locked). Cancel restores original entity values — re-call the datasource or stash pre-edit values in $editor.vars during onDataLoaded.
Save branches on $editor.entity.isNew. When new → crud_create_entity (or the equivalent create action wrapped in a function); when existing → crud_update_entity. Neither branch hardcoded. The UI-tier calling rule applies: editor code calls functions via $flows.<Package>.<fn>; the function wraps the CRUD action call as $apis.<Package>.FootprintApi.extendedActions.<action_name>({...}). See ../datex-studio-runtime/calling-conventions.md.
Save button gated by validation. onFormValidateFlowConfig runs whenever any field changes. Compute is_valid from field values (often via $utils.isDefinedTrimmed for required strings), then set $editor.toolbar.<save_button>.control.readOnly = !is_valid. Optionally push per-field messages to control.validationMessage. See references/editors.md → Save Button Gated by onFormValidateFlowConfig.
EditorFields parity. The platform auto-generates an EditorFields TypeScript type from the bound entity's interface. The editor must declare a controlConfig-bearing binding for every field on that interface — nested paths flatten with __ (e.g. replenishments.rules → field id replenishments__rules). Missing a binding fails import. When the schema gains a field, the editor adds the binding in the same edit; a stub binding with removed: true is acceptable if real UX isn't ready. For array-of-object sub-trees, use the button + $editor.vars.<var> + set_config-merge pattern. See references/editors.md → EditorFields Requires a Binding for Every Schema Field and the serialized-config-JSON section for the wrapper-entity case.
TypeScript-expression encoding on every declarative-string slot. Every tooltip, placeholder, value, format, button label (when bound to a var) is inlined verbatim into generated TS. Wrap display text in backticks ("`Manage rules…`"), wrap plain-string literals in TS quotes ("'MM/DD/YYYY'"), leave raw expressions unwrapped ("$editor.vars.foo"). An unwrapped tooltip like "Manage rules." compiles to bare TS tokens and breaks the build. See ../datex-studio-conventions/file-format.md → Declarative String Values Are TypeScript Expressions.
No custom-enum FQNs in vars / inParams / outParams. Editors can't resolve $types.<Package>.e_<enum> in param declarations — declare those fields as primitive (string for string-valued enums, number for numeric) and cast at usage inside flow code.
onCustomization* slots stay null unless customization hooks are explicitly needed — they're platform-extension points for tailored overlays (tailoring-overlay), not everyday editor wiring.
Host contract audit. The hub tab, grid row action, or form that opens this editor as a dialog must declare a full configParameters contract — every inParam the editor declares gets an entry on the host, including unused ones with value: null. Invoke component-wiring-check to audit reference contracts before push. See ../component-wiring-check/references/component-wiring.md.
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 editor -b <branchId> -D body.json
# For a new editor
dxs configuration upsert editor -b <branchId> -D body.json
# For modify-existing (round-trip — never skip the jq extract)
dxs configuration get editor <configId> -b <branchId> -O envelope.json
jq .json envelope.json > body.json
# ... edit body.json ...
dxs configuration upsert editor -b <branchId> -D body.json
Validation surfaces missing required fields, malformed parameter-descriptor shapes, EditorFields parity violations, and reference errors before push. It does not catch the onInit-vs-onDataLoaded flip, undeclared $editor.vars.<id> writes, or unwrapped TypeScript-expression slots — those are behavioral and only surface at runtime. Walk the references/editors.md → Pre-Flight Checklist before push.
Phase 5: Verify in Studio (optional)
Open the editor as a dialog through its normal invocation path (a hub tab toolbar button, a grid row action, or a form that chains into it):
- Hydration succeeds —
$editor.entity is populated when onDataLoaded runs; fields display the loaded values.
- Edit-mode toggle works — Edit button flips
edit_mode = true; Save + Cancel appear; fields become editable.
- Save commits — branches correctly on
$editor.entity.isNew; the wrapping CRUD action succeeds; the dialog closes or flips back to read-only.
- Cancel restores original values — pre-edit snapshot survives (or the datasource re-fetches cleanly).
- For create mode (
isNew: true), the embedded datasource synthesizes a blank entity; the save path takes the create branch.
- Save button stays disabled until
onFormValidateFlowConfig reports is_valid.
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 the full checklist in references/editors.md → Pre-Flight Checklist. The fast version:
- File basics.
configurationTypeId: 4, suffix -editor.json, referenceName ends _editor — plus the universal checks (../datex-studio-conventions/universal-checklist.md).
- Exactly one embedded datasource —
accessModifier: "private", type: "flows", single-result shape (getFlow populated; getListFlow + getByKeysFlow null; resultIsCollection: false; outParams[0].isCollection: false).
- Entity shape mirrored —
datasourceConfig.configOutParameters.result.objectTypeDef field-for-field matches datasources[0].outParams[0].objectTypeDef. datasourceKeyDef matches the embedded datasource's keyDef and the host's inParams shape.
- Init-hook split correct —
onInitFlowConfig does not read $editor.entity; entity-dependent defaults and pre-edit snapshots live in onDataLoadedFlowConfig.
- Save branches on
$editor.entity.isNew — crud_create_entity when new, crud_update_entity when existing. Neither branch hardcoded.
onFormValidateFlowConfig gates $editor.toolbar.<save>.control.readOnly on field validity.
$editor.vars declared — every var written in flow code (edit_mode, snapshots, in-progress arrays) is in top-level vars[].
- EditorFields parity — every entity-interface field has a binding (stub
removed: true if UX isn't ready). Array-of-object sub-trees use the button + $editor.vars + set_config-merge pattern.
- Calling-tier compliance — editor code calls functions via
$flows.<Package>.<fn>; the function wraps CRUD actions via $apis.<Package>.FootprintApi.extendedActions.<action_name>({...}); no direct action calls from the editor.
- TypeScript-expression strings wrapped correctly — display text in backticks; raw expressions unwrapped; plain literals quoted.
onCustomization* slots null unless customization is intentional.
- No
$types.<Package>.e_<enum> in vars / inParams / outParams — primitives only at the param layer; cast at usage.
- Host carries a full
configParameters contract — every inParam the editor declares has an entry; unused ones use value: null. Audit via component-wiring-check.
Common Mistakes
| Mistake |
Fix |
Reading $editor.entity inside onInitFlowConfig |
Entity isn't hydrated yet — yields undefined. Move entity-derived defaults and pre-edit snapshots to onDataLoadedFlowConfig. |
Embedded datasource returns a collection (resultIsCollection: true or outParams[0].isCollection: true) |
Editor can't hydrate $editor.entity from a list. Switch to single-result shape — getFlow populated, getListFlow/getByKeysFlow null. |
datasources[0].outParams[0].objectTypeDef and datasourceConfig.configOutParameters.result.objectTypeDef drift apart |
Editor reference breaks at runtime. They mirror each other — any entity-shape change touches both. |
Save path hardcoded to crud_update_entity (or crud_create_entity) |
Breaks the other mode. Branch on $editor.entity.isNew. |
$editor.vars.edit_mode = true written in flow code without declaring edit_mode in top-level vars[] |
Var is undeclared — write fails silently or runtime error. Declare every var. |
| Missing field binding for a new schema field |
EditorFields type error: Property 'foo__bar' is missing... but required in type 'EditorFields'. Add the binding in the same edit; stub with removed: true if UX isn't ready. |
Mutating $editor.entity.<field> directly when the entity is a serialized-config wrapper |
Type error — entity exposes the wrapper, not the parsed config. Parse via JSON.parse($editor.entity.config) in set_state; reassemble in set_config; post $editor.vars.new_config via the CRUD action. |
| Array-of-object sub-tree authored as a flat field binding |
id__path flattening doesn't fit arrays. Use the button + $editor.vars.<array_var> + set_config-merge pattern (or a codeBox for power-user admin tooling). |
| Editor code calls an action directly |
UI-tier rule: editor calls functions only. The function wraps the action via $apis.<Package>.FootprintApi.extendedActions.<action_name>({...}). |
Unwrapped declarative-string slot ("Manage rules." instead of "`Manage rules.`") |
Inlined verbatim into generated TS — bare tokens break the build. Backticks for display text; quotes for plain literals; raw expressions unwrapped. |
$types.<Package>.e_<enum> in vars / inParams / outParams |
Custom-enum FQN doesn't resolve at the param layer. Declare as primitive (string / number); cast at usage inside flow code. |
datasourceConfig.configOutParameters authored as an object keyed by param name ({"result": {...}}) |
It is an array of parameter descriptors mirroring the embedded datasource's outParams — [{"id":"result","type":"object","objectTypeDef":[...],"isCollection":false}]. The object form fails validation with a raw Cannot deserialize ... into type 'IList<VarConfig>' dump that names no field. |
Embedded datasource missing isOwned: true on datasourceConfig |
isOwned is the only thing that routes the reference to the embedded datasources[] instead of a cross-application lookup. Without it the platform resolves configId + moduleId against other applications, fails, and reports Invalid contract. Referenced configuration <name> does not exist or has been renamed. Set isOwned: true. Removing moduleId does not fix this — on an owned reference moduleId is never read. See ../datasource-creator/references/datasources.md → Resolving an Owned Reference. |
Embedded datasource missing hasKey / hasResult / queryOptionsObjectTypeDef |
All three are required. queryOptionsObjectTypeDef repeats the entity shape (the same array as outParams[0].objectTypeDef) and is the fifth entity-shape typedef site — the one most often missed. Omitting it fails with the bare, undiagnostic message Entity definition is required. |
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 _editor or doesn't match filename stem |
Import / lookup breaks. Snake_case, _editor suffix, filename stem matches. |
moduleId on the host's reference set to the host's package instead of the editor's |
Cross-component reference rule — moduleId is always the target's package. See ../component-wiring-check/references/component-wiring.md. |
After your edit, invoke post-edit-verification to surface description/JSON/schema violations. For a final review, invoke component-validator.
1---2name: editor-creator3description: Use when authoring or modifying a Datex Studio editor (configurationTypeId=4, *-editor.json suffix) on a branch — single-entity view/edit screen with embedded single-result datasource, onInit/onDataLoaded lifecycle, view/edit mode toggle, and save-button gating via onFormValidateFlowConfig. Triggers: "create an editor", "build a detail screen for X entity", "view/edit a single record", "add a field to xxx_editor", "add save/cancel buttons", "$editor.entity is undefined in onInit", "save button never enables".4---56# Editor Creator78Author or modify a Datex Studio editor (configurationTypeId=4) on a branch — a single-entity view/edit screen that hydrates one record, binds fields 1:1 to its properties, toggles between read-only and edit mode, and persists changes through a wrapping function that calls a CRUD action.910## References1112- [../datex-studio-shared/branch-setup.md](../datex-studio-shared/branch-setup.md) — Branch/connection selection (shared across skills)13- [references/editors.md](references/editors.md) — Authoritative editor authoring reference: file shape, runtime globals, invocation contract, common patterns, pre-flight checklist14- [../datex-studio-conventions/file-format.md](../datex-studio-conventions/file-format.md) — `configurationTypeId` table and TypeScript-expression encoding rules15- [../datex-studio-conventions/naming-conventions.md](../datex-studio-conventions/naming-conventions.md) — `_editor` suffix, filename stem matching, display-name rule16- [../datex-studio-runtime/runtime-globals.md](../datex-studio-runtime/runtime-globals.md) — platform-injected globals available in editor code (`$editor`, `$flows`, `$shell`, `$utils`, ...)17- [../datex-studio-runtime/calling-conventions.md](../datex-studio-runtime/calling-conventions.md) — UI-tier calling rules (call functions, never actions; CRUD via `$apis.<Package>.FootprintApi.extendedActions.<action_name>`)18- [../form-creator/references/forms.md](../form-creator/references/forms.md) — sibling component for transient input collection (the editor-vs-form decision)19- [../grid-creator/references/grids.md](../grid-creator/references/grids.md) — typical host for editors via row-click / row-action flows20- [../datasource-creator/references/flow-datasources.md](../datasource-creator/references/flow-datasources.md) — single-result shape required for editor-backing datasources21- [../component-wiring-check/references/component-wiring.md](../component-wiring-check/references/component-wiring.md) — host reference contracts, vars-must-be-declared rule, moduleId rule2223## Dependencies2425- **`requirements-gathering`** skill — invoked to produce a requirements brief if one doesn't already exist in the conversation context26- **`form-creator`** / **`grid-creator`** skills — invoked when the requirement is actually transient input collection (form) or a multi-record list (grid), not a single-entity detail screen27- **`datasource-creator`** skill — invoked when the single-result flow datasource backing the editor needs to be authored as a standalone config (rare — the embedded private datasource is the usual shape). To author the embedded one, follow [../datasource-creator/references/datasources.md → Creating an Owned Datasource](../datasource-creator/references/datasources.md#creating-an-owned-datasource) — `generate-flow --single --get-flow` already emits the shape an editor requires, so only the envelope delta applies.28- **`component-wiring-check`** skill — invoked to audit `configParameters` ↔ target `inParams` contracts on the editor's host (hub tab / grid row action / form) before push29- **`type-definition-creator`** skill — invoked when the editor's bound entity interface or a related type definition needs authoring or extension (e.g. adding a field to the schema that the editor must mirror with a new binding)30- **`tailoring-overlay`** skill — invoked when customer-specific extensions to the editor (added fields, modified bindings) need to live in a tailored overlay rather than the base config3132## CLI Lifecycle3334Editor authoring goes through `dxs configuration` — the generic CRUD primitive over every platform configuration type. There is no `dxs editor` 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 **`editor`** (lowercase, matches `ConfigurationEndpoints.normalize_type` output), mapping to `configurationTypeId: 4`.3536**Create a new editor:**3738```bash39# 1. Build body.json from scratch (see references/editors.md → Minimal Valid Skeleton)40# 2. Validate — gates the push. Exit 1 = errors found (read validation_errors, fix, re-run), not a broken CLI41dxs configuration validate editor -b <branchId> -D body.json42# 3. Create43dxs configuration upsert editor -b <branchId> -D body.json44```4546**Edit an existing editor:**4748```bash49# 1. Fetch — note the envelope wrapper50dxs configuration get editor <configId> -b <branchId> -O envelope.json51# 2. EXTRACT THE INNER BODY (round-trip footgun guard — see "Round-trip rule" below)52jq .json envelope.json > body.json53# 3. Edit body.json54# 4. Validate — gates the push. Exit 1 = errors found (read validation_errors, fix, re-run), not a broken CLI55dxs configuration validate editor -b <branchId> -D body.json56# 5. Push57dxs configuration upsert editor -b <branchId> -D body.json58```5960### Round-trip rule (critical)6162When 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.6364Editors are dense — the body carries `toolbar`, `fieldsets`, `flows`, `datasourceConfig`, and `datasources` substantially populated, with a `code` field on every embedded flow. Surgical edits in this much JSON are error-prone; round-trip discipline (fetch → jq-extract → edit → validate → push) is non-negotiable.6566## Workflow6768```69[Phase 1: Setup + Requirements]70Follow branch-setup.md for branch/connection selection71 |72[requirements brief in context?]73 +-----+-----+74 | |75 YES NO -> invoke `requirements-gathering`76 | |77 +-----+------+78 |79[Phase 2: Editor vs Form vs Grid decision]80Consult references/editors.md → "Purpose & When to Use":81 - single hydrated entity, view/edit toggle, persist via CRUD -> editor82 - transient input collection, returns outParams, no entity -> form83 - multi-record tabular view -> grid84If form -> invoke `form-creator` instead and stop here.85If grid -> invoke `grid-creator` instead and stop here.86Create-only dialogs are usually forms, not editors.87 |88[Phase 3: Author editor body]89Build body.json:90 - File shape (configurationTypeId=4, *-editor.json suffix,91 referenceName ends _editor)92 - Embedded single-result private datasource in datasources[]93 (configurationTypeId=6, type=flows, accessModifier=private,94 getFlow populated; getListFlow/getByKeysFlow null;95 resultIsCollection=false; outParams[0].isCollection=false)96 - Flow shape: getFlow populated; getListFlow and getByKeysFlow null97 (editors call .get() — code left in getListFlow silently breaks hydration)98 - Entity shape mirrored: datasources[0].outParams[0].objectTypeDef99 == datasourceConfig.configOutParameters.result.objectTypeDef100 - onInitFlowConfig (pre-hydration; no $editor.entity reads)101 vs onDataLoadedFlowConfig (post-hydration; entity-derived defaults)102 - View/edit toggle via $editor.vars.edit_mode (declared in vars[])103 - Save branches on $editor.entity.isNew (crud_create vs crud_update)104 - onFormValidateFlowConfig gates $editor.toolbar.<save>.control.readOnly105 - EditorFields parity: every entity-interface field has a binding106 (stub with removed:true when UX isn't ready)107 - Invoke `datasource-creator` if a standalone datasource is missing108 - Invoke `component-wiring-check` to audit host's configParameters109 |110[Phase 4: Validate + push]111dxs configuration validate editor -b <branchId> -D body.json112 |113 +----+----+114 | |115 CREATE MODIFY-EXISTING116 | |117 | use the corrected round-trip118 | (get -O envelope -> jq .json -> body)119 | |120 +----+----+121 |122 v123dxs configuration upsert editor -b <branchId> -D body.json124 (upsert creates or updates by referenceName — one command for both)125 |126[Phase 5: Verify in Studio (optional)]127Open the editor as a dialog (from its host hub/grid); confirm128hydration, toggle into edit mode, save commits, cancel restores129 |130[invoke `post-edit-verification`; then `component-validator`]131```132133## Phase Details134135### Phase 1: Setup + Requirements1361371. 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.1382. Check whether a **requirements brief** already exists in the conversation context (produced by `requirements-gathering` or another calling skill).139 - **Brief exists** — use it. The brief should establish the entity being edited (and which key the host passes in), which fields are visible/editable, view-only-vs-edit-mode behavior, save semantics (which CRUD action persists changes), and whether the same editor handles create mode.140 - **No brief** — invoke the `requirements-gathering` skill first. Getting the entity shape and save semantics right up front avoids the dense round-trip that follows.141142### Phase 2: Editor vs Form vs Grid decision143144Consult [references/editors.md → Purpose & When to Use](references/editors.md#purpose--when-to-use) before authoring. The choice is not stylistic — editors, forms, and grids serve different roles and aren't interchangeable.145146Pick an **editor** when:147148- The user views or modifies a **single entity** identified by a key.149- Field-level inputs map 1:1 to properties of that entity.150- The UX wants a distinct view-mode (read-only) and edit-mode (inputs unlocked, save active) toggle.151- The flow persists changes through a CRUD action (`crud_create_entity` / `crud_update_entity`).152153Pick a **form** instead when:154155- The work is transient input collection that doesn't correspond to a stored record.156- The dialog returns `outParams` to the caller; nothing persists implicitly.157- The dialog is creation-only and no entity yet exists to hydrate.158159Pick a **grid** instead when:160161- The user views or operates on **multiple records** in a tabular layout.162163If the answer is form, stop and invoke `form-creator`. If the answer is grid, stop and invoke `grid-creator`. Editors can handle create mode via `$editor.entity.isNew`, but that path is justified only when **the same component handles both create and edit for the same entity type** — pure create dialogs are usually a better fit for a form.164165### Phase 3: Author editor body166167Build `body.json` from the skeleton in [references/editors.md → Minimal Valid Skeleton](references/editors.md#minimal-valid-skeleton). Key points:1681691. **File basics.** Per the **Pre-Flight Checklist** below + [../datex-studio-conventions/universal-checklist.md](../datex-studio-conventions/universal-checklist.md); see [references/editors.md → File Location & Naming](references/editors.md#file-location--naming) for the `-editor.json` file shape.1701712. **Embedded single-result datasource.** Each editor embeds **exactly one** private, single-result flow datasource in `datasources[]`. Required shape: `configurationTypeId: 6`, `type: "flows"`, `accessModifier: "private"`, `getFlow` populated, `getListFlow: null`, `getByKeysFlow: null`, `resultIsCollection: false`, `outParams[0].isCollection: false`. A collection-returning datasource breaks the editor reference — `datasourceConfig.get({...})` can't hydrate `$editor.entity` from a list. See [references/editors.md → Embedded Private Datasource](references/editors.md#embedded-private-datasource-keyed-by-the-entity-id) and [../datasource-creator/references/flow-datasources.md → Single-Result Shape](../datasource-creator/references/flow-datasources.md#single-result-shape--getflow). **Owned is the default** — author the datasource embedded unless an override applies, and promote to standalone only when a second consumer genuinely needs the same query (see [../datasource-creator/references/datasources.md → Owned by Default](../datasource-creator/references/datasources.md#owned-by-default) for the full override list). If one does, invoke `datasource-creator` to author a standalone version. The branch's server-side usage gate independently enforces this at contract-validation time — it blocks publish if the editor's datasource doesn't implement `get` on a single, non-collection result.1721733. **Entity shape mirror.** `datasources[0].outParams[0].objectTypeDef` (the datasource side) must mirror `datasourceConfig.configOutParameters.result.objectTypeDef` (the editor's consumer side) field-for-field. Any entity-shape change touches **both places** in the same edit. The `datasourceKeyDef` on `datasourceConfig` matches the embedded datasource's `keyDef` exactly and matches the `inParams` shape the host passes.1741754. **`onInitFlowConfig` vs `onDataLoadedFlowConfig`.** The single most common editor bug is flipping these. `onInit` fires **before** the datasource resolves — `$editor.entity` is **not yet populated**; reading it yields undefined. Use `onInit` for var setup, defaulting `edit_mode` on `isNew`, or logic independent of entity data. `onDataLoaded` fires **after** the entity hydrates — use it for entity-derived defaults, stashing pre-edit snapshots, and populating UI-only state from entity fields. See [references/editors.md → onInitFlowConfig vs onDataLoadedFlowConfig](references/editors.md#oninitflowconfig-vs-ondataloadedflowconfig).1761775. **View/edit mode toggle.** Declare `edit_mode` in top-level `vars[]` (every `$editor.vars.<id>` written in flow code must be declared — see [../component-wiring-check/references/component-wiring.md](../component-wiring-check/references/component-wiring.md)). Edit button visible when `!edit_mode`; Save + Cancel visible when `edit_mode`. Field `readOnly` bound to `!edit_mode` (or computed per-field when some stay locked). Cancel restores original entity values — re-call the datasource or stash pre-edit values in `$editor.vars` during `onDataLoaded`.1781796. **Save branches on `$editor.entity.isNew`.** When new → `crud_create_entity` (or the equivalent create action wrapped in a function); when existing → `crud_update_entity`. Neither branch hardcoded. The UI-tier calling rule applies: editor code calls **functions** via `$flows.<Package>.<fn>`; the function wraps the CRUD action call as `$apis.<Package>.FootprintApi.extendedActions.<action_name>({...})`. See [../datex-studio-runtime/calling-conventions.md](../datex-studio-runtime/calling-conventions.md).1801817. **Save button gated by validation.** `onFormValidateFlowConfig` runs whenever any field changes. Compute `is_valid` from field values (often via `$utils.isDefinedTrimmed` for required strings), then set `$editor.toolbar.<save_button>.control.readOnly = !is_valid`. Optionally push per-field messages to `control.validationMessage`. See [references/editors.md → Save Button Gated by onFormValidateFlowConfig](references/editors.md#save-button-gated-by-onformvalidateflowconfig).1821838. **EditorFields parity.** The platform auto-generates an `EditorFields` TypeScript type from the bound entity's interface. The editor must declare a `controlConfig`-bearing binding for **every** field on that interface — nested paths flatten with `__` (e.g. `replenishments.rules` → field id `replenishments__rules`). Missing a binding fails import. When the schema gains a field, the editor adds the binding in the same edit; a stub binding with `removed: true` is acceptable if real UX isn't ready. For array-of-object sub-trees, use the button + `$editor.vars.<var>` + `set_config`-merge pattern. See [references/editors.md → EditorFields Requires a Binding for Every Schema Field](references/editors.md#editorfields-requires-a-binding-for-every-schema-field) and the serialized-config-JSON section for the wrapper-entity case.1841859. **TypeScript-expression encoding on every declarative-string slot.** Every `tooltip`, `placeholder`, `value`, `format`, button `label` (when bound to a var) is inlined verbatim into generated TS. Wrap display text in backticks (`` "`Manage rules…`" ``), wrap plain-string literals in TS quotes (`"'MM/DD/YYYY'"`), leave raw expressions unwrapped (`"$editor.vars.foo"`). An unwrapped tooltip like `"Manage rules."` compiles to bare TS tokens and breaks the build. See [../datex-studio-conventions/file-format.md → Declarative String Values Are TypeScript Expressions](../datex-studio-conventions/file-format.md#declarative-string-values-are-typescript-expressions).18618710. **No custom-enum FQNs in `vars` / `inParams` / `outParams`.** Editors can't resolve `$types.<Package>.e_<enum>` in param declarations — declare those fields as primitive (`string` for string-valued enums, `number` for numeric) and cast at usage inside flow code.18818911. **`onCustomization*` slots stay `null`** unless customization hooks are explicitly needed — they're platform-extension points for tailored overlays (`tailoring-overlay`), not everyday editor wiring.19019112. **Host contract audit.** The hub tab, grid row action, or form that opens this editor as a dialog must declare a full `configParameters` contract — every `inParam` the editor declares gets an entry on the host, including unused ones with `value: null`. Invoke `component-wiring-check` to audit reference contracts before push. See [../component-wiring-check/references/component-wiring.md](../component-wiring-check/references/component-wiring.md).192193### Phase 4: Validate + push194195```bash196# Validate the body locally against the branch. Exit 1 = validation found errors197# (read validation_errors, fix body.json, re-run) — not a broken CLI. Do not push on exit 1.198dxs configuration validate editor -b <branchId> -D body.json199200# For a new editor201dxs configuration upsert editor -b <branchId> -D body.json202203# For modify-existing (round-trip — never skip the jq extract)204dxs configuration get editor <configId> -b <branchId> -O envelope.json205jq .json envelope.json > body.json206# ... edit body.json ...207dxs configuration upsert editor -b <branchId> -D body.json208```209210Validation surfaces missing required fields, malformed parameter-descriptor shapes, `EditorFields` parity violations, and reference errors before push. It does **not** catch the `onInit`-vs-`onDataLoaded` flip, undeclared `$editor.vars.<id>` writes, or unwrapped TypeScript-expression slots — those are behavioral and only surface at runtime. Walk the [references/editors.md → Pre-Flight Checklist](references/editors.md#pre-flight-checklist) before push.211212### Phase 5: Verify in Studio (optional)213214Open the editor as a dialog through its normal invocation path (a hub tab toolbar button, a grid row action, or a form that chains into it):215216- Hydration succeeds — `$editor.entity` is populated when `onDataLoaded` runs; fields display the loaded values.217- Edit-mode toggle works — Edit button flips `edit_mode = true`; Save + Cancel appear; fields become editable.218- Save commits — branches correctly on `$editor.entity.isNew`; the wrapping CRUD action succeeds; the dialog closes or flips back to read-only.219- Cancel restores original values — pre-edit snapshot survives (or the datasource re-fetches cleanly).220- For create mode (`isNew: true`), the embedded datasource synthesizes a blank entity; the save path takes the create branch.221- Save button stays disabled until `onFormValidateFlowConfig` reports `is_valid`.222223If 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.224225## Pre-Flight Checklist226227Before push, walk the full checklist in [references/editors.md → Pre-Flight Checklist](references/editors.md#pre-flight-checklist). The fast version:2282291. **File basics.** `configurationTypeId: 4`, suffix `-editor.json`, `referenceName` ends `_editor` — plus the universal checks ([../datex-studio-conventions/universal-checklist.md](../datex-studio-conventions/universal-checklist.md)).2302. **Exactly one embedded datasource** — `accessModifier: "private"`, `type: "flows"`, single-result shape (`getFlow` populated; `getListFlow` + `getByKeysFlow` null; `resultIsCollection: false`; `outParams[0].isCollection: false`).2313. **Entity shape mirrored** — `datasourceConfig.configOutParameters.result.objectTypeDef` field-for-field matches `datasources[0].outParams[0].objectTypeDef`. `datasourceKeyDef` matches the embedded datasource's `keyDef` and the host's `inParams` shape.2324. **Init-hook split correct** — `onInitFlowConfig` does not read `$editor.entity`; entity-dependent defaults and pre-edit snapshots live in `onDataLoadedFlowConfig`.2335. **Save branches on `$editor.entity.isNew`** — `crud_create_entity` when new, `crud_update_entity` when existing. Neither branch hardcoded.2346. **`onFormValidateFlowConfig` gates `$editor.toolbar.<save>.control.readOnly`** on field validity.2357. **`$editor.vars` declared** — every var written in flow code (`edit_mode`, snapshots, in-progress arrays) is in top-level `vars[]`.2368. **EditorFields parity** — every entity-interface field has a binding (stub `removed: true` if UX isn't ready). Array-of-object sub-trees use the button + `$editor.vars` + `set_config`-merge pattern.2379. **Calling-tier compliance** — editor code calls functions via `$flows.<Package>.<fn>`; the function wraps CRUD actions via `$apis.<Package>.FootprintApi.extendedActions.<action_name>({...})`; no direct action calls from the editor.23810. **TypeScript-expression strings wrapped correctly** — display text in backticks; raw expressions unwrapped; plain literals quoted.23911. **`onCustomization*` slots `null`** unless customization is intentional.24012. **No `$types.<Package>.e_<enum>`** in `vars` / `inParams` / `outParams` — primitives only at the param layer; cast at usage.24113. **Host carries a full `configParameters` contract** — every inParam the editor declares has an entry; unused ones use `value: null`. Audit via `component-wiring-check`.242243## Common Mistakes244245| Mistake | Fix |246|---|---|247| Reading `$editor.entity` inside `onInitFlowConfig` | Entity isn't hydrated yet — yields undefined. Move entity-derived defaults and pre-edit snapshots to `onDataLoadedFlowConfig`. |248| Embedded datasource returns a collection (`resultIsCollection: true` or `outParams[0].isCollection: true`) | Editor can't hydrate `$editor.entity` from a list. Switch to single-result shape — `getFlow` populated, `getListFlow`/`getByKeysFlow` null. |249| `datasources[0].outParams[0].objectTypeDef` and `datasourceConfig.configOutParameters.result.objectTypeDef` drift apart | Editor reference breaks at runtime. They mirror each other — any entity-shape change touches both. |250| Save path hardcoded to `crud_update_entity` (or `crud_create_entity`) | Breaks the other mode. Branch on `$editor.entity.isNew`. |251| `$editor.vars.edit_mode = true` written in flow code without declaring `edit_mode` in top-level `vars[]` | Var is undeclared — write fails silently or runtime error. Declare every var. |252| Missing field binding for a new schema field | `EditorFields` type error: `Property 'foo__bar' is missing... but required in type 'EditorFields'`. Add the binding in the same edit; stub with `removed: true` if UX isn't ready. |253| Mutating `$editor.entity.<field>` directly when the entity is a serialized-config wrapper | Type error — `entity` exposes the wrapper, not the parsed config. Parse via `JSON.parse($editor.entity.config)` in `set_state`; reassemble in `set_config`; post `$editor.vars.new_config` via the CRUD action. |254| Array-of-object sub-tree authored as a flat field binding | `id__path` flattening doesn't fit arrays. Use the button + `$editor.vars.<array_var>` + `set_config`-merge pattern (or a `codeBox` for power-user admin tooling). |255| Editor code calls an action directly | UI-tier rule: editor calls functions only. The function wraps the action via `$apis.<Package>.FootprintApi.extendedActions.<action_name>({...})`. |256| Unwrapped declarative-string slot (`"Manage rules."` instead of `` "`Manage rules.`" ``) | Inlined verbatim into generated TS — bare tokens break the build. Backticks for display text; quotes for plain literals; raw expressions unwrapped. |257| `$types.<Package>.e_<enum>` in `vars` / `inParams` / `outParams` | Custom-enum FQN doesn't resolve at the param layer. Declare as primitive (`string` / `number`); cast at usage inside flow code. |258| `datasourceConfig.configOutParameters` authored as an object keyed by param name (`{"result": {...}}`) | It is an **array of parameter descriptors** mirroring the embedded datasource's `outParams` — `[{"id":"result","type":"object","objectTypeDef":[...],"isCollection":false}]`. The object form fails validation with a raw `Cannot deserialize ... into type 'IList<VarConfig>'` dump that names no field. |259| Embedded datasource missing `isOwned: true` on `datasourceConfig` | `isOwned` is the **only** thing that routes the reference to the embedded `datasources[]` instead of a cross-application lookup. Without it the platform resolves `configId` + `moduleId` against other applications, fails, and reports `Invalid contract. Referenced configuration <name> does not exist or has been renamed`. Set `isOwned: true`. Removing `moduleId` does **not** fix this — on an owned reference `moduleId` is never read. See [../datasource-creator/references/datasources.md → Resolving an Owned Reference](../datasource-creator/references/datasources.md#resolving-an-owned-reference--isowned-alone-decides). |260| Embedded datasource missing `hasKey` / `hasResult` / `queryOptionsObjectTypeDef` | All three are required. `queryOptionsObjectTypeDef` repeats the **entity shape** (the same array as `outParams[0].objectTypeDef`) and is the fifth entity-shape typedef site — the one most often missed. Omitting it fails with the bare, undiagnostic message `Entity definition is required`. |261| 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. |262| `description` exceeds 100 chars | SQL column limit — push will fail validation. Tighten. |263| `referenceName` doesn't end in `_editor` or doesn't match filename stem | Import / lookup breaks. Snake_case, `_editor` suffix, filename stem matches. |264| `moduleId` on the host's reference set to the host's package instead of the editor's | Cross-component reference rule — `moduleId` is always the target's package. See `../component-wiring-check/references/component-wiring.md`. |265266**After your edit, invoke `post-edit-verification` to surface description/JSON/schema violations. For a final review, invoke `component-validator`.**