Skip to content

How-to: form

List, clone, and export entity forms (systemform). See the CLI reference for every flag.

List an entity's forms

crm form list cwx_ticket

Output columns: name, type, formid, isdefault (type is the raw systemform optionset integer). By default only main forms are returned.

Use --type to filter to one or more form types (repeatable, case-insensitive); recognized tokens are dashboard, main, quickview, quickcreate, dialog, and card:

crm form list cwx_ticket --type quickcreate
crm form list cwx_ticket --type main --type quickview

Use --all to list every form type (no type filter):

crm form list cwx_ticket --all

--type and --all are mutually exclusive — passing both is a usage error.

Clone a form to another entity

crm form clone cwx_ticket "Ticket Main Form" --to new_incidentlog --solution cwx_crmworx

The source form's formxml is read from cwx_ticket, the entity reference is retargeted to new_incidentlog, the form's internal element/label ids are regenerated, and a new systemform record is created via the Web API. No solution-zip is needed. Regenerating the internal ids lets you clone the same source form repeatedly without collisions on on-premises (which — unlike Dataverse online — enforces those ids as unique); references to control types, security roles, and subgrid views are preserved.

--solution is required — a component created without an explicit target solution would otherwise land only in the system Default Solution. Pass --solution Default for a deliberate Default-Solution-only write.

By default the clone stages the change — no PublishAllXml runs. Pass --publish to publish it immediately:

crm form clone cwx_ticket "Ticket Main Form" --to new_incidentlog \
    --solution cwx_crmworx --publish

Ambiguous form names

If two main forms share the same name, the command errors rather than guessing:

Error: Ambiguous: 2 forms named 'Ticket Main Form' — cannot pick one
automatically. Matches: formid='aaa...' type=2, formid='bbb...' type=2

Use crm form list cwx_ticket to inspect the colliding formids, then rename one form in the UI before cloning.

Relationship to metadata clone-entity --with-forms

crm form clone is the standalone surface for the same form-clone logic that crm metadata clone-entity --with-forms invokes when duplicating a whole entity. Use crm form clone when you only need to push a single form to another entity without cloning the entity itself.

Add a field to a form

crm form add-field cwx_ticket cwx_priority --solution cwx_crmworx

--solution is required on every mutating form verb — a component created without an explicit target solution would otherwise land only in the system Default Solution. Pass --solution Default for a deliberate Default-Solution-only write.

Resolves the control classid from the attribute's live metadata (no manual lookup needed), then splices a <cell>/<control> into the target section and PATCHes the systemform record. Errors if the field is already on the form, the attribute does not exist on the entity, or the attribute type has no mapped classid (see "Unmapped types" below).

By default the field lands in the first section of the first tab. Narrow the target with --tab and --section (by name or id):

crm form add-field cwx_ticket cwx_priority --tab "General" --section "Details" \
    --solution cwx_crmworx

Use --form when the entity has more than one main form; without it the command errors if the choice is ambiguous.

Publishing

These verbs stage the change by default — no PublishAllXml runs (the CLI-wide convention shared with form clone, metadata create-entity, etc.). This matters here: GET /systemforms returns the published FormXml, so a staged (unpublished) PATCH is invisible in the UI and on re-export until you publish.

Pass --publish to publish a single edit immediately, or batch several staged edits and publish once at the end with crm solution publish-all:

crm form add-field cwx_ticket cwx_priority --solution cwx_crmworx --publish   # publish this edit now
crm solution publish-all                                    # or: publish everything once the batch is done

Don't chain staged edits to one form

Each verb recomputes the FormXml from the form's published snapshot. A second staged (flagless) edit therefore reads the form without the first edit's pending change and overwrites it. To make several edits safely, either pass --publish on each one, or publish between edits — chaining staged writes loses all but the last.

Preview without writing

crm --dry-run form add-field cwx_ticket cwx_priority --solution cwx_crmworx

Returns would_add: true with the resolved classid and target coordinates; no PATCH is issued. --solution is still required under --dry-run — it is validated before any backend call.

Unmapped attribute types

The command maps these AttributeType values to their control classid: String, Memo, Integer, Decimal, Money, DateTime, Boolean, Picklist, State, Status, Lookup, Customer, Owner, PartyList. For any other type (Double, MultiSelectPicklist, BigInt, Uniqueidentifier, …) the command errors rather than guess a classid. In that case, export the form, hand-splice the <control> (copy the classid from a stock table that already has the control), PATCH back, and publish — same pipeline as before this feature existed (see crm form export).

Remove a field from a form

crm form remove-field cwx_ticket cwx_priority --solution cwx_crmworx

Removes the field's <cell> from the form layout (and tidies an emptied <row>), then PATCHes the systemform record. Errors if the field is not on the form.

crm --dry-run form remove-field cwx_ticket cwx_priority --solution cwx_crmworx      # would_remove: true

Move a field to a different tab or section

crm form set-field cwx_ticket cwx_priority --tab "Details" --section "Status" \
    --solution cwx_crmworx

Relocates an existing field's <cell> to the target tab/section. The cell (including its id and control binding) is preserved — only its position changes. Errors if the field is not already on the form (use add-field first).

crm --dry-run form set-field cwx_ticket cwx_priority --tab "Details" \
    --solution cwx_crmworx  # would_move: true

Toggle field presentation properties

crm form set-field-props cwx_ticket cwx_priority --disabled --hidden --solution cwx_crmworx

Toggles one or more presentation properties of an existing field on the form — locked/unlocked, disabled/enabled, show/hide label, visible/hidden. The field must already be on the form; use add-field first if it is not.

At least one property flag is required; any omitted flag is left untouched.

crm form set-field-props cwx_ticket cwx_priority \
    --locked --no-show-label --solution cwx_crmworx
crm --dry-run form set-field-props cwx_ticket cwx_priority --visible --solution cwx_crmworx

Under --dry-run the response carries would_update: true; no PATCH is issued.

Required-level is not a form property

--required LEVEL does not set a form property. Required-level is attribute metadata — passing it here routes you to the correct command with a clear error rather than silently no-op'ing at the form layer:

Error: Required-level is attribute metadata, not a form property; setting it on a
form has no effect. Use: crm metadata update-attribute cwx_ticket cwx_priority
--required LEVEL

Cell vs control attributes

disabled is a <control> attribute; locklevel, showlabel, and visible are <cell> attributes. The FormXml schema rejects visible on a <control> — the CLI applies each flag to the correct element automatically.

Wire JS event handlers

The CLI manages JS script libraries and event handlers directly in the form's <formLibraries> and <events> XML without any manual FormXml editing. The three mutating verbs mirror the field editors and share the same --form, --publish, --solution, --json, and --dry-run conventions; list-handlers is read-only (--form and --json only).

Prerequisite: the web resource must already exist. The editor never creates web resources — register them first with crm webresource create, then wire them.

Register a script library

crm form add-library cwx_ticket --library cwx_/scripts/ticket.js --solution cwx_crmworx

Registers cwx_/scripts/ticket.js in the form's <formLibraries>. The operation is idempotent — if the library is already registered, the command succeeds without adding a duplicate entry. Under --dry-run the response carries would_add_library: true and issues no PATCH.

crm --dry-run form add-library cwx_ticket --library cwx_/scripts/ticket.js \
    --solution cwx_crmworx

Wire an event handler

form add-handler registers the library (deduped) and adds the handler in one call. The handler lands in <Handlers> (the customizer-owned block) and is always appended last, so existing handler order is preserved.

# onload — fires when the form opens
crm form add-handler cwx_ticket \
    --event onload \
    --library cwx_/scripts/ticket.js \
    --function App.onLoad \
    --solution cwx_crmworx

# onsave — fires when the record is saved
crm form add-handler cwx_ticket \
    --event onsave \
    --library cwx_/scripts/ticket.js \
    --function App.onSave \
    --solution cwx_crmworx

# onchange — fires when a specific field changes; --field is required
crm form add-handler cwx_ticket \
    --event onchange --field cwx_priority \
    --library cwx_/scripts/ticket.js \
    --function App.onPriorityChange \
    --solution cwx_crmworx

--field is required for onchange, and invalid for onload/onsave. The command also validates that --field names a field that is already on the form — add it first with crm form add-field if needed.

Duplicate handlers are refused. Adding the same event + function combination a second time errors rather than creating a duplicate entry.

Additional options (defaults shown):

  • --pass-context / --no-pass-context — pass the execution context as the function's first argument (default: on).
  • --enabled / --no-enabled — whether the handler is active (default: on).
  • --param — repeatable; emitted as a comma-separated parameters attribute.
crm form add-handler cwx_ticket \
    --event onload \
    --library cwx_/scripts/ticket.js \
    --function App.onLoad \
    --solution cwx_crmworx \
    --no-pass-context \
    --param "debug=false" \
    --param "verbose=true"

Under --dry-run the response carries would_add_handler: true.

List handlers

form list-handlers is read-only — no --publish or --solution flag.

crm form list-handlers cwx_ticket

Output columns: event, field (blank for onload/onsave), function, library, enabled, pass_context. Per the output contract, data is a bare array of handler rows and the resolved form goes to meta:

{
  "ok": true,
  "data": [
    {
      "event":            "onload",
      "field":            null,
      "function":         "App.onLoad",
      "library":          "cwx_/scripts/ticket.js",
      "enabled":          true,
      "pass_context":     true,
      "handler_unique_id":"<guid>"
    }
  ],
  "meta": { "formid": "<guid>", "form": "Ticket Main Form" }
}

Only the customizer-wired <Handlers> are listed — the platform-internal <InternalHandlers> are never reported.

Remove a handler

crm form remove-handler cwx_ticket \
    --event onload --function App.onLoad --solution cwx_crmworx

# onchange requires --field
crm form remove-handler cwx_ticket \
    --event onchange --field cwx_priority --function App.onPriorityChange \
    --solution cwx_crmworx

Removes the handler identified by event + function (plus field for onchange). Tidies any now-empty <Handlers>, <event>, or <events> containers so no invalid empty XML is left behind. Errors if the handler is not found.

crm --dry-run form remove-handler cwx_ticket \
    --event onload --function App.onLoad --solution cwx_crmworx  # would_remove_handler: true, no PATCH

Publishing and the publish-then-read-back gotcha

These verbs stage the change by default (same as the field editors). Because GET /systemforms returns the published FormXml, a staged PATCH is invisible on re-export and in the UI — always publish (--publish, or crm solution publish-all afterward) before verifying with form list-handlers or form export.

Don't chain staged edits to one form

Each verb recomputes the FormXml from the published snapshot. A second staged (flagless) edit overwrites any pending unpublished change from the first. Either pass --publish on each edit, or publish between edits — only the last staged write survives otherwise.

crm form add-handler cwx_ticket \
    --event onload --library cwx_/scripts/ticket.js \
    --function App.onLoad --solution cwx_crmworx --publish    # publish this edit now

Handlers vs InternalHandlers

The FormXml <events> element contains two sibling blocks per event: <Handlers> (customizer-owned) and <InternalHandlers> (platform-owned). The CLI only reads and writes <Handlers>. Do not hand-splice entries into <InternalHandlers> — those are managed by the platform and survive upgrades independently.

Edit the tab & section structure

Beyond placing fields, you can edit the form's tab/section skeleton directly — no manual FormXml editing. Eight verbs cover tabs and sections symmetrically: add-tab, remove-tab, rename-tab, move-tab and add-section, remove-section, rename-section, move-section. Each PATCHes the systemform record and honors --form, --publish / --no-publish, --solution, and the global --dry-run (which returns would_add / would_remove / would_rename / would_move with no PATCH) — the same conventions and publish gotcha as the field verbs above.

Tabs

crm form add-tab cwx_ticket cwx_details --label "Details" --solution cwx_crmworx           # append a tab
crm form add-tab cwx_ticket cwx_details --after "General" --columns 2 --solution cwx_crmworx  # 2-column tab, after "General"
crm form rename-tab cwx_ticket cwx_details --label "Ticket Details" --solution cwx_crmworx # change its display label
crm form move-tab cwx_ticket cwx_details --after "General" --solution cwx_crmworx          # reorder (no --after ⇒ move to front)
crm form remove-tab cwx_ticket cwx_details --solution cwx_crmworx                          # remove

A new tab is created with a fresh internal id, IsUserDefined="1", the requested number of layout columns (--columns, 1–4, default 1), and a non-empty starter section — an empty tab is XSD-valid but renders broken. rename-tab changes only the display label; the logical name is left intact because form scripts bind to it.

remove-tab refuses to remove the only tab on the form, and refuses to remove a tab that still holds bound fields (which would orphan them) unless you pass --force — in which case the orphaned field names are surfaced in the output.

Sections

crm form add-section cwx_ticket cwx_status --tab cwx_details --label "Status" --solution cwx_crmworx
crm form add-section cwx_ticket cwx_status --tab cwx_details --after "Summary" --columns 2 --solution cwx_crmworx
crm form rename-section cwx_ticket cwx_status --tab cwx_details --label "Ticket Status" --solution cwx_crmworx
crm form move-section cwx_ticket cwx_status --tab cwx_details --solution cwx_crmworx          # reorder within the tab
crm form remove-section cwx_ticket cwx_status --tab cwx_details --solution cwx_crmworx

Sections default to the first tab when --tab is omitted, mirroring add-field. A new section gets a fresh id, IsUserDefined="1", and the requested cell-column count (--columns, 1–4, default 1). add-section is also how you create a section to target before add-field on a tab that has none. remove-section refuses an orphaning remove without --force, exactly like remove-tab.

Export a form's formxml

crm form export cwx_ticket "Ticket Main Form"      # prints formxml to stdout
crm form export cwx_ticket "Ticket Main Form" --output ticket_main.xml

export is read-only and does not require --solution. The exported XML can be inspected directly or used as a reference when authoring customizations. --output writes the XML to the given path; omitting it prints to stdout, which is convenient for piping or quick inspection.