About editor-creator
A skill for creating or modifying Datex Studio editor configurations on a branch, aimed at single-entity view and edit screens. It focuses on binding fields to entity properties, switching between read-only and edit modes, and saving through a wrapping function that calls a CRUD action.
Key features
- 01Create or modify editor configurations
- 02Bind fields to a single entity
- 03Configure read-only and edit modes
- 04Call CRUD actions through a wrapping function
- 05Validate configurations before writing to a branch
Use cases
- 01Build a detail and edit screen for one record
- 02Bind editor fields to entity properties
- 03Adjust save and edit behavior in an existing configuration
Install
Copy the command and run it in a terminal.
npx skills add https:/ / github.com/ datex/ skills --skill editor-creatorAfter installing, type/editor-creatorin Claude Code to confirm it loaded.
Bundled filesevals/evals.jsonreferences/editors.md
Details
From the project READMEEditor 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 —
configurationTypeIdtable and TypeScript-expression encoding rules - ../datex-studio-conventions/naming-conventions.md —
_editorsuffix, 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-gatheringskill — invoked to produce a requirements brief if one doesn't already exist in the conversation contextform-creator/grid-creatorskills — invoked when the requirement is actually transient input collection (form) or a multi-record list (grid), not a single-entity detail screendatasource-creatorskill — 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-flowalready emits the shape an editor requires, so only the envelope delta applies.component-wiring-checkskill — invoked to auditconfigParameters↔ targetinParamscontracts on the editor's host (hub tab / grid row action / form) before pushtype-definition-creatorskill — 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-overlayskill — 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-gatheringor 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-gatheringskill 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
outParamsto 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.jsonfile 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",getFlowpopulated,getListFlow: null,getByKeysFlow: null,resultIsCollection: false,outParams[0].isCollection: false. A collection-returning datasource breaks the editor reference —datasourceConfig.get({...})can't hydrate$editor.entityfrom 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, invokedatasource-creatorto 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 implementgeton a single, non-collection result. -
Entity shape mirror.
datasources[0].outParams[0].objectTypeDef(the datasource side) must mirrordatasourceConfig.configOutParameters.result.objectTypeDef(the editor's consumer side) field-for-field. Any entity-shape change touches both places in the same edit. ThedatasourceKeyDefondatasourceConfigmatches the embedded datasource'skeyDefexactly and matches theinParamsshape the host passes. -
onInitFlowConfigvsonDataLoadedFlowConfig. The single most common editor bug is flipping these.onInitfires before the datasource resolves —$editor.entityis not yet populated; reading it yields undefined. UseonInitfor var setup, defaultingedit_modeonisNew, or logic independent of entity data.onDataLoadedfires 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_modein top-levelvars[](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 whenedit_mode. FieldreadOnlybound 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.varsduringonDataLoaded. -
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.
onFormValidateFlowConfigruns whenever any field changes. Computeis_validfrom field values (often via$utils.isDefinedTrimmedfor required strings), then set$editor.toolbar.<save_button>.control.readOnly = !is_valid. Optionally push per-field messages tocontrol.validationMessage. See references/editors.md → Save Button Gated by onFormValidateFlowConfig. -
EditorFields parity. The platform auto-generates an
EditorFieldsTypeScript type from the bound entity's interface. The editor must declare acontrolConfig-bearing binding for every field on that interface — nested paths flatten with__(e.g.replenishments.rules→ field idreplenishments__rules). Missing a binding fails import. When the schema gains a field, the editor adds the binding in the same edit; a stub binding withremoved: trueis 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, buttonlabel(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 (stringfor string-valued enums,numberfor numeric) and cast at usage inside flow code. -
onCustomization*slots staynullunless 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
configParameterscontract — everyinParamthe editor declares gets an entry on the host, including unused ones withvalue: null. Invokecomponent-wiring-checkto 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.entityis populated whenonDataLoadedruns; 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
onFormValidateFlowConfigreportsis_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,referenceNameends_editor— plus the universal checks (../datex-studio-conventions/universal-checklist.md). - Exactly one embedded datasource —
accessModifier: "private",type: "flows", single-result shape (getFlowpopulated;getListFlow+getByKeysFlownull;resultIsCollection: false;outParams[0].isCollection: false). - Entity shape mirrored —
datasourceConfig.configOutParameters.result.objectTypeDeffield-for-field matchesdatasources[0].outParams[0].objectTypeDef.datasourceKeyDefmatches the embedded datasource'skeyDefand the host'sinParamsshape. - Init-hook split correct —
onInitFlowConfigdoes not read$editor.entity; entity-dependent defaults and pre-edit snapshots live inonDataLoadedFlowConfig. - Save branches on
$editor.entity.isNew—crud_create_entitywhen new,crud_update_entitywhen existing. Neither branch hardcoded. onFormValidateFlowConfiggates$editor.toolbar.<save>.control.readOnlyon field validity.$editor.varsdeclared — every var written in flow code (edit_mode, snapshots, in-progress arrays) is in top-levelvars[].- EditorFields parity — every entity-interface field has a binding (stub
removed: trueif 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*slotsnullunless customization is intentional.- No
$types.<Package>.e_<enum>invars/inParams/outParams— primitives only at the param layer; cast at usage. - Host carries a full
configParameterscontract — every inParam the editor declares has an entry; unused ones usevalue: null. Audit viacomponent-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.